Skip to content

Handle Lifecycle

Free host-side handle registry entries your plugin no longer needs — individually (release), by kind (releaseAll), or as a mint-recording region (beginScope / endScope). Releasing is a handle-lifecycle operation only: it frees host memory and quota room and makes the handle unresolvable; it never deletes engine-side geometry or scene entities. Accessed via snaptrude.core.handles.

Long-running plugins should release deterministically: every value handle (vec3/quat math intermediates, curves) and every topology enumeration you keep costs host memory until your plugin stops. The SDK also auto-releases handles your code has completely dropped (a garbage-collection backstop), but that is eventual — loops and batch jobs should use scopes or explicit release.

Types

ArenaKind

The 12 releasable handle kinds: "vec3" | "quat" | "line" | "arc" | "circle" | "profile" | "contour" | "brep" | "face" | "edge" | "halfedge" | "vertex". Entity-style handles (components, materials, underlays, terrain, import jobs, comments) have no registry entry and no lifecycle — passing one to release is a silent no-op.

ScopeToken

Opaque token returned by beginScope and consumed by endScope. Not an arena handle: it pins no memory, and release([token]) ignores it.

PluginHandlesStatsResult

PropertyTypeDescription
resourcesnumberLive resource-arena entries (profile/contour)
valuesnumberLive value-arena entries (vec3/quat/line/arc/circle)
topologynumberLive topology-arena entries (brep/face/edge/halfedge/vertex)
scopesnumberCurrently open handle scopes
hostHeapUsedBytesnumber | nullHost page usedJSHeapSize (null where unsupported)
hostHeapTotalBytesnumber | nullHost page totalJSHeapSize (null where unsupported)

Functions

release(handles)

Free arena handles in bulk. Unknown, foreign, or already-released ids are silent no-ops — there is no per-id result and no count.

  • Parameters:
    • handles: AnyArenaHandle[] — handles to release (max 10 000 per call).
  • Returns: Nothing.
ts
const faces = await snaptrude.core.geom.query.brep.listFaces(brep);
// ... read what you need ...
await snaptrude.core.handles.release(faces);

releaseAll(kind?)

Free every live arena handle of one kind, or ALL of your plugin's arena handles when the kind is omitted. Useful at batch boundaries.

  • Parameters:
    • kind: ArenaKind (optional) — restrict the sweep to one kind.
  • Returns: Nothing.
ts
const footprint = await snaptrude.design.query.spaces.getFootprint(space);
// ... measure ...
await snaptrude.core.handles.releaseAll("contour");

beginScope()

Open a mint-recording scope: every arena handle minted after this call is freed by the matching endScope. Fresh mints only — re-enumerating existing topology (identity-deduped) inside a scope does not capture those handles. Scopes nest, up to 64 deep. Always pair beginScope/endScope in try/finally so a thrown body cannot leak the scope.

  • Returns: ScopeToken — pass it to endScope.
ts
for (const space of spaces) {
  const scope = await snaptrude.core.handles.beginScope();
  try {
    const footprint = await snaptrude.design.query.spaces.getFootprint(space);
    const area = await snaptrude.core.geom.query.contour.getArea(footprint);
  } finally {
    await snaptrude.core.handles.endScope(scope); // footprint + intermediates freed
  }
}

endScope(scope, retain?)

Close a scope by its token and free every handle it recorded, except those listed in retain. Retained handles are promoted to the enclosing scope, so nesting composes like block scopes. Closing is token-addressed: an out-of-order close (from interleaved async tasks) frees only that scope's mints and leaves other open scopes intact.

  • Parameters:
    • scope: ScopeToken — the token from the matching beginScope.
    • retain: AnyArenaHandle[] (optional) — scope-minted handles that must survive.
  • Returns: Nothing.
ts
const scope = await snaptrude.core.handles.beginScope();
const outline = await snaptrude.design.query.spaces.getFootprint(space);
const outer = await snaptrude.core.geom.query.contour.getOuterProfile(outline);
await snaptrude.core.handles.endScope(scope, [outer]); // outline freed, outer survives

Concurrency note: a scope captures all mints between begin and end temporally. If you fire RPCs with Promise.all while a scope is open, their mints land in that scope too. Sequential await flows (the normal pattern) are exact.


stats()

Live handle-registry statistics for your plugin plus the host page's JS heap usage — the diagnostic surface for verifying that releases actually shrink host memory. Arena counts are exact and drop immediately on release; heap bytes are GC-dependent (the browser collects when it wants), so heap deltas are best-effort.

  • Returns: PluginHandlesStatsResult.
ts
const before = await snaptrude.core.handles.stats();
await snaptrude.core.handles.releaseAll("vec3");
const after = await snaptrude.core.handles.stats();
console.log(`freed ${before.values - after.values} value entries`);