@snaptrude/plugin-client 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/api/index.d.ts +2 -1
- package/dist/api/index.d.ts.map +1 -1
- package/dist/index.cjs +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/upgrade-notes/0.8.0-to-0.9.0.md +61 -0
- package/upgrade-notes/index.json +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @snaptrude/plugin-client
|
|
2
2
|
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Plugin API 0.9.0 — client types/passthrough for the 0.9 surface: the namespace reorganization (deprecate-in-place aliases — `entity.*` retired, `core.zoom` → `core.camera`, new `workspace.*` and `core.mode`), the analysis wave (`analysis.weather`/`solar`/`daylight` incl. async sDA polling), heatmaps v2 (named overlays, `renderField`, `renderSurfaceGrid`, scales, hover), the constructive-geometry brep family (`core.geom.create.brepFrom*` + `design.create.massFromBrep`), `presentation.shapes` and `presentation.placedViews`, geometry reads (`getTriangulatedMeshes`, `getEnclosure`), and the spreadsheet data lane (`datasets`, plugin-sourced bindings, `addImage`, ratified `addChart` enum). See `@snaptrude/plugin-core@0.9.0` and `upgrade-notes/0.8.0-to-0.9.0.md`.
|
|
8
|
+
|
|
3
9
|
## 0.8.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/dist/api/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PluginApi, PluginCoreApi, PluginDesignApi, PluginEntityApi, PluginProgramApi, PluginPresentationApi, PluginAnalysisApi } from "@snaptrude/plugin-core";
|
|
1
|
+
import { PluginApi, PluginCoreApi, PluginDesignApi, PluginEntityApi, PluginProgramApi, PluginPresentationApi, PluginAnalysisApi, PluginWorkspaceApi } from "@snaptrude/plugin-core";
|
|
2
2
|
export declare class ClientPluginApi extends PluginApi {
|
|
3
3
|
private static instance;
|
|
4
4
|
/**
|
|
@@ -13,6 +13,7 @@ export declare class ClientPluginApi extends PluginApi {
|
|
|
13
13
|
program: PluginProgramApi;
|
|
14
14
|
presentation: PluginPresentationApi;
|
|
15
15
|
analysis: PluginAnalysisApi;
|
|
16
|
+
workspace: PluginWorkspaceApi;
|
|
16
17
|
private constructor();
|
|
17
18
|
static getInstance(): ClientPluginApi;
|
|
18
19
|
}
|
package/dist/api/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,aAAa,EACb,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,qBAAqB,EACrB,iBAAiB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,aAAa,EACb,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,qBAAqB,EACrB,iBAAiB,EACjB,kBAAkB,EACnB,MAAM,wBAAwB,CAAA;AAG/B,qBAAa,eAAgB,SAAQ,SAAS;IAC5C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAiB;IAExC;;;;;OAKG;IACI,IAAI,EAAE,aAAa,CAAA;IACnB,MAAM,EAAE,eAAe,CAAA;IACvB,MAAM,EAAE,eAAe,CAAA;IACvB,OAAO,EAAE,gBAAgB,CAAA;IACzB,YAAY,EAAE,qBAAqB,CAAA;IACnC,QAAQ,EAAE,iBAAiB,CAAA;IAC3B,SAAS,EAAE,kBAAkB,CAAA;IAEpC,OAAO;IAYP,MAAM,CAAC,WAAW,IAAI,eAAe;CAMtC"}
|
package/dist/index.cjs
CHANGED
|
@@ -257,6 +257,7 @@ var ClientPluginApi = class _ClientPluginApi extends import_plugin_core3.PluginA
|
|
|
257
257
|
this.program = createRpcNamespace("program");
|
|
258
258
|
this.presentation = createRpcNamespace("presentation");
|
|
259
259
|
this.analysis = createRpcNamespace("analysis");
|
|
260
|
+
this.workspace = createRpcNamespace("workspace");
|
|
260
261
|
}
|
|
261
262
|
static getInstance() {
|
|
262
263
|
if (!_ClientPluginApi.instance) {
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/api/index.ts","../src/host-api.ts","../src/handle-runtime.ts","../src/rpc-proxy.ts","../src/plugin-worker.ts","../src/events.ts"],"sourcesContent":["import { ClientPluginApi } from \"./api\"\n\nexport * from \"./api\"\nexport * from \"./host-api\"\nexport * from \"./plugin-worker\"\nexport * from \"./events\"\nexport {\n flushReleaseQueue,\n intern,\n rewrapResult,\n unwrapArgs,\n __setReleaseTransport,\n} from \"./handle-runtime\"\n\n// Error surface — plugins branch on `PluginError.is(e)` + `e.code`.\nexport {\n PluginError,\n PluginValidationError,\n PluginNotFoundError,\n PluginPermissionError,\n PluginHandleError,\n PluginQuotaError,\n PluginTimeoutError,\n PluginTransportError,\n PluginLifecycleError,\n PluginExecutionError,\n PluginInternalError,\n fromEnvelope,\n isErrorEnvelope,\n isPluginErrorCode,\n PLUGIN_ERROR_CODES,\n CODE_META,\n} from \"@snaptrude/plugin-core\"\nexport type {\n ErrorEnvelope,\n PluginErrorCode,\n WirePluginErrorCode,\n PluginErrorCategory,\n} from \"@snaptrude/plugin-core\"\n\n/**\n * The Snaptrude plugin client API.\n *\n * The main entry point for plugins to interact with the Snaptrude platform.\n */\nexport const snaptrude = ClientPluginApi.getInstance()\n","import {\n PluginApi,\n PluginCoreApi,\n PluginDesignApi,\n PluginEntityApi,\n PluginProgramApi,\n PluginPresentationApi,\n PluginAnalysisApi,\n} from \"@snaptrude/plugin-core\"\nimport { createRpcNamespace } from \"../rpc-proxy\"\n\nexport class ClientPluginApi extends PluginApi {\n private static instance: ClientPluginApi\n\n /**\n * Every namespace is fully remote under the all-handle model: math/geom now\n * cross to the host (values are opaque handles), so there is no in-worker\n * compute left. All dispatch through a single generic RPC Proxy. Units live\n * under `core.units`, so they ride the `core` proxy.\n */\n public core: PluginCoreApi\n public design: PluginDesignApi\n public entity: PluginEntityApi\n public program: PluginProgramApi\n public presentation: PluginPresentationApi\n public analysis: PluginAnalysisApi\n\n private constructor() {\n super()\n this.core = createRpcNamespace<PluginCoreApi>(\"core\")\n this.design = createRpcNamespace<PluginDesignApi>(\"design\")\n this.entity = createRpcNamespace<PluginEntityApi>(\"entity\")\n this.program = createRpcNamespace<PluginProgramApi>(\"program\")\n this.presentation =\n createRpcNamespace<PluginPresentationApi>(\"presentation\")\n this.analysis = createRpcNamespace<PluginAnalysisApi>(\"analysis\")\n }\n\n static getInstance(): ClientPluginApi {\n if (!ClientPluginApi.instance) {\n ClientPluginApi.instance = new ClientPluginApi()\n }\n return ClientPluginApi.instance\n }\n}\n","import * as Comlink from \"comlink\"\nimport type {\n PluginApiMethod,\n PluginApiCallPayload,\n PluginApiCallWrappedResult,\n PluginApiCallResult,\n} from \"@snaptrude/plugin-core\"\nimport { PluginError, fromEnvelope, makeClientEnvelope } from \"@snaptrude/plugin-core\"\nimport { rewrapResult } from \"./handle-runtime\"\n\nexport interface HostApi {\n call<M extends PluginApiMethod>(\n payload: PluginApiCallPayload<M>\n ): Promise<PluginApiCallWrappedResult<M>>\n}\n\nexport interface HostApiWrapped {\n call<M extends PluginApiMethod>(\n payload: PluginApiCallPayload<M>\n ): Promise<PluginApiCallResult<M>>\n}\n\nexport function createHostApi(endpoint?: Comlink.Endpoint): HostApi {\n return Comlink.wrap<HostApi>(\n endpoint ?? (globalThis as unknown as Comlink.Endpoint)\n ) as unknown as HostApi\n}\n\nlet _instance: HostApi | null = null\n\n/** TEST SEAM ONLY: replace the Comlink host instance (pass `undefined` to reset). */\nexport function __setHostApiInstance(instance?: HostApi): void {\n _instance = instance ?? null\n}\n\nexport function getHostApi(): HostApiWrapped {\n if (!_instance) {\n _instance = createHostApi()\n }\n return {\n call: async <M extends PluginApiMethod>(payload: PluginApiCallPayload<M>): Promise<PluginApiCallResult<M>> => {\n if (!_instance) {\n throw new Error(\"Host API not initialized\")\n }\n\n let result: PluginApiCallWrappedResult<M>\n try {\n result = await _instance.call(payload)\n } catch (transportErr) {\n // Comlink-level rejection: port closed, worker terminated, clone\n // failure. Never a routed failure — the router always RETURNS its\n // envelope — so normalize to a typed transport error.\n if (PluginError.is(transportErr)) throw transportErr\n throw rehydrate(\n makeClientEnvelope(\n \"TRANSPORT_LOST\",\n transportErr instanceof Error ? transportErr.message : String(transportErr),\n { methodPath: payload.method }\n )\n )\n }\n\n if (result.success) {\n // Re-wrap tagged handles ({__h: id} → interned Handle instances).\n return rewrapResult(result.data) as PluginApiCallResult<M>\n }\n\n // Structured envelope when the host provides one; legacy hosts (string\n // `error` only) degrade to UNKNOWN with the message preserved.\n throw rehydrate(\n result.errorInfo ??\n makeClientEnvelope(\"UNKNOWN\", result.error ?? \"Unknown host error\", {\n methodPath: payload.method,\n })\n )\n }\n }\n}\n\n/**\n * Envelope → typed `PluginError`, with the stack trimmed to the plugin's call\n * site (V8 only; harmless no-op elsewhere) instead of transport internals.\n */\nfunction rehydrate(envelope: Parameters<typeof fromEnvelope>[0]): PluginError {\n const error = fromEnvelope(envelope)\n ;(Error as { captureStackTrace?: (target: object, ctor: Function) => void })\n .captureStackTrace?.(error, rehydrate)\n return error\n}\n","import { Handle } from \"@snaptrude/plugin-core\"\nimport { getHostApi } from \"./host-api\"\n\n/**\n * Worker-side handle lifecycle runtime (P1):\n *\n * - **Interning** — one live `Handle` instance per id, so `===`, `Set`, and\n * `Map` keys keep working exactly as they did when handles were raw strings\n * (the host identity-dedups resource/topology handles, so the same id\n * arrives repeatedly).\n * - **FinalizationRegistry backstop** — when the plugin drops every reference\n * to a handle, its host registry entry is eventually released without any\n * author action. Eventual, not timely: deterministic release\n * (`core.handles.release` / scopes / `await using`) remains the primary tool.\n * - **Batched release queue** — finalizer hits and `Symbol.asyncDispose` calls\n * collapse into one `core.handles.release([...])` RPC per flush.\n */\n\nconst interned = new Map<string, WeakRef<Handle<string>>>()\n\nconst RELEASE_FLUSH_SIZE = 64\nconst RELEASE_FLUSH_MS = 250\nconst MAX_FLUSH_FAILURES = 5\n\nconst releaseQueue = new Set<string>()\nlet flushTimer: ReturnType<typeof setTimeout> | null = null\nlet consecutiveFlushFailures = 0\n\nconst finalizer = new FinalizationRegistry<string>((id) => {\n // A NEW wrapper for the same id may have been interned after the collected\n // one died (deduped topology re-enumeration) — releasing then would free a\n // handle the plugin still holds. Only release when no live wrapper remains.\n if (interned.get(id)?.deref()) return\n interned.delete(id)\n enqueueRelease(id)\n})\n\n/**\n * Return THE `Handle` instance for this id — the existing live wrapper when\n * present, else a fresh one wired for auto-release.\n */\nexport function intern(id: string): Handle<string> {\n // The host just (re-)vended this id, so it is live host-side. Cancel any\n // PENDING auto-release for it — covers the finalizer-already-ran window\n // (old wrapper collected, id queued, same id re-vended before the flush;\n // without this the flush would release a handle the plugin holds live).\n releaseQueue.delete(id)\n\n const existing = interned.get(id)?.deref()\n if (existing) return existing\n\n const handle = new Handle<string>(id)\n // Instance-level override shadows the class's no-op placeholder. Explicit\n // dispose also UN-interns the wrapper: (a) the flush-failure retry filter\n // skips ids with a live interned wrapper, so a still-interned explicit\n // dispose would never be retried; (b) if the host re-vends the id later,\n // a fresh wrapper is minted instead of resurrecting the disposed one.\n ;(handle as { [Symbol.asyncDispose]?: () => Promise<void> })[Symbol.asyncDispose] =\n async () => {\n if (interned.get(id)?.deref() === handle) interned.delete(id)\n enqueueRelease(id)\n }\n interned.set(id, new WeakRef(handle))\n finalizer.register(handle, id)\n return handle\n}\n\nfunction enqueueRelease(id: string): void {\n releaseQueue.add(id)\n if (releaseQueue.size >= RELEASE_FLUSH_SIZE) {\n void flushReleaseQueue()\n return\n }\n flushTimer ??= setTimeout(() => {\n void flushReleaseQueue()\n }, RELEASE_FLUSH_MS)\n}\n\n/** How a release batch reaches the host. Swappable for tests (`__setReleaseTransport`). */\nlet releaseTransport = async (batch: string[]): Promise<void> => {\n await getHostApi().call({\n method: \"core.handles.release\",\n args: [batch],\n } as never)\n}\n\n/** TEST SEAM ONLY: replace the release transport (pass `undefined` to restore). */\nexport function __setReleaseTransport(\n transport?: (batch: string[]) => Promise<void>\n): void {\n releaseTransport =\n transport ??\n (async (batch: string[]) => {\n await getHostApi().call({\n method: \"core.handles.release\",\n args: [batch],\n } as never)\n })\n}\n\n/** Exported for tests and for an eager flush before a plugin self-completes. */\nexport async function flushReleaseQueue(): Promise<void> {\n if (flushTimer) {\n clearTimeout(flushTimer)\n flushTimer = null\n }\n if (releaseQueue.size === 0) return\n const batch = [...releaseQueue]\n releaseQueue.clear()\n try {\n await releaseTransport(batch)\n consecutiveFlushFailures = 0\n } catch (err) {\n // Transient failure (rate limit, timeout): re-queue so the entries are not\n // permanently leaked host-side; the next enqueue/flush retries. Ids the\n // plugin re-acquired in the meantime were already purged by intern() and\n // must not be re-added. If the host is simply gone, the plugin is stopping\n // and registry teardown reclaims everything anyway.\n consecutiveFlushFailures += 1\n if (consecutiveFlushFailures >= MAX_FLUSH_FAILURES) {\n // Persistent failure: retrying forever would just spin. Drop the batch\n // loudly — host teardown reclaims the entries when the plugin stops.\n console.warn(\n `[snaptrude] dropping ${batch.length} handle release(s) after ${consecutiveFlushFailures} failed flushes`,\n err\n )\n consecutiveFlushFailures = 0\n return\n }\n for (const id of batch) {\n if (!interned.get(id)?.deref()) releaseQueue.add(id)\n }\n }\n}\n\n/**\n * Recursively re-wrap a host result: `{ __h: \"<id>\" }` tags (produced by\n * `Handle.toJSON` through the host's JSON-round-trip serialization) become\n * interned `Handle` instances. Arrays and plain records are walked; all other\n * values pass through untouched.\n */\nexport function rewrapResult(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(rewrapResult)\n if (value && typeof value === \"object\") {\n const record = value as Record<string, unknown>\n const keys = Object.keys(record)\n if (keys.length === 1 && keys[0] === \"__h\" && typeof record.__h === \"string\") {\n return intern(record.__h)\n }\n for (const key of keys) record[key] = rewrapResult(record[key])\n return record\n }\n return value\n}\n\n/**\n * Recursively unwrap outgoing args: `Handle` instances become their wire id\n * strings. Uses a memo Map (not a bail-out set) so shared/diamond references\n * get the SAME converted subtree — a bail-out would leak un-unwrapped Handle\n * instances through the second reference. Cycles are handled by memoizing the\n * output object before recursing. Non-plain objects pass through untouched\n * (structured clone imposes plain-data args anyway).\n */\nexport function unwrapArgs(value: unknown, memo = new Map<object, unknown>()): unknown {\n if (value instanceof Handle) return value.id\n if (value === null || typeof value !== \"object\") return value\n\n const hit = memo.get(value)\n if (hit !== undefined) return hit\n\n if (Array.isArray(value)) {\n const out: unknown[] = []\n memo.set(value, out)\n for (const item of value) out.push(unwrapArgs(item, memo))\n return out\n }\n\n const proto = Object.getPrototypeOf(value)\n if (proto !== Object.prototype && proto !== null) return value\n\n const out: Record<string, unknown> = {}\n memo.set(value, out)\n for (const [key, item] of Object.entries(value)) out[key] = unwrapArgs(item, memo)\n return out\n}\n","import type {\n PluginApiCallPayload,\n PluginApiMethod,\n} from \"@snaptrude/plugin-core\"\nimport { getHostApi } from \"./host-api\"\nimport { unwrapArgs } from \"./handle-runtime\"\n\n/**\n * Build a namespace object whose nested property access maps to a\n * dot-separated host RPC method path, and whose every call dispatches that\n * path through the host bridge.\n *\n * The host exposes the entire plugin API behind a single generic `call()`\n * (see the host `bridge.ts`), and the method string is exactly the property\n * path — so one Proxy replaces every hand-written per-method RPC wrapper for\n * every namespace (`core.*`, `design.*`, `entity.*`):\n *\n * The POSITIONAL transport forwards the whole argument tuple; the host router\n * spreads it back into the resolved method (`fn(...args)`):\n *\n * ```ts\n * snaptrude.core.math.vec3.new(1, 2, 3)\n * // → getHostApi().call({ method: \"core.math.vec3.new\", args: [1, 2, 3] })\n * ```\n *\n * Typed at the call site, e.g. `createRpcNamespace<PluginEntityApi>(\"entity\")`.\n * The Proxy is structurally cast to the abstract API type — argument and\n * return types are enforced by that type, while dispatch is dynamic.\n *\n * Every namespace uses this — including `core.math.*` and `core.geom.*`: under\n * the all-handle model there is no in-worker compute; math and geometry are\n * host calls like everything else.\n */\nexport function createRpcNamespace<T extends object>(basePath: string): T {\n const build = (path: string): unknown =>\n new Proxy(NOOP, {\n get(_target, prop) {\n // Symbols and `then` must not resolve to a callable proxy, otherwise\n // the namespace would look thenable and break Promise resolution if it\n // ever reached an `await`.\n if (typeof prop !== \"string\" || prop === \"then\") return undefined\n return build(`${path}.${prop}`)\n },\n apply(_target, _thisArg, argArray: unknown[]) {\n const payload = {\n method: path,\n // Handle instances anywhere in the tuple become their wire id strings.\n args: argArray.map((arg) => unwrapArgs(arg)),\n } as unknown as PluginApiCallPayload<PluginApiMethod>\n return getHostApi().call(payload)\n },\n })\n\n return build(basePath) as T\n}\n\n/** Proxy target must be callable for the `apply` trap; identity is irrelevant. */\nconst NOOP = (): void => {}\n","import * as Comlink from \"comlink\"\nimport type { PluginEventName } from \"./events\"\n\nexport interface UIMessage {\n action: string\n payload: unknown\n}\n\ninterface PluginConfig {\n pluginId: string\n}\n\n/**\n * Base class for Snaptrude plugin workers.\n *\n * Handles Comlink wiring, host communication, and the standard lifecycle\n * methods (`init`, `destroy`, `ping`, `onUIMessage`). Subclass this and\n * override only the methods you need — then call `start()` to expose the\n * worker API.\n *\n * The plugin ID is received automatically from the host during\n * initialization — no need to pass it manually.\n *\n * @example\n * ```ts\n * import { PluginWorker } from \"@snaptrude/plugin-client\";\n *\n * class MyPlugin extends PluginWorker {\n * async onUIMessage(message: UIMessage) {\n * // handle messages from the UI panel\n * }\n * }\n *\n * new MyPlugin().start();\n * ```\n */\nexport abstract class PluginWorker {\n protected pluginId!: string\n private hostAPI: Comlink.Remote<Record<string, unknown>>\n\n constructor() {\n this.hostAPI = Comlink.wrap<Record<string, unknown>>(\n self as unknown as Comlink.Endpoint,\n )\n }\n\n protected sendToUI(action: string, payload: unknown): void {\n ;(this.hostAPI as Record<string, any>).ui.sendToUI({ action, payload })\n }\n\n /**\n * Show a transient notification toast in the Snaptrude editor.\n *\n * @param message - Text to display.\n * @param type - Severity; `\"error\"`/`\"warning\"` linger a little longer than\n * `\"info\"`. Defaults to `\"info\"`.\n */\n protected notify(\n message: string,\n type: \"info\" | \"warning\" | \"error\" = \"info\",\n ): void {\n ;(this.hostAPI as Record<string, any>).ui.showNotification(message, type)\n }\n\n /**\n * Signal the host that this plugin has finished its work and should be\n * stopped. Use this in headless (UI-less) plugins that run a task and\n * self-terminate.\n */\n protected complete(): void {\n ;(this.hostAPI as Record<string, any>).lifecycle.complete()\n }\n\n /**\n * Subscribe to a host event (e.g. `\"model:changed\"`, fired debounced on any\n * user- or plugin-initiated model edit). The callback runs in this worker\n * each time the event fires; its argument is a small structured-clone-safe\n * payload (see the event's payload type, e.g. `ModelChangedEvent`).\n *\n * Fire-and-forget: subscribe once (typically in `init()`). Subscriptions live\n * for the plugin's lifetime and are cleared automatically when the plugin is\n * stopped — there is no `unsubscribe` yet.\n *\n * @example\n * ```ts\n * import type { ModelChangedEvent } from \"@snaptrude/plugin-client\";\n *\n * async init() {\n * this.subscribe(\"model:changed\", (e) => {\n * const { source } = e as ModelChangedEvent;\n * console.log(\"model changed via\", source);\n * });\n * }\n * ```\n */\n protected subscribe(\n event: PluginEventName,\n callback: (payload?: unknown) => void,\n ): void {\n ;(this.hostAPI as Record<string, any>).events.on(\n event,\n Comlink.proxy(callback),\n )\n }\n\n async init(): Promise<void> {\n console.log(this.pluginId, \"init() called\")\n console.log(this.pluginId, \"Initialization complete\")\n }\n\n async destroy(): Promise<void> {\n console.log(this.pluginId, \"destroy() called — cleaning up\")\n }\n\n async ping(): Promise<string> {\n return \"pong\"\n }\n\n async onUIMessage(_message: UIMessage): Promise<void> {\n // Override in subclass to handle UI messages\n }\n\n /**\n * Expose the worker API via Comlink and start listening.\n * Call this once after constructing the plugin instance.\n *\n * The host calls `init(config)` with `{ pluginId }`,\n * which is captured here to set `this.pluginId` before the\n * subclass's `init()` runs.\n */\n start(): void {\n Comlink.expose(\n {\n init: (config: PluginConfig) => {\n this.pluginId = config.pluginId\n return this.init()\n },\n destroy: () => this.destroy(),\n ping: () => this.ping(),\n onUIMessage: (message: UIMessage) => this.onUIMessage(message),\n },\n self as unknown as Comlink.Endpoint,\n )\n console.log(\"Worker loaded, API exposed via Comlink\")\n }\n}\n","/**\n * Snaptrude plugin events.\n *\n * Plugins can react to changes in the host app. Events are delivered through\n * the host event bus — a separate channel from the request/response\n * `snaptrude.*` API — and carry a small structured-clone-safe payload, never\n * live model data.\n *\n * Subscribe from a {@link PluginWorker} subclass with `this.subscribe(...)`.\n *\n * NOTE: events are a subscription surface, not `call()` RPC methods, so they do\n * NOT appear in the discovery manifest (which enumerates callable methods\n * only). This module is the typed, discoverable declaration of the surface.\n */\n\n/** Every event a plugin can subscribe to. */\nexport const PLUGIN_EVENTS = [\n \"selection:changed\",\n \"tool:activated\",\n \"project:saved\",\n \"view:changed\",\n \"model:changed\",\n] as const\n\n/** Union of subscribable event names. */\nexport type PluginEventName = (typeof PLUGIN_EVENTS)[number]\n\n/**\n * Payload for `model:changed` — fired (debounced) whenever the project model is\n * mutated, by a user edit OR a plugin edit. Deliberately minimal: it signals\n * *that* the model changed, never *what* changed. Re-query the API for details.\n */\nexport interface ModelChangedEvent {\n /** Origin of the change. Currently always `\"command\"` (the edit chokepoint). */\n source: string\n}\n"],"mappings":"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","names":["import_plugin_core","import_plugin_core","out","Comlink","import_plugin_core"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/api/index.ts","../src/host-api.ts","../src/handle-runtime.ts","../src/rpc-proxy.ts","../src/plugin-worker.ts","../src/events.ts"],"sourcesContent":["import { ClientPluginApi } from \"./api\"\n\nexport * from \"./api\"\nexport * from \"./host-api\"\nexport * from \"./plugin-worker\"\nexport * from \"./events\"\nexport {\n flushReleaseQueue,\n intern,\n rewrapResult,\n unwrapArgs,\n __setReleaseTransport,\n} from \"./handle-runtime\"\n\n// Error surface — plugins branch on `PluginError.is(e)` + `e.code`.\nexport {\n PluginError,\n PluginValidationError,\n PluginNotFoundError,\n PluginPermissionError,\n PluginHandleError,\n PluginQuotaError,\n PluginTimeoutError,\n PluginTransportError,\n PluginLifecycleError,\n PluginExecutionError,\n PluginInternalError,\n fromEnvelope,\n isErrorEnvelope,\n isPluginErrorCode,\n PLUGIN_ERROR_CODES,\n CODE_META,\n} from \"@snaptrude/plugin-core\"\nexport type {\n ErrorEnvelope,\n PluginErrorCode,\n WirePluginErrorCode,\n PluginErrorCategory,\n} from \"@snaptrude/plugin-core\"\n\n/**\n * The Snaptrude plugin client API.\n *\n * The main entry point for plugins to interact with the Snaptrude platform.\n */\nexport const snaptrude = ClientPluginApi.getInstance()\n","import {\n PluginApi,\n PluginCoreApi,\n PluginDesignApi,\n PluginEntityApi,\n PluginProgramApi,\n PluginPresentationApi,\n PluginAnalysisApi,\n PluginWorkspaceApi,\n} from \"@snaptrude/plugin-core\"\nimport { createRpcNamespace } from \"../rpc-proxy\"\n\nexport class ClientPluginApi extends PluginApi {\n private static instance: ClientPluginApi\n\n /**\n * Every namespace is fully remote under the all-handle model: math/geom now\n * cross to the host (values are opaque handles), so there is no in-worker\n * compute left. All dispatch through a single generic RPC Proxy. Units live\n * under `core.units`, so they ride the `core` proxy.\n */\n public core: PluginCoreApi\n public design: PluginDesignApi\n public entity: PluginEntityApi\n public program: PluginProgramApi\n public presentation: PluginPresentationApi\n public analysis: PluginAnalysisApi\n public workspace: PluginWorkspaceApi\n\n private constructor() {\n super()\n this.core = createRpcNamespace<PluginCoreApi>(\"core\")\n this.design = createRpcNamespace<PluginDesignApi>(\"design\")\n this.entity = createRpcNamespace<PluginEntityApi>(\"entity\")\n this.program = createRpcNamespace<PluginProgramApi>(\"program\")\n this.presentation =\n createRpcNamespace<PluginPresentationApi>(\"presentation\")\n this.analysis = createRpcNamespace<PluginAnalysisApi>(\"analysis\")\n this.workspace = createRpcNamespace<PluginWorkspaceApi>(\"workspace\")\n }\n\n static getInstance(): ClientPluginApi {\n if (!ClientPluginApi.instance) {\n ClientPluginApi.instance = new ClientPluginApi()\n }\n return ClientPluginApi.instance\n }\n}\n","import * as Comlink from \"comlink\"\nimport type {\n PluginApiMethod,\n PluginApiCallPayload,\n PluginApiCallWrappedResult,\n PluginApiCallResult,\n} from \"@snaptrude/plugin-core\"\nimport { PluginError, fromEnvelope, makeClientEnvelope } from \"@snaptrude/plugin-core\"\nimport { rewrapResult } from \"./handle-runtime\"\n\nexport interface HostApi {\n call<M extends PluginApiMethod>(\n payload: PluginApiCallPayload<M>\n ): Promise<PluginApiCallWrappedResult<M>>\n}\n\nexport interface HostApiWrapped {\n call<M extends PluginApiMethod>(\n payload: PluginApiCallPayload<M>\n ): Promise<PluginApiCallResult<M>>\n}\n\nexport function createHostApi(endpoint?: Comlink.Endpoint): HostApi {\n return Comlink.wrap<HostApi>(\n endpoint ?? (globalThis as unknown as Comlink.Endpoint)\n ) as unknown as HostApi\n}\n\nlet _instance: HostApi | null = null\n\n/** TEST SEAM ONLY: replace the Comlink host instance (pass `undefined` to reset). */\nexport function __setHostApiInstance(instance?: HostApi): void {\n _instance = instance ?? null\n}\n\nexport function getHostApi(): HostApiWrapped {\n if (!_instance) {\n _instance = createHostApi()\n }\n return {\n call: async <M extends PluginApiMethod>(payload: PluginApiCallPayload<M>): Promise<PluginApiCallResult<M>> => {\n if (!_instance) {\n throw new Error(\"Host API not initialized\")\n }\n\n let result: PluginApiCallWrappedResult<M>\n try {\n result = await _instance.call(payload)\n } catch (transportErr) {\n // Comlink-level rejection: port closed, worker terminated, clone\n // failure. Never a routed failure — the router always RETURNS its\n // envelope — so normalize to a typed transport error.\n if (PluginError.is(transportErr)) throw transportErr\n throw rehydrate(\n makeClientEnvelope(\n \"TRANSPORT_LOST\",\n transportErr instanceof Error ? transportErr.message : String(transportErr),\n { methodPath: payload.method }\n )\n )\n }\n\n if (result.success) {\n // Re-wrap tagged handles ({__h: id} → interned Handle instances).\n return rewrapResult(result.data) as PluginApiCallResult<M>\n }\n\n // Structured envelope when the host provides one; legacy hosts (string\n // `error` only) degrade to UNKNOWN with the message preserved.\n throw rehydrate(\n result.errorInfo ??\n makeClientEnvelope(\"UNKNOWN\", result.error ?? \"Unknown host error\", {\n methodPath: payload.method,\n })\n )\n }\n }\n}\n\n/**\n * Envelope → typed `PluginError`, with the stack trimmed to the plugin's call\n * site (V8 only; harmless no-op elsewhere) instead of transport internals.\n */\nfunction rehydrate(envelope: Parameters<typeof fromEnvelope>[0]): PluginError {\n const error = fromEnvelope(envelope)\n ;(Error as { captureStackTrace?: (target: object, ctor: Function) => void })\n .captureStackTrace?.(error, rehydrate)\n return error\n}\n","import { Handle } from \"@snaptrude/plugin-core\"\nimport { getHostApi } from \"./host-api\"\n\n/**\n * Worker-side handle lifecycle runtime (P1):\n *\n * - **Interning** — one live `Handle` instance per id, so `===`, `Set`, and\n * `Map` keys keep working exactly as they did when handles were raw strings\n * (the host identity-dedups resource/topology handles, so the same id\n * arrives repeatedly).\n * - **FinalizationRegistry backstop** — when the plugin drops every reference\n * to a handle, its host registry entry is eventually released without any\n * author action. Eventual, not timely: deterministic release\n * (`core.handles.release` / scopes / `await using`) remains the primary tool.\n * - **Batched release queue** — finalizer hits and `Symbol.asyncDispose` calls\n * collapse into one `core.handles.release([...])` RPC per flush.\n */\n\nconst interned = new Map<string, WeakRef<Handle<string>>>()\n\nconst RELEASE_FLUSH_SIZE = 64\nconst RELEASE_FLUSH_MS = 250\nconst MAX_FLUSH_FAILURES = 5\n\nconst releaseQueue = new Set<string>()\nlet flushTimer: ReturnType<typeof setTimeout> | null = null\nlet consecutiveFlushFailures = 0\n\nconst finalizer = new FinalizationRegistry<string>((id) => {\n // A NEW wrapper for the same id may have been interned after the collected\n // one died (deduped topology re-enumeration) — releasing then would free a\n // handle the plugin still holds. Only release when no live wrapper remains.\n if (interned.get(id)?.deref()) return\n interned.delete(id)\n enqueueRelease(id)\n})\n\n/**\n * Return THE `Handle` instance for this id — the existing live wrapper when\n * present, else a fresh one wired for auto-release.\n */\nexport function intern(id: string): Handle<string> {\n // The host just (re-)vended this id, so it is live host-side. Cancel any\n // PENDING auto-release for it — covers the finalizer-already-ran window\n // (old wrapper collected, id queued, same id re-vended before the flush;\n // without this the flush would release a handle the plugin holds live).\n releaseQueue.delete(id)\n\n const existing = interned.get(id)?.deref()\n if (existing) return existing\n\n const handle = new Handle<string>(id)\n // Instance-level override shadows the class's no-op placeholder. Explicit\n // dispose also UN-interns the wrapper: (a) the flush-failure retry filter\n // skips ids with a live interned wrapper, so a still-interned explicit\n // dispose would never be retried; (b) if the host re-vends the id later,\n // a fresh wrapper is minted instead of resurrecting the disposed one.\n ;(handle as { [Symbol.asyncDispose]?: () => Promise<void> })[Symbol.asyncDispose] =\n async () => {\n if (interned.get(id)?.deref() === handle) interned.delete(id)\n enqueueRelease(id)\n }\n interned.set(id, new WeakRef(handle))\n finalizer.register(handle, id)\n return handle\n}\n\nfunction enqueueRelease(id: string): void {\n releaseQueue.add(id)\n if (releaseQueue.size >= RELEASE_FLUSH_SIZE) {\n void flushReleaseQueue()\n return\n }\n flushTimer ??= setTimeout(() => {\n void flushReleaseQueue()\n }, RELEASE_FLUSH_MS)\n}\n\n/** How a release batch reaches the host. Swappable for tests (`__setReleaseTransport`). */\nlet releaseTransport = async (batch: string[]): Promise<void> => {\n await getHostApi().call({\n method: \"core.handles.release\",\n args: [batch],\n } as never)\n}\n\n/** TEST SEAM ONLY: replace the release transport (pass `undefined` to restore). */\nexport function __setReleaseTransport(\n transport?: (batch: string[]) => Promise<void>\n): void {\n releaseTransport =\n transport ??\n (async (batch: string[]) => {\n await getHostApi().call({\n method: \"core.handles.release\",\n args: [batch],\n } as never)\n })\n}\n\n/** Exported for tests and for an eager flush before a plugin self-completes. */\nexport async function flushReleaseQueue(): Promise<void> {\n if (flushTimer) {\n clearTimeout(flushTimer)\n flushTimer = null\n }\n if (releaseQueue.size === 0) return\n const batch = [...releaseQueue]\n releaseQueue.clear()\n try {\n await releaseTransport(batch)\n consecutiveFlushFailures = 0\n } catch (err) {\n // Transient failure (rate limit, timeout): re-queue so the entries are not\n // permanently leaked host-side; the next enqueue/flush retries. Ids the\n // plugin re-acquired in the meantime were already purged by intern() and\n // must not be re-added. If the host is simply gone, the plugin is stopping\n // and registry teardown reclaims everything anyway.\n consecutiveFlushFailures += 1\n if (consecutiveFlushFailures >= MAX_FLUSH_FAILURES) {\n // Persistent failure: retrying forever would just spin. Drop the batch\n // loudly — host teardown reclaims the entries when the plugin stops.\n console.warn(\n `[snaptrude] dropping ${batch.length} handle release(s) after ${consecutiveFlushFailures} failed flushes`,\n err\n )\n consecutiveFlushFailures = 0\n return\n }\n for (const id of batch) {\n if (!interned.get(id)?.deref()) releaseQueue.add(id)\n }\n }\n}\n\n/**\n * Recursively re-wrap a host result: `{ __h: \"<id>\" }` tags (produced by\n * `Handle.toJSON` through the host's JSON-round-trip serialization) become\n * interned `Handle` instances. Arrays and plain records are walked; all other\n * values pass through untouched.\n */\nexport function rewrapResult(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(rewrapResult)\n if (value && typeof value === \"object\") {\n const record = value as Record<string, unknown>\n const keys = Object.keys(record)\n if (keys.length === 1 && keys[0] === \"__h\" && typeof record.__h === \"string\") {\n return intern(record.__h)\n }\n for (const key of keys) record[key] = rewrapResult(record[key])\n return record\n }\n return value\n}\n\n/**\n * Recursively unwrap outgoing args: `Handle` instances become their wire id\n * strings. Uses a memo Map (not a bail-out set) so shared/diamond references\n * get the SAME converted subtree — a bail-out would leak un-unwrapped Handle\n * instances through the second reference. Cycles are handled by memoizing the\n * output object before recursing. Non-plain objects pass through untouched\n * (structured clone imposes plain-data args anyway).\n */\nexport function unwrapArgs(value: unknown, memo = new Map<object, unknown>()): unknown {\n if (value instanceof Handle) return value.id\n if (value === null || typeof value !== \"object\") return value\n\n const hit = memo.get(value)\n if (hit !== undefined) return hit\n\n if (Array.isArray(value)) {\n const out: unknown[] = []\n memo.set(value, out)\n for (const item of value) out.push(unwrapArgs(item, memo))\n return out\n }\n\n const proto = Object.getPrototypeOf(value)\n if (proto !== Object.prototype && proto !== null) return value\n\n const out: Record<string, unknown> = {}\n memo.set(value, out)\n for (const [key, item] of Object.entries(value)) out[key] = unwrapArgs(item, memo)\n return out\n}\n","import type {\n PluginApiCallPayload,\n PluginApiMethod,\n} from \"@snaptrude/plugin-core\"\nimport { getHostApi } from \"./host-api\"\nimport { unwrapArgs } from \"./handle-runtime\"\n\n/**\n * Build a namespace object whose nested property access maps to a\n * dot-separated host RPC method path, and whose every call dispatches that\n * path through the host bridge.\n *\n * The host exposes the entire plugin API behind a single generic `call()`\n * (see the host `bridge.ts`), and the method string is exactly the property\n * path — so one Proxy replaces every hand-written per-method RPC wrapper for\n * every namespace (`core.*`, `design.*`, `entity.*`):\n *\n * The POSITIONAL transport forwards the whole argument tuple; the host router\n * spreads it back into the resolved method (`fn(...args)`):\n *\n * ```ts\n * snaptrude.core.math.vec3.new(1, 2, 3)\n * // → getHostApi().call({ method: \"core.math.vec3.new\", args: [1, 2, 3] })\n * ```\n *\n * Typed at the call site, e.g. `createRpcNamespace<PluginEntityApi>(\"entity\")`.\n * The Proxy is structurally cast to the abstract API type — argument and\n * return types are enforced by that type, while dispatch is dynamic.\n *\n * Every namespace uses this — including `core.math.*` and `core.geom.*`: under\n * the all-handle model there is no in-worker compute; math and geometry are\n * host calls like everything else.\n */\nexport function createRpcNamespace<T extends object>(basePath: string): T {\n const build = (path: string): unknown =>\n new Proxy(NOOP, {\n get(_target, prop) {\n // Symbols and `then` must not resolve to a callable proxy, otherwise\n // the namespace would look thenable and break Promise resolution if it\n // ever reached an `await`.\n if (typeof prop !== \"string\" || prop === \"then\") return undefined\n return build(`${path}.${prop}`)\n },\n apply(_target, _thisArg, argArray: unknown[]) {\n const payload = {\n method: path,\n // Handle instances anywhere in the tuple become their wire id strings.\n args: argArray.map((arg) => unwrapArgs(arg)),\n } as unknown as PluginApiCallPayload<PluginApiMethod>\n return getHostApi().call(payload)\n },\n })\n\n return build(basePath) as T\n}\n\n/** Proxy target must be callable for the `apply` trap; identity is irrelevant. */\nconst NOOP = (): void => {}\n","import * as Comlink from \"comlink\"\nimport type { PluginEventName } from \"./events\"\n\nexport interface UIMessage {\n action: string\n payload: unknown\n}\n\ninterface PluginConfig {\n pluginId: string\n}\n\n/**\n * Base class for Snaptrude plugin workers.\n *\n * Handles Comlink wiring, host communication, and the standard lifecycle\n * methods (`init`, `destroy`, `ping`, `onUIMessage`). Subclass this and\n * override only the methods you need — then call `start()` to expose the\n * worker API.\n *\n * The plugin ID is received automatically from the host during\n * initialization — no need to pass it manually.\n *\n * @example\n * ```ts\n * import { PluginWorker } from \"@snaptrude/plugin-client\";\n *\n * class MyPlugin extends PluginWorker {\n * async onUIMessage(message: UIMessage) {\n * // handle messages from the UI panel\n * }\n * }\n *\n * new MyPlugin().start();\n * ```\n */\nexport abstract class PluginWorker {\n protected pluginId!: string\n private hostAPI: Comlink.Remote<Record<string, unknown>>\n\n constructor() {\n this.hostAPI = Comlink.wrap<Record<string, unknown>>(\n self as unknown as Comlink.Endpoint,\n )\n }\n\n protected sendToUI(action: string, payload: unknown): void {\n ;(this.hostAPI as Record<string, any>).ui.sendToUI({ action, payload })\n }\n\n /**\n * Show a transient notification toast in the Snaptrude editor.\n *\n * @param message - Text to display.\n * @param type - Severity; `\"error\"`/`\"warning\"` linger a little longer than\n * `\"info\"`. Defaults to `\"info\"`.\n */\n protected notify(\n message: string,\n type: \"info\" | \"warning\" | \"error\" = \"info\",\n ): void {\n ;(this.hostAPI as Record<string, any>).ui.showNotification(message, type)\n }\n\n /**\n * Signal the host that this plugin has finished its work and should be\n * stopped. Use this in headless (UI-less) plugins that run a task and\n * self-terminate.\n */\n protected complete(): void {\n ;(this.hostAPI as Record<string, any>).lifecycle.complete()\n }\n\n /**\n * Subscribe to a host event (e.g. `\"model:changed\"`, fired debounced on any\n * user- or plugin-initiated model edit). The callback runs in this worker\n * each time the event fires; its argument is a small structured-clone-safe\n * payload (see the event's payload type, e.g. `ModelChangedEvent`).\n *\n * Fire-and-forget: subscribe once (typically in `init()`). Subscriptions live\n * for the plugin's lifetime and are cleared automatically when the plugin is\n * stopped — there is no `unsubscribe` yet.\n *\n * @example\n * ```ts\n * import type { ModelChangedEvent } from \"@snaptrude/plugin-client\";\n *\n * async init() {\n * this.subscribe(\"model:changed\", (e) => {\n * const { source } = e as ModelChangedEvent;\n * console.log(\"model changed via\", source);\n * });\n * }\n * ```\n */\n protected subscribe(\n event: PluginEventName,\n callback: (payload?: unknown) => void,\n ): void {\n ;(this.hostAPI as Record<string, any>).events.on(\n event,\n Comlink.proxy(callback),\n )\n }\n\n async init(): Promise<void> {\n console.log(this.pluginId, \"init() called\")\n console.log(this.pluginId, \"Initialization complete\")\n }\n\n async destroy(): Promise<void> {\n console.log(this.pluginId, \"destroy() called — cleaning up\")\n }\n\n async ping(): Promise<string> {\n return \"pong\"\n }\n\n async onUIMessage(_message: UIMessage): Promise<void> {\n // Override in subclass to handle UI messages\n }\n\n /**\n * Expose the worker API via Comlink and start listening.\n * Call this once after constructing the plugin instance.\n *\n * The host calls `init(config)` with `{ pluginId }`,\n * which is captured here to set `this.pluginId` before the\n * subclass's `init()` runs.\n */\n start(): void {\n Comlink.expose(\n {\n init: (config: PluginConfig) => {\n this.pluginId = config.pluginId\n return this.init()\n },\n destroy: () => this.destroy(),\n ping: () => this.ping(),\n onUIMessage: (message: UIMessage) => this.onUIMessage(message),\n },\n self as unknown as Comlink.Endpoint,\n )\n console.log(\"Worker loaded, API exposed via Comlink\")\n }\n}\n","/**\n * Snaptrude plugin events.\n *\n * Plugins can react to changes in the host app. Events are delivered through\n * the host event bus — a separate channel from the request/response\n * `snaptrude.*` API — and carry a small structured-clone-safe payload, never\n * live model data.\n *\n * Subscribe from a {@link PluginWorker} subclass with `this.subscribe(...)`.\n *\n * NOTE: events are a subscription surface, not `call()` RPC methods, so they do\n * NOT appear in the discovery manifest (which enumerates callable methods\n * only). This module is the typed, discoverable declaration of the surface.\n */\n\n/** Every event a plugin can subscribe to. */\nexport const PLUGIN_EVENTS = [\n \"selection:changed\",\n \"tool:activated\",\n \"project:saved\",\n \"view:changed\",\n \"model:changed\",\n] as const\n\n/** Union of subscribable event names. */\nexport type PluginEventName = (typeof PLUGIN_EVENTS)[number]\n\n/**\n * Payload for `model:changed` — fired (debounced) whenever the project model is\n * mutated, by a user edit OR a plugin edit. Deliberately minimal: it signals\n * *that* the model changed, never *what* changed. Re-query the API for details.\n */\nexport interface ModelChangedEvent {\n /** Origin of the change. Currently always `\"command\"` (the edit chokepoint). */\n source: string\n}\n"],"mappings":"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","names":["import_plugin_core","import_plugin_core","out","Comlink","import_plugin_core"]}
|
package/dist/index.js
CHANGED
|
@@ -196,6 +196,7 @@ var ClientPluginApi = class _ClientPluginApi extends PluginApi {
|
|
|
196
196
|
this.program = createRpcNamespace("program");
|
|
197
197
|
this.presentation = createRpcNamespace("presentation");
|
|
198
198
|
this.analysis = createRpcNamespace("analysis");
|
|
199
|
+
this.workspace = createRpcNamespace("workspace");
|
|
199
200
|
}
|
|
200
201
|
static getInstance() {
|
|
201
202
|
if (!_ClientPluginApi.instance) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/api/index.ts","../src/host-api.ts","../src/handle-runtime.ts","../src/rpc-proxy.ts","../src/plugin-worker.ts","../src/events.ts","../src/index.ts"],"sourcesContent":["import {\n PluginApi,\n PluginCoreApi,\n PluginDesignApi,\n PluginEntityApi,\n PluginProgramApi,\n PluginPresentationApi,\n PluginAnalysisApi,\n} from \"@snaptrude/plugin-core\"\nimport { createRpcNamespace } from \"../rpc-proxy\"\n\nexport class ClientPluginApi extends PluginApi {\n private static instance: ClientPluginApi\n\n /**\n * Every namespace is fully remote under the all-handle model: math/geom now\n * cross to the host (values are opaque handles), so there is no in-worker\n * compute left. All dispatch through a single generic RPC Proxy. Units live\n * under `core.units`, so they ride the `core` proxy.\n */\n public core: PluginCoreApi\n public design: PluginDesignApi\n public entity: PluginEntityApi\n public program: PluginProgramApi\n public presentation: PluginPresentationApi\n public analysis: PluginAnalysisApi\n\n private constructor() {\n super()\n this.core = createRpcNamespace<PluginCoreApi>(\"core\")\n this.design = createRpcNamespace<PluginDesignApi>(\"design\")\n this.entity = createRpcNamespace<PluginEntityApi>(\"entity\")\n this.program = createRpcNamespace<PluginProgramApi>(\"program\")\n this.presentation =\n createRpcNamespace<PluginPresentationApi>(\"presentation\")\n this.analysis = createRpcNamespace<PluginAnalysisApi>(\"analysis\")\n }\n\n static getInstance(): ClientPluginApi {\n if (!ClientPluginApi.instance) {\n ClientPluginApi.instance = new ClientPluginApi()\n }\n return ClientPluginApi.instance\n }\n}\n","import * as Comlink from \"comlink\"\nimport type {\n PluginApiMethod,\n PluginApiCallPayload,\n PluginApiCallWrappedResult,\n PluginApiCallResult,\n} from \"@snaptrude/plugin-core\"\nimport { PluginError, fromEnvelope, makeClientEnvelope } from \"@snaptrude/plugin-core\"\nimport { rewrapResult } from \"./handle-runtime\"\n\nexport interface HostApi {\n call<M extends PluginApiMethod>(\n payload: PluginApiCallPayload<M>\n ): Promise<PluginApiCallWrappedResult<M>>\n}\n\nexport interface HostApiWrapped {\n call<M extends PluginApiMethod>(\n payload: PluginApiCallPayload<M>\n ): Promise<PluginApiCallResult<M>>\n}\n\nexport function createHostApi(endpoint?: Comlink.Endpoint): HostApi {\n return Comlink.wrap<HostApi>(\n endpoint ?? (globalThis as unknown as Comlink.Endpoint)\n ) as unknown as HostApi\n}\n\nlet _instance: HostApi | null = null\n\n/** TEST SEAM ONLY: replace the Comlink host instance (pass `undefined` to reset). */\nexport function __setHostApiInstance(instance?: HostApi): void {\n _instance = instance ?? null\n}\n\nexport function getHostApi(): HostApiWrapped {\n if (!_instance) {\n _instance = createHostApi()\n }\n return {\n call: async <M extends PluginApiMethod>(payload: PluginApiCallPayload<M>): Promise<PluginApiCallResult<M>> => {\n if (!_instance) {\n throw new Error(\"Host API not initialized\")\n }\n\n let result: PluginApiCallWrappedResult<M>\n try {\n result = await _instance.call(payload)\n } catch (transportErr) {\n // Comlink-level rejection: port closed, worker terminated, clone\n // failure. Never a routed failure — the router always RETURNS its\n // envelope — so normalize to a typed transport error.\n if (PluginError.is(transportErr)) throw transportErr\n throw rehydrate(\n makeClientEnvelope(\n \"TRANSPORT_LOST\",\n transportErr instanceof Error ? transportErr.message : String(transportErr),\n { methodPath: payload.method }\n )\n )\n }\n\n if (result.success) {\n // Re-wrap tagged handles ({__h: id} → interned Handle instances).\n return rewrapResult(result.data) as PluginApiCallResult<M>\n }\n\n // Structured envelope when the host provides one; legacy hosts (string\n // `error` only) degrade to UNKNOWN with the message preserved.\n throw rehydrate(\n result.errorInfo ??\n makeClientEnvelope(\"UNKNOWN\", result.error ?? \"Unknown host error\", {\n methodPath: payload.method,\n })\n )\n }\n }\n}\n\n/**\n * Envelope → typed `PluginError`, with the stack trimmed to the plugin's call\n * site (V8 only; harmless no-op elsewhere) instead of transport internals.\n */\nfunction rehydrate(envelope: Parameters<typeof fromEnvelope>[0]): PluginError {\n const error = fromEnvelope(envelope)\n ;(Error as { captureStackTrace?: (target: object, ctor: Function) => void })\n .captureStackTrace?.(error, rehydrate)\n return error\n}\n","import { Handle } from \"@snaptrude/plugin-core\"\nimport { getHostApi } from \"./host-api\"\n\n/**\n * Worker-side handle lifecycle runtime (P1):\n *\n * - **Interning** — one live `Handle` instance per id, so `===`, `Set`, and\n * `Map` keys keep working exactly as they did when handles were raw strings\n * (the host identity-dedups resource/topology handles, so the same id\n * arrives repeatedly).\n * - **FinalizationRegistry backstop** — when the plugin drops every reference\n * to a handle, its host registry entry is eventually released without any\n * author action. Eventual, not timely: deterministic release\n * (`core.handles.release` / scopes / `await using`) remains the primary tool.\n * - **Batched release queue** — finalizer hits and `Symbol.asyncDispose` calls\n * collapse into one `core.handles.release([...])` RPC per flush.\n */\n\nconst interned = new Map<string, WeakRef<Handle<string>>>()\n\nconst RELEASE_FLUSH_SIZE = 64\nconst RELEASE_FLUSH_MS = 250\nconst MAX_FLUSH_FAILURES = 5\n\nconst releaseQueue = new Set<string>()\nlet flushTimer: ReturnType<typeof setTimeout> | null = null\nlet consecutiveFlushFailures = 0\n\nconst finalizer = new FinalizationRegistry<string>((id) => {\n // A NEW wrapper for the same id may have been interned after the collected\n // one died (deduped topology re-enumeration) — releasing then would free a\n // handle the plugin still holds. Only release when no live wrapper remains.\n if (interned.get(id)?.deref()) return\n interned.delete(id)\n enqueueRelease(id)\n})\n\n/**\n * Return THE `Handle` instance for this id — the existing live wrapper when\n * present, else a fresh one wired for auto-release.\n */\nexport function intern(id: string): Handle<string> {\n // The host just (re-)vended this id, so it is live host-side. Cancel any\n // PENDING auto-release for it — covers the finalizer-already-ran window\n // (old wrapper collected, id queued, same id re-vended before the flush;\n // without this the flush would release a handle the plugin holds live).\n releaseQueue.delete(id)\n\n const existing = interned.get(id)?.deref()\n if (existing) return existing\n\n const handle = new Handle<string>(id)\n // Instance-level override shadows the class's no-op placeholder. Explicit\n // dispose also UN-interns the wrapper: (a) the flush-failure retry filter\n // skips ids with a live interned wrapper, so a still-interned explicit\n // dispose would never be retried; (b) if the host re-vends the id later,\n // a fresh wrapper is minted instead of resurrecting the disposed one.\n ;(handle as { [Symbol.asyncDispose]?: () => Promise<void> })[Symbol.asyncDispose] =\n async () => {\n if (interned.get(id)?.deref() === handle) interned.delete(id)\n enqueueRelease(id)\n }\n interned.set(id, new WeakRef(handle))\n finalizer.register(handle, id)\n return handle\n}\n\nfunction enqueueRelease(id: string): void {\n releaseQueue.add(id)\n if (releaseQueue.size >= RELEASE_FLUSH_SIZE) {\n void flushReleaseQueue()\n return\n }\n flushTimer ??= setTimeout(() => {\n void flushReleaseQueue()\n }, RELEASE_FLUSH_MS)\n}\n\n/** How a release batch reaches the host. Swappable for tests (`__setReleaseTransport`). */\nlet releaseTransport = async (batch: string[]): Promise<void> => {\n await getHostApi().call({\n method: \"core.handles.release\",\n args: [batch],\n } as never)\n}\n\n/** TEST SEAM ONLY: replace the release transport (pass `undefined` to restore). */\nexport function __setReleaseTransport(\n transport?: (batch: string[]) => Promise<void>\n): void {\n releaseTransport =\n transport ??\n (async (batch: string[]) => {\n await getHostApi().call({\n method: \"core.handles.release\",\n args: [batch],\n } as never)\n })\n}\n\n/** Exported for tests and for an eager flush before a plugin self-completes. */\nexport async function flushReleaseQueue(): Promise<void> {\n if (flushTimer) {\n clearTimeout(flushTimer)\n flushTimer = null\n }\n if (releaseQueue.size === 0) return\n const batch = [...releaseQueue]\n releaseQueue.clear()\n try {\n await releaseTransport(batch)\n consecutiveFlushFailures = 0\n } catch (err) {\n // Transient failure (rate limit, timeout): re-queue so the entries are not\n // permanently leaked host-side; the next enqueue/flush retries. Ids the\n // plugin re-acquired in the meantime were already purged by intern() and\n // must not be re-added. If the host is simply gone, the plugin is stopping\n // and registry teardown reclaims everything anyway.\n consecutiveFlushFailures += 1\n if (consecutiveFlushFailures >= MAX_FLUSH_FAILURES) {\n // Persistent failure: retrying forever would just spin. Drop the batch\n // loudly — host teardown reclaims the entries when the plugin stops.\n console.warn(\n `[snaptrude] dropping ${batch.length} handle release(s) after ${consecutiveFlushFailures} failed flushes`,\n err\n )\n consecutiveFlushFailures = 0\n return\n }\n for (const id of batch) {\n if (!interned.get(id)?.deref()) releaseQueue.add(id)\n }\n }\n}\n\n/**\n * Recursively re-wrap a host result: `{ __h: \"<id>\" }` tags (produced by\n * `Handle.toJSON` through the host's JSON-round-trip serialization) become\n * interned `Handle` instances. Arrays and plain records are walked; all other\n * values pass through untouched.\n */\nexport function rewrapResult(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(rewrapResult)\n if (value && typeof value === \"object\") {\n const record = value as Record<string, unknown>\n const keys = Object.keys(record)\n if (keys.length === 1 && keys[0] === \"__h\" && typeof record.__h === \"string\") {\n return intern(record.__h)\n }\n for (const key of keys) record[key] = rewrapResult(record[key])\n return record\n }\n return value\n}\n\n/**\n * Recursively unwrap outgoing args: `Handle` instances become their wire id\n * strings. Uses a memo Map (not a bail-out set) so shared/diamond references\n * get the SAME converted subtree — a bail-out would leak un-unwrapped Handle\n * instances through the second reference. Cycles are handled by memoizing the\n * output object before recursing. Non-plain objects pass through untouched\n * (structured clone imposes plain-data args anyway).\n */\nexport function unwrapArgs(value: unknown, memo = new Map<object, unknown>()): unknown {\n if (value instanceof Handle) return value.id\n if (value === null || typeof value !== \"object\") return value\n\n const hit = memo.get(value)\n if (hit !== undefined) return hit\n\n if (Array.isArray(value)) {\n const out: unknown[] = []\n memo.set(value, out)\n for (const item of value) out.push(unwrapArgs(item, memo))\n return out\n }\n\n const proto = Object.getPrototypeOf(value)\n if (proto !== Object.prototype && proto !== null) return value\n\n const out: Record<string, unknown> = {}\n memo.set(value, out)\n for (const [key, item] of Object.entries(value)) out[key] = unwrapArgs(item, memo)\n return out\n}\n","import type {\n PluginApiCallPayload,\n PluginApiMethod,\n} from \"@snaptrude/plugin-core\"\nimport { getHostApi } from \"./host-api\"\nimport { unwrapArgs } from \"./handle-runtime\"\n\n/**\n * Build a namespace object whose nested property access maps to a\n * dot-separated host RPC method path, and whose every call dispatches that\n * path through the host bridge.\n *\n * The host exposes the entire plugin API behind a single generic `call()`\n * (see the host `bridge.ts`), and the method string is exactly the property\n * path — so one Proxy replaces every hand-written per-method RPC wrapper for\n * every namespace (`core.*`, `design.*`, `entity.*`):\n *\n * The POSITIONAL transport forwards the whole argument tuple; the host router\n * spreads it back into the resolved method (`fn(...args)`):\n *\n * ```ts\n * snaptrude.core.math.vec3.new(1, 2, 3)\n * // → getHostApi().call({ method: \"core.math.vec3.new\", args: [1, 2, 3] })\n * ```\n *\n * Typed at the call site, e.g. `createRpcNamespace<PluginEntityApi>(\"entity\")`.\n * The Proxy is structurally cast to the abstract API type — argument and\n * return types are enforced by that type, while dispatch is dynamic.\n *\n * Every namespace uses this — including `core.math.*` and `core.geom.*`: under\n * the all-handle model there is no in-worker compute; math and geometry are\n * host calls like everything else.\n */\nexport function createRpcNamespace<T extends object>(basePath: string): T {\n const build = (path: string): unknown =>\n new Proxy(NOOP, {\n get(_target, prop) {\n // Symbols and `then` must not resolve to a callable proxy, otherwise\n // the namespace would look thenable and break Promise resolution if it\n // ever reached an `await`.\n if (typeof prop !== \"string\" || prop === \"then\") return undefined\n return build(`${path}.${prop}`)\n },\n apply(_target, _thisArg, argArray: unknown[]) {\n const payload = {\n method: path,\n // Handle instances anywhere in the tuple become their wire id strings.\n args: argArray.map((arg) => unwrapArgs(arg)),\n } as unknown as PluginApiCallPayload<PluginApiMethod>\n return getHostApi().call(payload)\n },\n })\n\n return build(basePath) as T\n}\n\n/** Proxy target must be callable for the `apply` trap; identity is irrelevant. */\nconst NOOP = (): void => {}\n","import * as Comlink from \"comlink\"\nimport type { PluginEventName } from \"./events\"\n\nexport interface UIMessage {\n action: string\n payload: unknown\n}\n\ninterface PluginConfig {\n pluginId: string\n}\n\n/**\n * Base class for Snaptrude plugin workers.\n *\n * Handles Comlink wiring, host communication, and the standard lifecycle\n * methods (`init`, `destroy`, `ping`, `onUIMessage`). Subclass this and\n * override only the methods you need — then call `start()` to expose the\n * worker API.\n *\n * The plugin ID is received automatically from the host during\n * initialization — no need to pass it manually.\n *\n * @example\n * ```ts\n * import { PluginWorker } from \"@snaptrude/plugin-client\";\n *\n * class MyPlugin extends PluginWorker {\n * async onUIMessage(message: UIMessage) {\n * // handle messages from the UI panel\n * }\n * }\n *\n * new MyPlugin().start();\n * ```\n */\nexport abstract class PluginWorker {\n protected pluginId!: string\n private hostAPI: Comlink.Remote<Record<string, unknown>>\n\n constructor() {\n this.hostAPI = Comlink.wrap<Record<string, unknown>>(\n self as unknown as Comlink.Endpoint,\n )\n }\n\n protected sendToUI(action: string, payload: unknown): void {\n ;(this.hostAPI as Record<string, any>).ui.sendToUI({ action, payload })\n }\n\n /**\n * Show a transient notification toast in the Snaptrude editor.\n *\n * @param message - Text to display.\n * @param type - Severity; `\"error\"`/`\"warning\"` linger a little longer than\n * `\"info\"`. Defaults to `\"info\"`.\n */\n protected notify(\n message: string,\n type: \"info\" | \"warning\" | \"error\" = \"info\",\n ): void {\n ;(this.hostAPI as Record<string, any>).ui.showNotification(message, type)\n }\n\n /**\n * Signal the host that this plugin has finished its work and should be\n * stopped. Use this in headless (UI-less) plugins that run a task and\n * self-terminate.\n */\n protected complete(): void {\n ;(this.hostAPI as Record<string, any>).lifecycle.complete()\n }\n\n /**\n * Subscribe to a host event (e.g. `\"model:changed\"`, fired debounced on any\n * user- or plugin-initiated model edit). The callback runs in this worker\n * each time the event fires; its argument is a small structured-clone-safe\n * payload (see the event's payload type, e.g. `ModelChangedEvent`).\n *\n * Fire-and-forget: subscribe once (typically in `init()`). Subscriptions live\n * for the plugin's lifetime and are cleared automatically when the plugin is\n * stopped — there is no `unsubscribe` yet.\n *\n * @example\n * ```ts\n * import type { ModelChangedEvent } from \"@snaptrude/plugin-client\";\n *\n * async init() {\n * this.subscribe(\"model:changed\", (e) => {\n * const { source } = e as ModelChangedEvent;\n * console.log(\"model changed via\", source);\n * });\n * }\n * ```\n */\n protected subscribe(\n event: PluginEventName,\n callback: (payload?: unknown) => void,\n ): void {\n ;(this.hostAPI as Record<string, any>).events.on(\n event,\n Comlink.proxy(callback),\n )\n }\n\n async init(): Promise<void> {\n console.log(this.pluginId, \"init() called\")\n console.log(this.pluginId, \"Initialization complete\")\n }\n\n async destroy(): Promise<void> {\n console.log(this.pluginId, \"destroy() called — cleaning up\")\n }\n\n async ping(): Promise<string> {\n return \"pong\"\n }\n\n async onUIMessage(_message: UIMessage): Promise<void> {\n // Override in subclass to handle UI messages\n }\n\n /**\n * Expose the worker API via Comlink and start listening.\n * Call this once after constructing the plugin instance.\n *\n * The host calls `init(config)` with `{ pluginId }`,\n * which is captured here to set `this.pluginId` before the\n * subclass's `init()` runs.\n */\n start(): void {\n Comlink.expose(\n {\n init: (config: PluginConfig) => {\n this.pluginId = config.pluginId\n return this.init()\n },\n destroy: () => this.destroy(),\n ping: () => this.ping(),\n onUIMessage: (message: UIMessage) => this.onUIMessage(message),\n },\n self as unknown as Comlink.Endpoint,\n )\n console.log(\"Worker loaded, API exposed via Comlink\")\n }\n}\n","/**\n * Snaptrude plugin events.\n *\n * Plugins can react to changes in the host app. Events are delivered through\n * the host event bus — a separate channel from the request/response\n * `snaptrude.*` API — and carry a small structured-clone-safe payload, never\n * live model data.\n *\n * Subscribe from a {@link PluginWorker} subclass with `this.subscribe(...)`.\n *\n * NOTE: events are a subscription surface, not `call()` RPC methods, so they do\n * NOT appear in the discovery manifest (which enumerates callable methods\n * only). This module is the typed, discoverable declaration of the surface.\n */\n\n/** Every event a plugin can subscribe to. */\nexport const PLUGIN_EVENTS = [\n \"selection:changed\",\n \"tool:activated\",\n \"project:saved\",\n \"view:changed\",\n \"model:changed\",\n] as const\n\n/** Union of subscribable event names. */\nexport type PluginEventName = (typeof PLUGIN_EVENTS)[number]\n\n/**\n * Payload for `model:changed` — fired (debounced) whenever the project model is\n * mutated, by a user edit OR a plugin edit. Deliberately minimal: it signals\n * *that* the model changed, never *what* changed. Re-query the API for details.\n */\nexport interface ModelChangedEvent {\n /** Origin of the change. Currently always `\"command\"` (the edit chokepoint). */\n source: string\n}\n","import { ClientPluginApi } from \"./api\"\n\nexport * from \"./api\"\nexport * from \"./host-api\"\nexport * from \"./plugin-worker\"\nexport * from \"./events\"\nexport {\n flushReleaseQueue,\n intern,\n rewrapResult,\n unwrapArgs,\n __setReleaseTransport,\n} from \"./handle-runtime\"\n\n// Error surface — plugins branch on `PluginError.is(e)` + `e.code`.\nexport {\n PluginError,\n PluginValidationError,\n PluginNotFoundError,\n PluginPermissionError,\n PluginHandleError,\n PluginQuotaError,\n PluginTimeoutError,\n PluginTransportError,\n PluginLifecycleError,\n PluginExecutionError,\n PluginInternalError,\n fromEnvelope,\n isErrorEnvelope,\n isPluginErrorCode,\n PLUGIN_ERROR_CODES,\n CODE_META,\n} from \"@snaptrude/plugin-core\"\nexport type {\n ErrorEnvelope,\n PluginErrorCode,\n WirePluginErrorCode,\n PluginErrorCategory,\n} from \"@snaptrude/plugin-core\"\n\n/**\n * The Snaptrude plugin client API.\n *\n * The main entry point for plugins to interact with the Snaptrude platform.\n */\nexport const snaptrude = ClientPluginApi.getInstance()\n"],"mappings":"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","names":["out","Comlink","PluginError","fromEnvelope"]}
|
|
1
|
+
{"version":3,"sources":["../src/api/index.ts","../src/host-api.ts","../src/handle-runtime.ts","../src/rpc-proxy.ts","../src/plugin-worker.ts","../src/events.ts","../src/index.ts"],"sourcesContent":["import {\n PluginApi,\n PluginCoreApi,\n PluginDesignApi,\n PluginEntityApi,\n PluginProgramApi,\n PluginPresentationApi,\n PluginAnalysisApi,\n PluginWorkspaceApi,\n} from \"@snaptrude/plugin-core\"\nimport { createRpcNamespace } from \"../rpc-proxy\"\n\nexport class ClientPluginApi extends PluginApi {\n private static instance: ClientPluginApi\n\n /**\n * Every namespace is fully remote under the all-handle model: math/geom now\n * cross to the host (values are opaque handles), so there is no in-worker\n * compute left. All dispatch through a single generic RPC Proxy. Units live\n * under `core.units`, so they ride the `core` proxy.\n */\n public core: PluginCoreApi\n public design: PluginDesignApi\n public entity: PluginEntityApi\n public program: PluginProgramApi\n public presentation: PluginPresentationApi\n public analysis: PluginAnalysisApi\n public workspace: PluginWorkspaceApi\n\n private constructor() {\n super()\n this.core = createRpcNamespace<PluginCoreApi>(\"core\")\n this.design = createRpcNamespace<PluginDesignApi>(\"design\")\n this.entity = createRpcNamespace<PluginEntityApi>(\"entity\")\n this.program = createRpcNamespace<PluginProgramApi>(\"program\")\n this.presentation =\n createRpcNamespace<PluginPresentationApi>(\"presentation\")\n this.analysis = createRpcNamespace<PluginAnalysisApi>(\"analysis\")\n this.workspace = createRpcNamespace<PluginWorkspaceApi>(\"workspace\")\n }\n\n static getInstance(): ClientPluginApi {\n if (!ClientPluginApi.instance) {\n ClientPluginApi.instance = new ClientPluginApi()\n }\n return ClientPluginApi.instance\n }\n}\n","import * as Comlink from \"comlink\"\nimport type {\n PluginApiMethod,\n PluginApiCallPayload,\n PluginApiCallWrappedResult,\n PluginApiCallResult,\n} from \"@snaptrude/plugin-core\"\nimport { PluginError, fromEnvelope, makeClientEnvelope } from \"@snaptrude/plugin-core\"\nimport { rewrapResult } from \"./handle-runtime\"\n\nexport interface HostApi {\n call<M extends PluginApiMethod>(\n payload: PluginApiCallPayload<M>\n ): Promise<PluginApiCallWrappedResult<M>>\n}\n\nexport interface HostApiWrapped {\n call<M extends PluginApiMethod>(\n payload: PluginApiCallPayload<M>\n ): Promise<PluginApiCallResult<M>>\n}\n\nexport function createHostApi(endpoint?: Comlink.Endpoint): HostApi {\n return Comlink.wrap<HostApi>(\n endpoint ?? (globalThis as unknown as Comlink.Endpoint)\n ) as unknown as HostApi\n}\n\nlet _instance: HostApi | null = null\n\n/** TEST SEAM ONLY: replace the Comlink host instance (pass `undefined` to reset). */\nexport function __setHostApiInstance(instance?: HostApi): void {\n _instance = instance ?? null\n}\n\nexport function getHostApi(): HostApiWrapped {\n if (!_instance) {\n _instance = createHostApi()\n }\n return {\n call: async <M extends PluginApiMethod>(payload: PluginApiCallPayload<M>): Promise<PluginApiCallResult<M>> => {\n if (!_instance) {\n throw new Error(\"Host API not initialized\")\n }\n\n let result: PluginApiCallWrappedResult<M>\n try {\n result = await _instance.call(payload)\n } catch (transportErr) {\n // Comlink-level rejection: port closed, worker terminated, clone\n // failure. Never a routed failure — the router always RETURNS its\n // envelope — so normalize to a typed transport error.\n if (PluginError.is(transportErr)) throw transportErr\n throw rehydrate(\n makeClientEnvelope(\n \"TRANSPORT_LOST\",\n transportErr instanceof Error ? transportErr.message : String(transportErr),\n { methodPath: payload.method }\n )\n )\n }\n\n if (result.success) {\n // Re-wrap tagged handles ({__h: id} → interned Handle instances).\n return rewrapResult(result.data) as PluginApiCallResult<M>\n }\n\n // Structured envelope when the host provides one; legacy hosts (string\n // `error` only) degrade to UNKNOWN with the message preserved.\n throw rehydrate(\n result.errorInfo ??\n makeClientEnvelope(\"UNKNOWN\", result.error ?? \"Unknown host error\", {\n methodPath: payload.method,\n })\n )\n }\n }\n}\n\n/**\n * Envelope → typed `PluginError`, with the stack trimmed to the plugin's call\n * site (V8 only; harmless no-op elsewhere) instead of transport internals.\n */\nfunction rehydrate(envelope: Parameters<typeof fromEnvelope>[0]): PluginError {\n const error = fromEnvelope(envelope)\n ;(Error as { captureStackTrace?: (target: object, ctor: Function) => void })\n .captureStackTrace?.(error, rehydrate)\n return error\n}\n","import { Handle } from \"@snaptrude/plugin-core\"\nimport { getHostApi } from \"./host-api\"\n\n/**\n * Worker-side handle lifecycle runtime (P1):\n *\n * - **Interning** — one live `Handle` instance per id, so `===`, `Set`, and\n * `Map` keys keep working exactly as they did when handles were raw strings\n * (the host identity-dedups resource/topology handles, so the same id\n * arrives repeatedly).\n * - **FinalizationRegistry backstop** — when the plugin drops every reference\n * to a handle, its host registry entry is eventually released without any\n * author action. Eventual, not timely: deterministic release\n * (`core.handles.release` / scopes / `await using`) remains the primary tool.\n * - **Batched release queue** — finalizer hits and `Symbol.asyncDispose` calls\n * collapse into one `core.handles.release([...])` RPC per flush.\n */\n\nconst interned = new Map<string, WeakRef<Handle<string>>>()\n\nconst RELEASE_FLUSH_SIZE = 64\nconst RELEASE_FLUSH_MS = 250\nconst MAX_FLUSH_FAILURES = 5\n\nconst releaseQueue = new Set<string>()\nlet flushTimer: ReturnType<typeof setTimeout> | null = null\nlet consecutiveFlushFailures = 0\n\nconst finalizer = new FinalizationRegistry<string>((id) => {\n // A NEW wrapper for the same id may have been interned after the collected\n // one died (deduped topology re-enumeration) — releasing then would free a\n // handle the plugin still holds. Only release when no live wrapper remains.\n if (interned.get(id)?.deref()) return\n interned.delete(id)\n enqueueRelease(id)\n})\n\n/**\n * Return THE `Handle` instance for this id — the existing live wrapper when\n * present, else a fresh one wired for auto-release.\n */\nexport function intern(id: string): Handle<string> {\n // The host just (re-)vended this id, so it is live host-side. Cancel any\n // PENDING auto-release for it — covers the finalizer-already-ran window\n // (old wrapper collected, id queued, same id re-vended before the flush;\n // without this the flush would release a handle the plugin holds live).\n releaseQueue.delete(id)\n\n const existing = interned.get(id)?.deref()\n if (existing) return existing\n\n const handle = new Handle<string>(id)\n // Instance-level override shadows the class's no-op placeholder. Explicit\n // dispose also UN-interns the wrapper: (a) the flush-failure retry filter\n // skips ids with a live interned wrapper, so a still-interned explicit\n // dispose would never be retried; (b) if the host re-vends the id later,\n // a fresh wrapper is minted instead of resurrecting the disposed one.\n ;(handle as { [Symbol.asyncDispose]?: () => Promise<void> })[Symbol.asyncDispose] =\n async () => {\n if (interned.get(id)?.deref() === handle) interned.delete(id)\n enqueueRelease(id)\n }\n interned.set(id, new WeakRef(handle))\n finalizer.register(handle, id)\n return handle\n}\n\nfunction enqueueRelease(id: string): void {\n releaseQueue.add(id)\n if (releaseQueue.size >= RELEASE_FLUSH_SIZE) {\n void flushReleaseQueue()\n return\n }\n flushTimer ??= setTimeout(() => {\n void flushReleaseQueue()\n }, RELEASE_FLUSH_MS)\n}\n\n/** How a release batch reaches the host. Swappable for tests (`__setReleaseTransport`). */\nlet releaseTransport = async (batch: string[]): Promise<void> => {\n await getHostApi().call({\n method: \"core.handles.release\",\n args: [batch],\n } as never)\n}\n\n/** TEST SEAM ONLY: replace the release transport (pass `undefined` to restore). */\nexport function __setReleaseTransport(\n transport?: (batch: string[]) => Promise<void>\n): void {\n releaseTransport =\n transport ??\n (async (batch: string[]) => {\n await getHostApi().call({\n method: \"core.handles.release\",\n args: [batch],\n } as never)\n })\n}\n\n/** Exported for tests and for an eager flush before a plugin self-completes. */\nexport async function flushReleaseQueue(): Promise<void> {\n if (flushTimer) {\n clearTimeout(flushTimer)\n flushTimer = null\n }\n if (releaseQueue.size === 0) return\n const batch = [...releaseQueue]\n releaseQueue.clear()\n try {\n await releaseTransport(batch)\n consecutiveFlushFailures = 0\n } catch (err) {\n // Transient failure (rate limit, timeout): re-queue so the entries are not\n // permanently leaked host-side; the next enqueue/flush retries. Ids the\n // plugin re-acquired in the meantime were already purged by intern() and\n // must not be re-added. If the host is simply gone, the plugin is stopping\n // and registry teardown reclaims everything anyway.\n consecutiveFlushFailures += 1\n if (consecutiveFlushFailures >= MAX_FLUSH_FAILURES) {\n // Persistent failure: retrying forever would just spin. Drop the batch\n // loudly — host teardown reclaims the entries when the plugin stops.\n console.warn(\n `[snaptrude] dropping ${batch.length} handle release(s) after ${consecutiveFlushFailures} failed flushes`,\n err\n )\n consecutiveFlushFailures = 0\n return\n }\n for (const id of batch) {\n if (!interned.get(id)?.deref()) releaseQueue.add(id)\n }\n }\n}\n\n/**\n * Recursively re-wrap a host result: `{ __h: \"<id>\" }` tags (produced by\n * `Handle.toJSON` through the host's JSON-round-trip serialization) become\n * interned `Handle` instances. Arrays and plain records are walked; all other\n * values pass through untouched.\n */\nexport function rewrapResult(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(rewrapResult)\n if (value && typeof value === \"object\") {\n const record = value as Record<string, unknown>\n const keys = Object.keys(record)\n if (keys.length === 1 && keys[0] === \"__h\" && typeof record.__h === \"string\") {\n return intern(record.__h)\n }\n for (const key of keys) record[key] = rewrapResult(record[key])\n return record\n }\n return value\n}\n\n/**\n * Recursively unwrap outgoing args: `Handle` instances become their wire id\n * strings. Uses a memo Map (not a bail-out set) so shared/diamond references\n * get the SAME converted subtree — a bail-out would leak un-unwrapped Handle\n * instances through the second reference. Cycles are handled by memoizing the\n * output object before recursing. Non-plain objects pass through untouched\n * (structured clone imposes plain-data args anyway).\n */\nexport function unwrapArgs(value: unknown, memo = new Map<object, unknown>()): unknown {\n if (value instanceof Handle) return value.id\n if (value === null || typeof value !== \"object\") return value\n\n const hit = memo.get(value)\n if (hit !== undefined) return hit\n\n if (Array.isArray(value)) {\n const out: unknown[] = []\n memo.set(value, out)\n for (const item of value) out.push(unwrapArgs(item, memo))\n return out\n }\n\n const proto = Object.getPrototypeOf(value)\n if (proto !== Object.prototype && proto !== null) return value\n\n const out: Record<string, unknown> = {}\n memo.set(value, out)\n for (const [key, item] of Object.entries(value)) out[key] = unwrapArgs(item, memo)\n return out\n}\n","import type {\n PluginApiCallPayload,\n PluginApiMethod,\n} from \"@snaptrude/plugin-core\"\nimport { getHostApi } from \"./host-api\"\nimport { unwrapArgs } from \"./handle-runtime\"\n\n/**\n * Build a namespace object whose nested property access maps to a\n * dot-separated host RPC method path, and whose every call dispatches that\n * path through the host bridge.\n *\n * The host exposes the entire plugin API behind a single generic `call()`\n * (see the host `bridge.ts`), and the method string is exactly the property\n * path — so one Proxy replaces every hand-written per-method RPC wrapper for\n * every namespace (`core.*`, `design.*`, `entity.*`):\n *\n * The POSITIONAL transport forwards the whole argument tuple; the host router\n * spreads it back into the resolved method (`fn(...args)`):\n *\n * ```ts\n * snaptrude.core.math.vec3.new(1, 2, 3)\n * // → getHostApi().call({ method: \"core.math.vec3.new\", args: [1, 2, 3] })\n * ```\n *\n * Typed at the call site, e.g. `createRpcNamespace<PluginEntityApi>(\"entity\")`.\n * The Proxy is structurally cast to the abstract API type — argument and\n * return types are enforced by that type, while dispatch is dynamic.\n *\n * Every namespace uses this — including `core.math.*` and `core.geom.*`: under\n * the all-handle model there is no in-worker compute; math and geometry are\n * host calls like everything else.\n */\nexport function createRpcNamespace<T extends object>(basePath: string): T {\n const build = (path: string): unknown =>\n new Proxy(NOOP, {\n get(_target, prop) {\n // Symbols and `then` must not resolve to a callable proxy, otherwise\n // the namespace would look thenable and break Promise resolution if it\n // ever reached an `await`.\n if (typeof prop !== \"string\" || prop === \"then\") return undefined\n return build(`${path}.${prop}`)\n },\n apply(_target, _thisArg, argArray: unknown[]) {\n const payload = {\n method: path,\n // Handle instances anywhere in the tuple become their wire id strings.\n args: argArray.map((arg) => unwrapArgs(arg)),\n } as unknown as PluginApiCallPayload<PluginApiMethod>\n return getHostApi().call(payload)\n },\n })\n\n return build(basePath) as T\n}\n\n/** Proxy target must be callable for the `apply` trap; identity is irrelevant. */\nconst NOOP = (): void => {}\n","import * as Comlink from \"comlink\"\nimport type { PluginEventName } from \"./events\"\n\nexport interface UIMessage {\n action: string\n payload: unknown\n}\n\ninterface PluginConfig {\n pluginId: string\n}\n\n/**\n * Base class for Snaptrude plugin workers.\n *\n * Handles Comlink wiring, host communication, and the standard lifecycle\n * methods (`init`, `destroy`, `ping`, `onUIMessage`). Subclass this and\n * override only the methods you need — then call `start()` to expose the\n * worker API.\n *\n * The plugin ID is received automatically from the host during\n * initialization — no need to pass it manually.\n *\n * @example\n * ```ts\n * import { PluginWorker } from \"@snaptrude/plugin-client\";\n *\n * class MyPlugin extends PluginWorker {\n * async onUIMessage(message: UIMessage) {\n * // handle messages from the UI panel\n * }\n * }\n *\n * new MyPlugin().start();\n * ```\n */\nexport abstract class PluginWorker {\n protected pluginId!: string\n private hostAPI: Comlink.Remote<Record<string, unknown>>\n\n constructor() {\n this.hostAPI = Comlink.wrap<Record<string, unknown>>(\n self as unknown as Comlink.Endpoint,\n )\n }\n\n protected sendToUI(action: string, payload: unknown): void {\n ;(this.hostAPI as Record<string, any>).ui.sendToUI({ action, payload })\n }\n\n /**\n * Show a transient notification toast in the Snaptrude editor.\n *\n * @param message - Text to display.\n * @param type - Severity; `\"error\"`/`\"warning\"` linger a little longer than\n * `\"info\"`. Defaults to `\"info\"`.\n */\n protected notify(\n message: string,\n type: \"info\" | \"warning\" | \"error\" = \"info\",\n ): void {\n ;(this.hostAPI as Record<string, any>).ui.showNotification(message, type)\n }\n\n /**\n * Signal the host that this plugin has finished its work and should be\n * stopped. Use this in headless (UI-less) plugins that run a task and\n * self-terminate.\n */\n protected complete(): void {\n ;(this.hostAPI as Record<string, any>).lifecycle.complete()\n }\n\n /**\n * Subscribe to a host event (e.g. `\"model:changed\"`, fired debounced on any\n * user- or plugin-initiated model edit). The callback runs in this worker\n * each time the event fires; its argument is a small structured-clone-safe\n * payload (see the event's payload type, e.g. `ModelChangedEvent`).\n *\n * Fire-and-forget: subscribe once (typically in `init()`). Subscriptions live\n * for the plugin's lifetime and are cleared automatically when the plugin is\n * stopped — there is no `unsubscribe` yet.\n *\n * @example\n * ```ts\n * import type { ModelChangedEvent } from \"@snaptrude/plugin-client\";\n *\n * async init() {\n * this.subscribe(\"model:changed\", (e) => {\n * const { source } = e as ModelChangedEvent;\n * console.log(\"model changed via\", source);\n * });\n * }\n * ```\n */\n protected subscribe(\n event: PluginEventName,\n callback: (payload?: unknown) => void,\n ): void {\n ;(this.hostAPI as Record<string, any>).events.on(\n event,\n Comlink.proxy(callback),\n )\n }\n\n async init(): Promise<void> {\n console.log(this.pluginId, \"init() called\")\n console.log(this.pluginId, \"Initialization complete\")\n }\n\n async destroy(): Promise<void> {\n console.log(this.pluginId, \"destroy() called — cleaning up\")\n }\n\n async ping(): Promise<string> {\n return \"pong\"\n }\n\n async onUIMessage(_message: UIMessage): Promise<void> {\n // Override in subclass to handle UI messages\n }\n\n /**\n * Expose the worker API via Comlink and start listening.\n * Call this once after constructing the plugin instance.\n *\n * The host calls `init(config)` with `{ pluginId }`,\n * which is captured here to set `this.pluginId` before the\n * subclass's `init()` runs.\n */\n start(): void {\n Comlink.expose(\n {\n init: (config: PluginConfig) => {\n this.pluginId = config.pluginId\n return this.init()\n },\n destroy: () => this.destroy(),\n ping: () => this.ping(),\n onUIMessage: (message: UIMessage) => this.onUIMessage(message),\n },\n self as unknown as Comlink.Endpoint,\n )\n console.log(\"Worker loaded, API exposed via Comlink\")\n }\n}\n","/**\n * Snaptrude plugin events.\n *\n * Plugins can react to changes in the host app. Events are delivered through\n * the host event bus — a separate channel from the request/response\n * `snaptrude.*` API — and carry a small structured-clone-safe payload, never\n * live model data.\n *\n * Subscribe from a {@link PluginWorker} subclass with `this.subscribe(...)`.\n *\n * NOTE: events are a subscription surface, not `call()` RPC methods, so they do\n * NOT appear in the discovery manifest (which enumerates callable methods\n * only). This module is the typed, discoverable declaration of the surface.\n */\n\n/** Every event a plugin can subscribe to. */\nexport const PLUGIN_EVENTS = [\n \"selection:changed\",\n \"tool:activated\",\n \"project:saved\",\n \"view:changed\",\n \"model:changed\",\n] as const\n\n/** Union of subscribable event names. */\nexport type PluginEventName = (typeof PLUGIN_EVENTS)[number]\n\n/**\n * Payload for `model:changed` — fired (debounced) whenever the project model is\n * mutated, by a user edit OR a plugin edit. Deliberately minimal: it signals\n * *that* the model changed, never *what* changed. Re-query the API for details.\n */\nexport interface ModelChangedEvent {\n /** Origin of the change. Currently always `\"command\"` (the edit chokepoint). */\n source: string\n}\n","import { ClientPluginApi } from \"./api\"\n\nexport * from \"./api\"\nexport * from \"./host-api\"\nexport * from \"./plugin-worker\"\nexport * from \"./events\"\nexport {\n flushReleaseQueue,\n intern,\n rewrapResult,\n unwrapArgs,\n __setReleaseTransport,\n} from \"./handle-runtime\"\n\n// Error surface — plugins branch on `PluginError.is(e)` + `e.code`.\nexport {\n PluginError,\n PluginValidationError,\n PluginNotFoundError,\n PluginPermissionError,\n PluginHandleError,\n PluginQuotaError,\n PluginTimeoutError,\n PluginTransportError,\n PluginLifecycleError,\n PluginExecutionError,\n PluginInternalError,\n fromEnvelope,\n isErrorEnvelope,\n isPluginErrorCode,\n PLUGIN_ERROR_CODES,\n CODE_META,\n} from \"@snaptrude/plugin-core\"\nexport type {\n ErrorEnvelope,\n PluginErrorCode,\n WirePluginErrorCode,\n PluginErrorCategory,\n} from \"@snaptrude/plugin-core\"\n\n/**\n * The Snaptrude plugin client API.\n *\n * The main entry point for plugins to interact with the Snaptrude platform.\n */\nexport const snaptrude = ClientPluginApi.getInstance()\n"],"mappings":"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","names":["out","Comlink","PluginError","fromEnvelope"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@snaptrude/plugin-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"comlink": "^4.4.2",
|
|
26
|
-
"@snaptrude/plugin-core": "0.
|
|
26
|
+
"@snaptrude/plugin-core": "0.9.1"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"tsup": "^8.5.1",
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
from: 0.8.0
|
|
3
|
+
to: 0.9.0
|
|
4
|
+
breaking: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Summary
|
|
8
|
+
|
|
9
|
+
0.9.0 reorganizes the namespace map: the `entity` top-level is retired (storeys move under `core`, reference lines and buildable envelopes join the `design` family), two straggler renames land (`core.zoom` → `core.camera`, `getTagsForComponent` → `listForComponent`), and a new `workspace` top-level plus `core.mode` and a façade heatmap API are introduced. Nothing breaks: every deprecated path keeps working — each delegates to the same implementation as its canonical replacement — so existing plugins compile and run unchanged. Migrate names at your own pace; new code should use the canonical paths.
|
|
10
|
+
|
|
11
|
+
## Breaking changes
|
|
12
|
+
|
|
13
|
+
None. All 0.9.0 moves are deprecate-in-place aliases; no signatures or result shapes changed on existing paths.
|
|
14
|
+
|
|
15
|
+
## Moved paths (deprecated → canonical)
|
|
16
|
+
|
|
17
|
+
- `entity.story.get` → `core.storeys.get`
|
|
18
|
+
- `entity.story.getAll` → `core.storeys.list` — the canonical result names the field `storeys` where `getAll` named it `stories`; the records are identical
|
|
19
|
+
- `entity.story.create` → `core.storeys.create`
|
|
20
|
+
- `entity.story.update` → `core.storeys.update`
|
|
21
|
+
- `entity.story.delete` → `core.storeys.delete`
|
|
22
|
+
- `entity.story.duplicate` → `core.storeys.copy`
|
|
23
|
+
- `entity.story.setActive` → `core.storeys.setActive`
|
|
24
|
+
- `entity.referenceLine.createMulti` → `design.create.referenceLines` — adds optional styling args and returns a `ComponentHandle[]` where `createMulti` returned `{ referenceLineIds }`
|
|
25
|
+
- `entity.referenceLine.get` → `design.query.referenceLines.get`
|
|
26
|
+
- `entity.referenceLine.getAll` → `design.query.listReferenceLines` — returns a filterable `ComponentHandle[]` where `getAll` returned `{ referenceLineIds }`
|
|
27
|
+
- `entity.referenceLine.delete` → `design.delete.entities`
|
|
28
|
+
- `entity.buildableEnvelope.create` → `design.create.buildableEnvelope`
|
|
29
|
+
- `entity.buildableEnvelope.update` → `design.update.buildableEnvelope`
|
|
30
|
+
- `core.zoom.extents` → `core.camera.zoomExtents`
|
|
31
|
+
- `core.zoom.selection` → `core.camera.zoomSelection`
|
|
32
|
+
- `core.tags.getTagsForComponent` → `core.tags.listForComponent`
|
|
33
|
+
|
|
34
|
+
## New APIs
|
|
35
|
+
|
|
36
|
+
- **`workspace.*` — dashboard-level operations.** `workspace.projects.{list, get, create, copy, rename}` (`copy` is the dashboard's Save As) and `workspace.teams.{list, get, listMembers}` — project and team operations that previously had no plugin surface. Mutators are write-gated like every other mutating API.
|
|
37
|
+
- **`core.mode.{list, get, set}` — application mode.** Read and switch between `design`, `bim`, `present`, and `program`. `set("program")` throws `PRECONDITION_FAILED` (Program opens in a paired browser tab the plugin worker cannot drive).
|
|
38
|
+
- **`analysis.heatmaps.renderSurfaceGrid`** — oriented-plane (façade) heatmap grids: `renderGrid` plus a plane-normal argument, for vertical and tilted surfaces.
|
|
39
|
+
- **`analysis.heatmaps` named overlays** — render calls accept `options.name` to register up to 16 concurrent overlays (unnamed renders keep the old replace-in-place behaviour via a shared default), and the new `analysis.heatmaps.overlays` sub-API manages them: `list()`, `show(name)` / `hide(name)` / `remove(name)`, `removeAll()`; exactly one overlay is visible at a time (legend follows it), and `reset()` clears them all. Overlays are in-session only and every overlay clears on any scene-mutating edit.
|
|
40
|
+
- **`analysis.heatmaps.renderField(cells, options?)`** — arbitrary cell geometry: planar polygon rings (host-triangulated) or pre-tessellated `vertices`/`indices` meshes, one value each. Every heatmap render call now also accepts `options.scale`: discrete `bands` (2–64, default 11 — unchanged default), binary `threshold` (pass/fail with a labelled two-swatch legend), or exact-match `categorical` classes.
|
|
41
|
+
- **Heatmap hover tooltips** — pass `hover: true` in any `analysis.heatmaps` render call to get a per-cell tooltip while that overlay is visible, showing the cell's `value` (+ unit) and optional `meta` payload; grid/surface-grid cells now accept `meta` like field cells.
|
|
42
|
+
- **`design.query.geometry.getTriangulatedMeshes(components, options?)`** — triangulated render meshes as plain world-space arrays (`{ meshes: [{ id, positions, indices, materialIds? }] }`, no kernel handles), in plan units; optional per-triangle material names via `includeMaterialIds: true` (`null` = unpainted/default); capped at 500 000 triangles per call (over-cap throws `VALIDATION` with the actual total — split the component list and batch).
|
|
43
|
+
- **`presentation.shapes` — plugin-owned keyed shapes on Present sheets.** `upsert(key, shape, { sheetId? })` creates-or-updates one stable shape per key (deterministic id, returns `{ shapeId, created }`), so rerunning an analysis updates the sheet instead of duplicating; plus `remove(key)`, `removeAll()`, and `list()`. The `shape` spec is a `type: "text" | "note" | "arrow" | "geo"` union taking the same options as the matching `presentation.annotate` call.
|
|
44
|
+
- **`program.site.getTimezone()`** — the IANA timezone id (e.g. `"Asia/Kolkata"`) of the project's geo-location, or `null` when the project is not geo-located on terrain.
|
|
45
|
+
- **`analysis.weather.getSeries(args)`** — hourly weather rows (dry-bulb, humidity, wind, radiation, sky cover) for a date range from the project's resolved EPW station, with full source metadata (station, WMO, distance, checksum) and per-field quality flags. Throws typed `PRECONDITION_FAILED` / `NOT_GEOLOCATED` on non-geo-located projects.
|
|
46
|
+
- **`analysis.solar.sampleGrid(args)`** — per-point solar sampling at arbitrary world positions: direct-sun visibility, shade fraction, sky-view factor, and direct/diffuse/total irradiance (W/m² instant or kWh/m² over a range), GPU-raytraced against the full model + terrain context. Pro-gated and write-gated like `analysis.illuminance`.
|
|
47
|
+
- **`analysis.daylight.compute(args)` + `poll({ runId, cursor? })`** — IES LM-83 annual daylight metrics as numbers (no textures): per-sensor records and per-space/storey/project aggregates with a full provenance echo (weather file, thresholds, grid, resolved optics, blind operation). `metrics: ["ASE"]` runs synchronously as before; runs including `"sDA"` now compute real sDA300/50% (Radiance) with LM-83 dynamic blind operation (2% rule, 5% diffuse closed-blind default — the API operates the blinds; sDA without blinds is not LM-83-compliant) and are asynchronous: `compute` returns `{ status: "running", runId }` immediately, and the new `analysis.daylight.poll` fetches results until `status === "complete"`. Optional `optics` arg overrides surface optics per material (keyed by material name as shown in Snaptrude) or per category, over LM-83/convention defaults (wall 0.5 / ceiling 0.7 / floor 0.2 / furniture 0.5 / glazing Tvis 0.65 / context 0.2); glazing takes visible transmittance (Tvis), converted to Radiance transmissivity backend-side. Sensor records gain `da300Percent`/`sdaPass`, aggregates gain `sdaPercent`; ASE fields are present when ASE is requested.
|
|
48
|
+
- **`program.site.getWeather()`** — the project's resolved weather source (station metadata + any pinned override), without fetching rows.
|
|
49
|
+
- **`core.io.import` accepts `.epw` weather files** — upload into the project weather catalog (deduped by checksum) and optionally pin as the project's weather override; `analysis.weather`/`solar`/`daylight` then resolve to the pinned file.
|
|
50
|
+
- **`design.query.geometry.getTriangulatedMeshes` `includeFaceIndices`** — per-triangle B-rep face ids (`faceIds`, honest `null` when a component has no face mapping), enabling face-level post-processing on the triangulated output.
|
|
51
|
+
- **`design.query.spaces.getEnclosure(space)`** — geometric v1 enclosure read: the surfaces bounding a space and its adjacent spaces, derived from live geometry.
|
|
52
|
+
- **`program.spreadsheet.datasets.{set, list}` + plugin-sourced bindings** — store named plugin datasets (upsert; 10 000-row / 2 MB caps) and bind them to ranges with `bindings.create(name, { dataset: "plugin", name }, target)`; re-running `datasets.set` auto-refreshes every binding bound to that dataset. The takeoff source (`{ dataset: "takeoff" }`) is unchanged.
|
|
53
|
+
- **`program.spreadsheet.addImage(sheetName, image, options?)`** — floating images in workbook sheets from PNG/JPEG/SVG data URIs (2 MB cap): cell-anchored, optional explicit size (intrinsic by default), and a stable `name` replaces the picture on rerun. For images on Present-mode sheets use `presentation.shapes` instead.
|
|
54
|
+
- **`program.spreadsheet.addChart` contract enum** — the ratified `chartType` names (`"column"`, `"bar"`) now work on the wire; the legacy internal names (`"columnClustered"`, `"barClustered"`) remain accepted.
|
|
55
|
+
- **`core.geom.create.brepFrom*` — constructive solid modeling.** A full family of brep constructors that mint `BrepHandle`s at authored coordinates: `brepFromFaces(faces)` (plain `{x, y, z}` face loops — host validates welding, planarity, edge coherence and fixes global orientation), `brepFromExtrusion(contour, direction, amount)` (prisms along any direction, holes and arc profiles included), `brepFromLoft(bottomContour, topContour)` (tapered solids; lofts that would produce non-planar side faces are rejected), `brepFromMesh(positions, faces)` (indexed vertex + face-index input), and the booleans `brepFromUnion` / `brepFromSubtraction` (`a` minus `b`) / `brepFromIntersection` — the production OpenCascade lane; scene-derived `getBrep` handles are accepted as read-only boolean inputs, and empty or disjoint results throw typed `VALIDATION` errors. All minted breps are inspectable through `core.geom.query.brep.*` before committing.
|
|
56
|
+
- **`design.create.massFromBrep(brep, label?)`** — commit a mass from any `core.geom.create` brep constructor's handle (scene-derived `getBrep` breps stay read-only → `PRECONDITION_FAILED`): generic mass at the authored coordinates, single undo step, collab-synced. Together with the constructors this closes the loop — author faces / extrude / loft / boolean, then place the result in the scene.
|
|
57
|
+
- **`presentation.placedViews` — layout control over placed views.** Inspect and edit the LAYOUT of view shapes already placed on Present sheets (content refresh stays on `sheets.updatePlacedView`): `list(sheetId?)` / `get(shapeId)` return sheet, sheet-local position, size, architectural scale (`null` for 3D), rotation, crop and link state; `move(shapeId, position, { sheetId? })` repositions or reparents across sheets; `scale(shapeId, factor)` is a uniform corner-drag-equivalent resize; `setScale(shapeId, scale)` sets a standard architectural scale (2D views only); `setCrop(shapeId, crop | null)` crops with native tldraw fractions, keeping the visible region page-anchored — and the crop survives `updatePlacedView` refreshes. Identity is the shape id `sheets.place` returns.
|
|
58
|
+
|
|
59
|
+
## No action needed
|
|
60
|
+
|
|
61
|
+
All 0.9.0 changes are additive. Deprecated paths remain until at least the next major version, and removal is gated on call metrics showing zero live traffic plus a consumer sweep. To modernize a plugin: rename calls per the moved-paths list above (only `core.storeys.list` and the two reference-line list/create reads have different result shapes from their predecessors — everything else is a pure rename).
|
package/upgrade-notes/index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package": "@snaptrude/plugin-client",
|
|
3
|
-
"latest": "0.
|
|
3
|
+
"latest": "0.9.0",
|
|
4
4
|
"spans": [
|
|
5
5
|
{
|
|
6
6
|
"from": "0.4.0",
|
|
@@ -31,6 +31,12 @@
|
|
|
31
31
|
"to": "0.8.0",
|
|
32
32
|
"file": "0.7.1-to-0.8.0.md",
|
|
33
33
|
"breaking": true
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"from": "0.8.0",
|
|
37
|
+
"to": "0.9.0",
|
|
38
|
+
"file": "0.8.0-to-0.9.0.md",
|
|
39
|
+
"breaking": false
|
|
34
40
|
}
|
|
35
41
|
]
|
|
36
42
|
}
|