Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/history-and-recipes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@thatopen/services': minor
---

The history and recipes APIs, at services level. `client.history(projectId)` reads and writes a project's connected documents (Revit, AutoCAD…): version timelines ordered by the parent graph, deltas, full models, and change requests (suggestions) with their lifecycle. The flow storage underneath (`bimterop` folders, `revitflow_history.json`, `revitflow_ops.json`, hidden deltas) is fully encapsulated: this surface is the only supported way in, so the layout and the future platform-level API can change without touching callers. Appends take an `expectedLatestGuid` and refuse with `StaleHistoryError` when the head moved; the remaining write race is documented and owned by the agreed platform-level phase 2.

`client.recipes` is the registry and runner for recipes: scripts that write suggestions in a native application's own language, defined by a manifest (`to`, `inputs`, `runtime`, `settingsSchema`, a self-documenting rules table) plus one `run` function. Running a recipe only queues a suggestion, so trying one is always safe.
4 changes: 4 additions & 0 deletions docs/client/paths.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
"path": "src/core/examples/hidden-files.ts",
"description": "Hidden file lifecycle — create, get, list by parent, download, and delete."
},
{
"path": "src/core/examples/history.ts",
"description": "History and recipes — a project's connected documents (Revit, AutoCAD…), their version timelines, change requests (suggestions), and recipes: scripts that write suggestions in an app's native language."
},
{
"path": "src/core/examples/icons.ts",
"description": "Item icon management — upload, download, and remove icons on any item (file, app, or component)."
Expand Down
25 changes: 25 additions & 0 deletions src/core/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { io } from 'socket.io-client';
import { ProjectHistory } from './history';
import { Recipes } from './recipes';
import {
ExecutionEntity,
ExecutionSuscriptionReturnType,
Expand Down Expand Up @@ -1348,6 +1350,29 @@ export class EngineServicesClient {
});
}

// ─── History & Recipes ───────────────────────────────────────────

/**
* A project's history of connected documents (Revit models, AutoCAD
* drawings…): their version timelines and their change requests. See
* `src/core/history.ts` — this surface is the only supported way to read
* or write it; the storage underneath is private and will move.
*/
history(projectId: string): ProjectHistory {
return new ProjectHistory(this, projectId);
}

#recipes?: Recipes;

/**
* The recipe registry and runner: scripts that write change requests in a
* native application's language. See `src/core/recipes.ts`.
*/
get recipes(): Recipes {
this.#recipes ??= new Recipes(this);
return this.#recipes;
}

// ─── Hidden Files ────────────────────────────────────────────────

/**
Expand Down
103 changes: 103 additions & 0 deletions src/core/examples/history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// description: "History and recipes — a project's connected documents (Revit, AutoCAD…), their version timelines, change requests (suggestions), and recipes: scripts that write suggestions in an app's native language."
import { EngineServicesClient } from '../client';
import type { Recipe } from '../recipes';

// The mental model, in three sentences:
// 1. Every connected document (a Revit model, an AutoCAD drawing) has its
// version history: who synced, when, what changed.
// 2. Nobody writes in somebody else's document. Changes arrive as SUGGESTIONS
// (create / modify / delete native data) that the owner accepts or
// dismisses in the native app. That is why running any of this is safe.
// 3. A RECIPE is a script that writes suggestions in an application's own
// language. Anything that wants to change a Revit document uses a Revit
// recipe to say it in Revit's words.

export async function readTheProjectHistory(
client: EngineServicesClient,
projectId: string,
) {
const history = client.history(projectId);

// Which documents are connected, and from which application.
const docs = await history.docs();
for (const { source, docId } of docs) {
const doc = history.doc(source, docId);

// The timeline is ordered by the version graph, oldest first, with
// `ordinal` filled in. The last entry is the head.
const timeline = await doc.timeline();
const head = timeline[timeline.length - 1];
console.log(
`${source}-${docId}: ${timeline.length} versions, head by ${head?.author}`,
);

// A version's geometry: full exports ARE the model file; ordinary
// versions carry a hidden delta.
if (head?.deltaId) await doc.delta(head);
else if (head?.full) await doc.model();

// The change requests waiting for the document's owner.
const pending = await doc.pendingSuggestions();
console.log(`${pending.length} suggestions pending`);
}
}

// A recipe is its manifest plus one function. The settingsSchema is what apps
// render as a form — anything naming an EXISTING thing must be an enum the
// app fills, so people pick instead of typing names.
const pointsToBlocks: Recipe<{ layer: string; block: string }> = {
name: 'points-to-autocad-blocks',
description: 'Places an AutoCAD block on every input point.',
to: 'autocad',
inputs: ['points'],
runtime: 'browser',
settingsSchema: {
type: 'object',
properties: {
layer: { type: 'string' },
block: { type: 'string' },
},
required: ['layer', 'block'],
},
rules: [{ reads: 'input points', proposes: 'one block per point' }],
async run({ input, settings }) {
const points = input as { x: number; y: number; z: number }[];
// Build the suggestion payload (a .frag with envelope revitflow/op@2,
// one item per change, attributes in the TARGET's own vocabulary).
// The payload builder lives in the fragments toolkit; omitted here.
const payload = await buildOpFrag(points, settings);
return {
title: `Place ${points.length} ${settings.block} blocks`,
verb: 'add',
targets: points.length,
payload,
ruleTallies: [{ read: points.length, proposed: points.length }],
};
},
};

export async function runARecipe(
client: EngineServicesClient,
projectId: string,
) {
client.recipes.register(pointsToBlocks);

// Catalogs filter by target application: "who can speak autocad?".
client.recipes.list({ to: 'autocad' });

// Running is always safe: it only queues a suggestion on the target
// document. Nothing changes in AutoCAD until its owner applies it there.
const { suggestion } = await client.recipes.run('points-to-autocad-blocks', {
projectId,
target: { source: 'autocad', docId: 'site-plan' },
input: [{ x: 0, y: 0, z: 0 }],
settings: { layer: 'DETECTORS', block: 'SIEMENS-FDO181' },
author: 'recipe:points-to-autocad-blocks',
});
console.log(`queued suggestion ${suggestion.opId}`);
}

declare function buildOpFrag(
points: { x: number; y: number; z: number }[],
settings: { layer: string; block: string },
): Promise<Blob>;
Loading
Loading