diff --git a/.scout/evaluations/data-query-set.toml b/.scout/evaluations/data-query-set.toml new file mode 100644 index 00000000..d9d76483 --- /dev/null +++ b/.scout/evaluations/data-query-set.toml @@ -0,0 +1,192 @@ +description = "adobe/data search quality evaluation" + +# ── Identifier queries ──────────────────────────────────────────────────────── + +[[queries]] +id = "ident-1" +query = "createPlugin" +class = "identifier" + +[judgments.ident-1] +"packages/data/src/ecs/database/create-plugin.ts" = 3 +"packages/data/src/ecs/database/create-plugin.type-test.ts" = 2 +"packages/data/src/ecs/database/imports-chain.type-test.ts" = 1 +"packages/data/src/ecs/database/combine-plugins.type-test.ts" = 1 + +[[queries]] +id = "ident-2" +query = "createStructBuffer" +class = "identifier" + +[judgments.ident-2] +"packages/data/src/typed-buffer/create-struct-buffer.ts" = 3 +"packages/data/src/typed-buffer/typed-buffer.ts" = 2 +"packages/data-gpu/src/graphics/scene/scene-uniforms/scene-uniforms-plugin.ts" = 1 + +[[queries]] +id = "ident-3" +query = "getStructLayout" +class = "identifier" + +[judgments.ident-3] +"packages/data/src/typed-buffer/structs/get-struct-layout.ts" = 3 +"packages/data/src/typed-buffer/create-struct-buffer.ts" = 2 +"packages/data/src/schema/to-vertex-buffer-layout.ts" = 1 + +[[queries]] +id = "ident-4" +query = "ColliderShape" +class = "identifier" + +[judgments.ident-4] +"packages/data-gpu/src/physics/body/collider-shape/collider-shape.ts" = 3 +"packages/data-gpu/src/physics/body/collider-shape/mass-properties.ts" = 2 +"packages/data-gpu/src/physics/solvers/rapier-solver-plugin.ts" = 2 +"packages/data-gpu/src/physics/body/collider-shape/list.ts" = 1 + +[[queries]] +id = "ident-5" +query = "AnimationTrack" +class = "identifier" + +[judgments.ident-5] +"packages/data-gpu/src/graphics/animation/animation-track/animation-track.ts" = 3 +"packages/data-gpu/src/graphics/animation/animation-data-plugin.ts" = 2 +"packages/data-gpu/src/graphics/scene/model/gltf/parse-animations.ts" = 2 +"packages/data-gpu/src/graphics/animation/animation-track/sample.ts" = 1 + +[[queries]] +id = "ident-6" +query = "BlobStore" +class = "identifier" + +[judgments.ident-6] +"packages/data/src/cache/blob-store.ts" = 3 +"packages/data/src/cache/data-cache.ts" = 2 + +[[queries]] +id = "ident-7" +query = "Database" +class = "identifier" + +[judgments.ident-7] +"packages/data/src/ecs/database/database.ts" = 3 +"packages/data/src/ecs/database/public/create-database.ts" = 2 +"packages/data/src/ecs/database/create-plugin.ts" = 2 + +# ── Natural language queries ────────────────────────────────────────────────── + +[[queries]] +id = "nl-1" +query = "ECS plugin system composition and database creation" +class = "natural_language" + +[judgments.nl-1] +"packages/data/src/ecs/database/create-plugin.ts" = 3 +"packages/data/src/ecs/database/database.ts" = 3 +"packages/data/src/ecs/database/public/create-database.ts" = 2 + +[[queries]] +id = "nl-2" +query = "observe reactive stream subscription and notification" +class = "natural_language" + +[judgments.nl-2] +"packages/data/src/observe/index.ts" = 3 +"packages/data/src/observe/create-event.ts" = 2 +"packages/data/src/ecs/database/public/observe-select-deep.ts" = 2 +"packages/data/src/observe/with-filter.ts" = 1 +"packages/data/src/ecs/database/observed/create-observed-database.ts" = 1 + +[[queries]] +id = "nl-3" +query = "archetype entity storage row migration dense packing" +class = "natural_language" + +[judgments.nl-3] +"packages/data/src/ecs/archetype/archetype.ts" = 3 +"packages/data/src/ecs/archetype/create-archetype.ts" = 3 +"packages/data/src/ecs/archetype/delete-row.ts" = 2 +"packages/data/src/ecs/entity-location-table/create-entity-location-table.ts" = 1 + +[[queries]] +id = "nl-4" +query = "physics rigid body collider simulation rapier" +class = "natural_language" + +[judgments.nl-4] +"packages/data-gpu/src/physics/body/rigid-body.ts" = 3 +"packages/data-gpu/src/physics/solvers/rapier-solver-plugin.ts" = 3 +"packages/data-gpu/src/physics/body/collider-shape/collider-shape.ts" = 2 +"packages/data-gpu/src/physics/body/collider-mesh.ts" = 2 +"packages/data-gpu/src/physics/physics-data-plugin.ts" = 1 + +[[queries]] +id = "nl-5" +query = "persistence incremental checkpoint journal write backend" +class = "natural_language" + +[judgments.nl-5] +"packages/data-persistence/src/backend/persistence-backend.ts" = 3 +"packages/data-persistence/src/journal/journal-codec.ts" = 3 +"packages/data-persistence/src/encoder/journal-snapshot-codec.ts" = 2 +"packages/data-persistence/src/backend/memory-backend.ts" = 2 +"packages/data-persistence/src/browser/opfs-backend.ts" = 1 + +[[queries]] +id = "nl-6" +query = "GLTF model loading parsing scene mesh" +class = "natural_language" + +[judgments.nl-6] +"packages/data-gpu/src/graphics/scene/model/gltf/gltf-schema.ts" = 3 +"packages/data-gpu/src/graphics/scene/model/gltf/load-gltf-model.ts" = 3 +"packages/data-gpu/src/graphics/scene/model/gltf/parse-animations.ts" = 2 +"packages/data-gpu/src/graphics/scene/model/gltf/compute-world-matrices.ts" = 2 +"packages/data-gpu/src/graphics/scene/model/gltf/decode-images.ts" = 1 +"packages/data-gpu/src/graphics/scene/model/gltf/build-material-bind-group.ts" = 1 + +# ── Filtered queries ────────────────────────────────────────────────────────── + +[[queries]] +id = "filt-1" +query = "createPlugin" +class = "filtered" +paths = ["packages/data/src/ecs"] + +[judgments.filt-1] +"packages/data/src/ecs/database/create-plugin.ts" = 3 +"packages/data/src/ecs/database/combine-plugins.ts" = 2 + +[[queries]] +id = "filt-2" +query = "BlobStore" +class = "filtered" +paths = ["packages/data/src/cache"] + +[judgments.filt-2] +"packages/data/src/cache/blob-store.ts" = 3 +"packages/data/src/cache/data-cache.ts" = 2 + +[[queries]] +id = "filt-3" +query = "AnimationTrack" +class = "filtered" +paths = ["packages/data-gpu/src/graphics/animation"] + +[judgments.filt-3] +"packages/data-gpu/src/graphics/animation/animation-track/animation-track.ts" = 3 +"packages/data-gpu/src/graphics/animation/animation-data-plugin.ts" = 2 +"packages/data-gpu/src/graphics/animation/animation-track/sample.ts" = 1 + +[[queries]] +id = "filt-4" +query = "PersistOp" +class = "filtered" +extensions = ["ts"] +paths = ["packages/data-persistence/src/transport"] + +[judgments.filt-4] +"packages/data-persistence/src/transport/transport.ts" = 3 +"packages/data-persistence/src/transport/router.ts" = 2 +"packages/data-persistence/src/transport/inprocess-transport.ts" = 1 diff --git a/.scout/scout.config.yaml b/.scout/scout.config.yaml new file mode 100644 index 00000000..e97b420f --- /dev/null +++ b/.scout/scout.config.yaml @@ -0,0 +1,84 @@ +ignore: +- '**/CHANGELOG.md' +- '**/CHANGELOG*' +- '**/CHANGES.md' +- '**/HISTORY.md' +- pnpm-lock.yaml +- '*.png' +- '*.jpg' +- aidd-custom/ +- c/ +dampen: +- factor: 0.3 + patterns: + - '**/package.json' +- factor: 0.5 + patterns: + - '**/*.test.ts' + - '**/*.spec.ts' +- factor: 0.5 + patterns: + - packages/data-lit-tictactoe/ + - packages/data-lit-todo/ + - packages/data-p2p-tictactoe/ + - packages/data-react-hello/ + - packages/data-react-pixie/ + - packages/data-solid-dashboard/ + - packages/data-gpu-samples/ +- factor: 0.5 + patterns: + - packages/data/src/old-ecs/ +- factor: 0.4 + patterns: + - packages/data/src/assembly-test/ + - packages/data/src/perftest/ +- factor: 0.6 + patterns: + - packages/data/references/ +boost: +- factor: 1.3 + patterns: + - packages/data/src/ecs/ +- factor: 1.2 + patterns: + - packages/data/src/observe/ + - packages/data/src/typed-buffer/ + - packages/data/src/schema/ + - packages/data/src/math/ + - packages/data-gpu/src/ + - packages/data-persistence/src/ +tune: + rerank_scale: 0.0 + rerank_weight_nl: 0.4 + rerank_weight_ident: 0.35 + rerank_weight_filtered: 0.4 + rerank_candidates_nl: 20 + rerank_candidates_ident: 10 + rerank_confidence_threshold: 0.6 + rerank_floor: 0.3 + rerank_evidence_budget_nl: 600 + rerank_evidence_budget_ident: 800 + rrf_semantic_weight_nl: 1.5 + rrf_bm25_weight_nl: 0.5 + rrf_semantic_weight_ident: 0.7 + rrf_bm25_weight_ident: 1.3 + prf_top_k: 10 + prf_confidence_threshold: 0.75 + bm25_legacy_mode: false + recency_weight_nl: 0.1 + recency_weight_ident: 0.05 + recency_midpoint_days: 30.0 + coderank_weight_nl: 0.2 + coderank_weight_ident: 0.1 + coderank_midpoint: 0.3 + symbol_popularity_weight_nl: 0.15 + symbol_popularity_weight_ident: 0.15 + symbol_popularity_midpoint: 0.3 + learned_ranker_model_path: bundled:v16 + learned_ranker_blend: 0.5 + learned_ranker_calibrated_blend: false + learned_ranker_entry_blend: 0.0 + capsule_lane_weight: 1.0 + capsule_identifier_dampen: 0.85 + search_plan_router: true + search_plan_router_mode: deterministic_prf_only diff --git a/README.md b/README.md index d8f7f38f..106ad83f 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,71 @@ Adobe Data Oriented Programming Library Until we reach 1.0.0, minor version changes may be API breaking. +### 0.9.70 + +**`TransactionResult.ephemeral` → `TransactionResult.persistent` (inverted)** + +The boolean is now `true` when at least one changed entity is persistent (id ≥ 0), rather than `true` when all changed entities are non-persistent. + +```ts +// before +if (!result.ephemeral) save(result); +// after +if (result.persistent) save(result); +``` + +**`TransactionResult.transient` → `TransactionResult.intermediate`** + +Renamed to remove the false antonym relationship with `persistent`. Both are orthogonal: `intermediate` = preview step (will be rolled back), `persistent` = touches saved entities. + +```ts +// before +if (!result.transient && !result.ephemeral) save(result); +// after +if (!result.intermediate && result.persistent) save(result); +``` + +**`TransactionOptions.transient` → `TransactionOptions.intermediate`** + +```ts +// before +db.execute(fn, { transient: true }); +// after +db.execute(fn, { intermediate: true }); +``` + +**`TransactionIntent "transient"` → `"intermediate"` (sync wire protocol)** + +Affects `observe.envelopes` intent values and `ClientMessage` kind strings in `@adobe/data-sync`. + +```ts +// before +db.observe.envelopes(({ intent }) => { + if (intent === "transient") { ... } +}); +transport.send({ kind: "transient", envelope }); +// after +db.observe.envelopes(({ intent }) => { + if (intent === "intermediate") { ... } +}); +transport.send({ kind: "intermediate", envelope }); +``` + +**`SyncServiceOptions.maxTransientsPerSecond` → `maxIntermediatesPerSecond`** + +**`Schema.ephemeral` deprecated → use `Schema.nonPersistent`** + +The `"ephemeral"` component string still works at runtime with a `console.warn`; update to `"nonPersistent"`. + +```ts +// before +{ ..., ephemeral: true } +ensureArchetype(["id", "cursor", "ephemeral"]) +// after +{ ..., nonPersistent: true } +ensureArchetype(["id", "cursor", "nonPersistent"]) +``` + ## Data Oriented Programming This library is built using a data oriented and functional programming paradigm. diff --git a/packages/data-gpu-samples/src/samples/boids/boids-service.ts b/packages/data-gpu-samples/src/samples/boids/boids-service.ts index f8c1b1fc..be652879 100644 --- a/packages/data-gpu-samples/src/samples/boids/boids-service.ts +++ b/packages/data-gpu-samples/src/samples/boids/boids-service.ts @@ -140,17 +140,17 @@ interface BoidGpu { export const boidsPlugin = Database.Plugin.create({ extends: Database.Plugin.combine(graphics, SceneUniforms.plugin, Orbit.plugin), resources: { - boidsCount: { default: DEFAULT_BOIDS as number, transient: true }, - boidsGpu: { default: null as BoidGpu | null, transient: true }, - boidsPingFrame: { default: 0 as number, transient: true }, - boidsSceneBG: { default: null as GPUBindGroup | null, transient: true }, - boidsSceneBuffer: { default: null as GPUBuffer | null, transient: true }, + boidsCount: { default: DEFAULT_BOIDS as number, nonPersistent: true }, + boidsGpu: { default: null as BoidGpu | null, nonPersistent: true }, + boidsPingFrame: { default: 0 as number, nonPersistent: true }, + boidsSceneBG: { default: null as GPUBindGroup | null, nonPersistent: true }, + boidsSceneBuffer: { default: null as GPUBuffer | null, nonPersistent: true }, // Cursor scare ray. Origin is the camera eye; dir is the unit vector // from the eye through the cursor toward the far plane. Boids near // this line are pushed perpendicular to it. - boidsScareOrigin: { default: [0, 0, 0] as Vec3, transient: true }, - boidsScareDir: { default: [0, 0, 1] as Vec3, transient: true }, - boidsScareActive: { default: 0 as F32, transient: true }, + boidsScareOrigin: { default: [0, 0, 0] as Vec3, nonPersistent: true }, + boidsScareDir: { default: [0, 0, 1] as Vec3, nonPersistent: true }, + boidsScareActive: { default: 0 as F32, nonPersistent: true }, }, transactions: { setBoidsCount(t, count: number) { diff --git a/packages/data-gpu-samples/src/samples/rigid-stack/rigid-stack-debug-render.ts b/packages/data-gpu-samples/src/samples/rigid-stack/rigid-stack-debug-render.ts index 5405e92f..c46730a3 100644 --- a/packages/data-gpu-samples/src/samples/rigid-stack/rigid-stack-debug-render.ts +++ b/packages/data-gpu-samples/src/samples/rigid-stack/rigid-stack-debug-render.ts @@ -78,7 +78,7 @@ const RENDER_COMPONENTS = ["position", "rotation", "halfExtents", "colliderShape export const rigidStackDebugRender = Database.Plugin.create({ extends: Database.Plugin.combine(graphics, physicsData, SceneUniforms.plugin), resources: { - rigidGpu: { default: null as RigidGpu | null, transient: true }, + rigidGpu: { default: null as RigidGpu | null, nonPersistent: true }, }, systems: { rigidRenderInit: { diff --git a/packages/data-gpu-samples/src/samples/rigid-stack/rigid-stack-service.ts b/packages/data-gpu-samples/src/samples/rigid-stack/rigid-stack-service.ts index 01fb2711..bad31994 100644 --- a/packages/data-gpu-samples/src/samples/rigid-stack/rigid-stack-service.ts +++ b/packages/data-gpu-samples/src/samples/rigid-stack/rigid-stack-service.ts @@ -88,10 +88,10 @@ const DROPS: Drop[] = (() => { const rigidStackScene = Database.Plugin.create({ extends: Database.Plugin.combine(pbrRender, shapeGeometry, physicsRenderBridge, modelCollider, jointData, Orbit.plugin), resources: { - _spawnAccum: { default: 0 as number, transient: true }, - _spawnElapsed: { default: 0 as number, transient: true }, - _spawnedDynamic: { default: 0 as number, transient: true }, - _sweeper: { default: 0 as Entity, transient: true }, // the kinematic bar + _spawnAccum: { default: 0 as number, nonPersistent: true }, + _spawnElapsed: { default: 0 as number, nonPersistent: true }, + _spawnedDynamic: { default: 0 as number, nonPersistent: true }, + _sweeper: { default: 0 as Entity, nonPersistent: true }, // the kinematic bar }, transactions: { initializeScene(t) { diff --git a/packages/data-gpu/src/core/core-plugin.ts b/packages/data-gpu/src/core/core-plugin.ts index a5644b8d..ba6737d1 100644 --- a/packages/data-gpu/src/core/core-plugin.ts +++ b/packages/data-gpu/src/core/core-plugin.ts @@ -33,8 +33,8 @@ async function getWebGPUDevice() { export const core = Database.Plugin.create({ extends: Database.Plugin.combine(scheduler, FrameTime.plugin), resources: { - device: { default: null as GPUDevice | null, transient: true }, - commandEncoder: { default: null as GPUCommandEncoder | null, transient: true }, + device: { default: null as GPUDevice | null, nonPersistent: true }, + commandEncoder: { default: null as GPUCommandEncoder | null, nonPersistent: true }, }, systems: { input: { diff --git a/packages/data-gpu/src/core/frame-time/frame-time-plugin.ts b/packages/data-gpu/src/core/frame-time/frame-time-plugin.ts index 78af176f..1528c8b9 100644 --- a/packages/data-gpu/src/core/frame-time/frame-time-plugin.ts +++ b/packages/data-gpu/src/core/frame-time/frame-time-plugin.ts @@ -22,7 +22,7 @@ export const plugin = Database.Plugin.create({ resources: { frameTime: { default: { now: 0, dt: 0, elapsed: 0 } satisfies FrameTime as FrameTime, - transient: true, + nonPersistent: true, }, }, systems: { diff --git a/packages/data-gpu/src/graphics/graphics-plugin.ts b/packages/data-gpu/src/graphics/graphics-plugin.ts index 17a62d3a..444ef236 100644 --- a/packages/data-gpu/src/graphics/graphics-plugin.ts +++ b/packages/data-gpu/src/graphics/graphics-plugin.ts @@ -20,11 +20,11 @@ import { core } from "../core/core-plugin.js"; export const graphics = Database.Plugin.create({ extends: core, resources: { - renderPassEncoder: { default: null as GPURenderPassEncoder | null, transient: true }, - depthTexture: { default: null as GPUTexture | null, transient: true }, - clearColor: { default: [0, 0, 0, 1] as Vec4, transient: true }, - canvas: { default: null as HTMLCanvasElement | null, transient: true }, - canvasContext: { default: null as GPUCanvasContext | null, transient: true }, + renderPassEncoder: { default: null as GPURenderPassEncoder | null, nonPersistent: true }, + depthTexture: { default: null as GPUTexture | null, nonPersistent: true }, + clearColor: { default: [0, 0, 0, 1] as Vec4, nonPersistent: true }, + canvas: { default: null as HTMLCanvasElement | null, nonPersistent: true }, + canvasContext: { default: null as GPUCanvasContext | null, nonPersistent: true }, // Guarded for headless hosts (Node): this default is evaluated at module // load, and `navigator` is browser-only. Rendering re-derives the real // preferred format when a canvas is configured; the placeholder is unused. @@ -32,9 +32,9 @@ export const graphics = Database.Plugin.create({ default: (typeof navigator !== "undefined" && navigator.gpu ? navigator.gpu.getPreferredCanvasFormat() : "bgra8unorm") as GPUTextureFormat, - transient: true, + nonPersistent: true, }, - depthFormat: { default: "depth24plus" as GPUTextureFormat, transient: true }, + depthFormat: { default: "depth24plus" as GPUTextureFormat, nonPersistent: true }, }, transactions: { setCanvas(t, canvas: HTMLCanvasElement | null) { diff --git a/packages/data-gpu/src/graphics/rendering/ibl-render/ibl-render-plugin.ts b/packages/data-gpu/src/graphics/rendering/ibl-render/ibl-render-plugin.ts index 6d0f2a1f..42c337a0 100644 --- a/packages/data-gpu/src/graphics/rendering/ibl-render/ibl-render-plugin.ts +++ b/packages/data-gpu/src/graphics/rendering/ibl-render/ibl-render-plugin.ts @@ -60,10 +60,10 @@ function createSkyboxBindGroupLayout(device: GPUDevice): GPUBindGroupLayout { export const pbrIblRender = Database.Plugin.create({ extends: Database.Plugin.combine(pbrCore, modelLoader, SceneUniforms.plugin, transform), resources: { - _iblEnvironment: { default: null as GPUTexture | null, transient: true }, - _iblIrradiance: { default: null as GPUTexture | null, transient: true }, - _iblPrefiltered: { default: null as GPUTexture | null, transient: true }, - _iblBrdfLut: { default: null as GPUTexture | null, transient: true }, + _iblEnvironment: { default: null as GPUTexture | null, nonPersistent: true }, + _iblIrradiance: { default: null as GPUTexture | null, nonPersistent: true }, + _iblPrefiltered: { default: null as GPUTexture | null, nonPersistent: true }, + _iblBrdfLut: { default: null as GPUTexture | null, nonPersistent: true }, }, systems: { iblInitSystem: { diff --git a/packages/data-gpu/src/graphics/rendering/material-gpu/material-gpu-plugin.ts b/packages/data-gpu/src/graphics/rendering/material-gpu/material-gpu-plugin.ts index 9ee1cfe2..aac364ee 100644 --- a/packages/data-gpu/src/graphics/rendering/material-gpu/material-gpu-plugin.ts +++ b/packages/data-gpu/src/graphics/rendering/material-gpu/material-gpu-plugin.ts @@ -34,9 +34,9 @@ export const materialGpu = Database.Plugin.create({ _layerIndex: U32.schema, }, resources: { - _materialArrays: { default: null as MaterialArrays | null, transient: true }, - _materialPalette: { default: null as GPUBuffer | null, transient: true }, - _materialBindGroup: { default: null as GPUBindGroup | null, transient: true }, + _materialArrays: { default: null as MaterialArrays | null, nonPersistent: true }, + _materialPalette: { default: null as GPUBuffer | null, nonPersistent: true }, + _materialBindGroup: { default: null as GPUBindGroup | null, nonPersistent: true }, }, systems: { materialGpuBuilder: { diff --git a/packages/data-gpu/src/graphics/rendering/ragdoll-trigger-plugin.ts b/packages/data-gpu/src/graphics/rendering/ragdoll-trigger-plugin.ts index 8564695d..f0e4a435 100644 --- a/packages/data-gpu/src/graphics/rendering/ragdoll-trigger-plugin.ts +++ b/packages/data-gpu/src/graphics/rendering/ragdoll-trigger-plugin.ts @@ -12,7 +12,7 @@ import { Database } from "@adobe/data/ecs"; */ export const ragdollTrigger = Database.Plugin.create({ resources: { - _ragdollTrigger: { default: false as boolean, transient: true }, + _ragdollTrigger: { default: false as boolean, nonPersistent: true }, }, transactions: { /** Go limp: the active ragdoll backend flips its bones to dynamic. */ diff --git a/packages/data-gpu/src/graphics/scene/model/shape/shape-geometry-plugin.ts b/packages/data-gpu/src/graphics/scene/model/shape/shape-geometry-plugin.ts index 48da30e8..bfa98f58 100644 --- a/packages/data-gpu/src/graphics/scene/model/shape/shape-geometry-plugin.ts +++ b/packages/data-gpu/src/graphics/scene/model/shape/shape-geometry-plugin.ts @@ -19,7 +19,7 @@ import { uploadShapeMesh } from "./upload-shape-mesh.js"; export const shapeGeometry = Database.Plugin.create({ extends: Database.Plugin.combine(pbrCore, model, core), resources: { - _shapeGeometry: { default: null as { sphere: Entity; cube: Entity } | null, transient: true }, + _shapeGeometry: { default: null as { sphere: Entity; cube: Entity } | null, nonPersistent: true }, }, transactions: { insertShapePrimitive(t, args: { vertexBuffer: GPUBuffer; indexBuffer: GPUBuffer; indexCount: number }): Entity { diff --git a/packages/data-gpu/src/graphics/scene/scene-uniforms/scene-uniforms-plugin.ts b/packages/data-gpu/src/graphics/scene/scene-uniforms/scene-uniforms-plugin.ts index 22bdca8b..7a270f14 100644 --- a/packages/data-gpu/src/graphics/scene/scene-uniforms/scene-uniforms-plugin.ts +++ b/packages/data-gpu/src/graphics/scene/scene-uniforms/scene-uniforms-plugin.ts @@ -25,7 +25,7 @@ const sceneUniformsStructLayout = getStructLayout(sceneUniformsSchema); export const plugin = Database.Plugin.create({ extends: Database.Plugin.combine(Camera.plugin, Light.plugin), resources: { - _sceneUniformsBuffer: { default: null as GPUBuffer | null, transient: true }, + _sceneUniformsBuffer: { default: null as GPUBuffer | null, nonPersistent: true }, }, systems: { sceneUniformsSystem: { diff --git a/packages/data-gpu/src/physics/physics-clock-plugin.ts b/packages/data-gpu/src/physics/physics-clock-plugin.ts index b605ff14..3635a95c 100644 --- a/packages/data-gpu/src/physics/physics-clock-plugin.ts +++ b/packages/data-gpu/src/physics/physics-clock-plugin.ts @@ -36,7 +36,7 @@ const DEFAULT: PhysicsClock = { fixedDt: 1 / 60, accumulator: 0, alpha: 0, steps export const physicsClock = Database.Plugin.create({ extends: core, resources: { - physicsClock: { default: DEFAULT as PhysicsClock, transient: true }, + physicsClock: { default: DEFAULT as PhysicsClock, nonPersistent: true }, }, transactions: { /** Set the simulation rate (Hz). Call once at init; default is 60. */ diff --git a/packages/data-gpu/src/physics/solvers/jolt-solver-plugin.ts b/packages/data-gpu/src/physics/solvers/jolt-solver-plugin.ts index c2c0b344..95f6186a 100644 --- a/packages/data-gpu/src/physics/solvers/jolt-solver-plugin.ts +++ b/packages/data-gpu/src/physics/solvers/jolt-solver-plugin.ts @@ -80,7 +80,7 @@ export const joltSolver = Database.Plugin.create({ _joltJoint: True.schema, // tag: this joint has been mirrored into the Jolt world }, resources: { - _joltContext: { default: null as JoltContext | null, transient: true }, + _joltContext: { default: null as JoltContext | null, nonPersistent: true }, }, systems: { joltStep: { diff --git a/packages/data-lit/src/hooks/use-pointer-observe.ts b/packages/data-lit/src/hooks/use-pointer-observe.ts index cc4834fa..4d5b5fb7 100644 --- a/packages/data-lit/src/hooks/use-pointer-observe.ts +++ b/packages/data-lit/src/hooks/use-pointer-observe.ts @@ -17,8 +17,8 @@ import { useMemo } from "./use-memo.js"; * ### Driving a never-ending presence transaction * * Pair with `Observe.toAsyncGenerator` to feed a never-ending async-generator - * transaction. Each `yield` becomes a transient envelope that the sync - * service forwards as `kind: "transient"`. The reconciler's `(userId, id)` + * transaction. Each `yield` becomes an intermediate envelope that the sync + * service forwards as `kind: "intermediate"`. The reconciler's `(userId, id)` * compound key replaces the previous sample, so each peer has at most one * outstanding cursor sample at any time. * diff --git a/packages/data-persistence/src/provider/persistence-provider.ts b/packages/data-persistence/src/provider/persistence-provider.ts index 560ee574..aaa2ad56 100644 --- a/packages/data-persistence/src/provider/persistence-provider.ts +++ b/packages/data-persistence/src/provider/persistence-provider.ts @@ -8,7 +8,7 @@ import type { IncrementalPersistenceService } from "../service/incremental-persi */ export interface ProviderMountOptions { /** - * Automatically persist every non-transient, non-ephemeral transaction. + * Automatically persist every non-transient, persistent transaction. * Defaults to `true`. */ readonly autoPersist?: boolean; diff --git a/packages/data-persistence/src/service/create-incremental-persistence-service.ts b/packages/data-persistence/src/service/create-incremental-persistence-service.ts index 8eb94a14..c6d1f3fd 100644 --- a/packages/data-persistence/src/service/create-incremental-persistence-service.ts +++ b/packages/data-persistence/src/service/create-incremental-persistence-service.ts @@ -497,7 +497,7 @@ export const createIncrementalPersistenceService = async ( let unsubscribe: (() => void) | null = null; if (autoPersist) { unsubscribe = database.observe.transactions((result) => { - if (result.transient || result.ephemeral) return; + if (result.intermediate || !result.persistent) return; // One txId per observer firing — every entity-level entry // we emit for this user transaction shares it, and the // trailing commit entry uses the same id. Replay groups by @@ -858,7 +858,7 @@ export const createIncrementalPersistenceService = async ( const applyJournalEntry = (manifest: Manifest, eltState: EltState, entry: JournalEntry): void => { if (entry.kind === "commit") return; // tx-end markers carry no state to apply - if (entry.entity < 0) return; // ephemeral / sentinel — should not appear + if (entry.entity < 0) return; // non-persistent / sentinel — should not appear if (entry.kind === "delete") { ensureEntityCapacity(eltState, entry.entity); diff --git a/packages/data-persistence/src/service/incremental-persistence-service.ts b/packages/data-persistence/src/service/incremental-persistence-service.ts index 15191792..01c68299 100644 --- a/packages/data-persistence/src/service/incremental-persistence-service.ts +++ b/packages/data-persistence/src/service/incremental-persistence-service.ts @@ -46,7 +46,7 @@ export interface IncrementalPersistenceServiceOptions { */ readonly transport?: Transport; /** - * Automatically persist every non-transient, non-ephemeral + * Automatically persist every non-transient, persistent * transaction. Defaults to `true`. */ readonly autoPersist?: boolean; diff --git a/packages/data-persistence/src/transport/router.ts b/packages/data-persistence/src/transport/router.ts index 4a94dfce..3a3f710d 100644 --- a/packages/data-persistence/src/transport/router.ts +++ b/packages/data-persistence/src/transport/router.ts @@ -61,7 +61,7 @@ export const createPersistRouter = (backend: PersistenceBackend): PersistRouter } case "writeEntityLocation": { if (op.entity < 0) { - throw new Error(`writeEntityLocation: ephemeral entity not persistable (entity=${op.entity})`); + throw new Error(`writeEntityLocation: non-persistent entity not persistable (entity=${op.entity})`); } const file = await openCached(ENTITY_LOCATION_FILE); const entry = new ArrayBuffer(ELT_STRIDE); @@ -74,7 +74,7 @@ export const createPersistRouter = (backend: PersistenceBackend): PersistRouter } case "deleteEntityLocation": { if (op.entity < 0) { - throw new Error(`deleteEntityLocation: ephemeral entity not persistable (entity=${op.entity})`); + throw new Error(`deleteEntityLocation: non-persistent entity not persistable (entity=${op.entity})`); } const file = await openCached(ENTITY_LOCATION_FILE); const tombstone = new Uint8Array(ELT_STRIDE).fill(0xff); diff --git a/packages/data-sync/src/create-sync-server.ts b/packages/data-sync/src/create-sync-server.ts index 29800331..d2e88561 100644 --- a/packages/data-sync/src/create-sync-server.ts +++ b/packages/data-sync/src/create-sync-server.ts @@ -123,8 +123,8 @@ export const createSyncServer = (options: SyncServerOptions = {}): SyncServer => for (const c of clients) { c.send({ kind: "cancelled", id: msg.id }); } - } else if (msg.kind === "transient") { - // Speculative — relay to peers, never logged. + } else if (msg.kind === "intermediate") { + // Speculative intermediate — relay to peers, never logged. for (const c of clients) { if (c !== origin) c.send({ kind: "committed", envelope: { ...msg.envelope } }); } diff --git a/packages/data-sync/src/create-sync-service.ts b/packages/data-sync/src/create-sync-service.ts index 4140bba4..5132f5e6 100644 --- a/packages/data-sync/src/create-sync-service.ts +++ b/packages/data-sync/src/create-sync-service.ts @@ -19,13 +19,13 @@ export interface SyncServiceOptions { /** Transport connecting this peer to the sync server. */ readonly transport: ClientTransport; /** - * Maximum `"transient"` messages forwarded per second. Intermediate + * Maximum `"intermediate"` messages forwarded per second. Intermediate * samples beyond this rate are silently dropped. Commits and cancels * are always forwarded immediately. * * Defaults to 20 (one per ~50 ms). */ - readonly maxTransientsPerSecond?: number; + readonly maxIntermediatesPerSecond?: number; /** * Session id received from a previous connection's `welcome` message. * Omit (or pass `undefined`) for a fresh client. If the server echoes back @@ -100,11 +100,11 @@ export interface SyncService { * wrapper notifies it after `applyEnvelope` returns). Replays inside the * reconciler and inbound `db.apply()` calls do NOT fire it, so there is * no bounce-back to filter. - * - Envelopes whose `TransactionResult.ephemeral === true` (i.e. they only - * touched ephemeral resources/entities) are skipped. UI-only state lives + * - Envelopes whose `TransactionResult.persistent === false` (i.e. they only + * touched non-persistent resources/entities) are skipped. UI-only state lives * inside the same database without ever reaching the wire. * - `time > 0` → `kind: "propose"` (reliable). - * - `time < 0` → `kind: "transient"` (rate-limited, lossy). + * - `time < 0` → `kind: "intermediate"` (rate-limited, lossy). * - `time === 0` → `kind: "cancel"` (reliable). * * @example @@ -123,7 +123,7 @@ export const createSyncService = (options: SyncServiceOptions): SyncService => { const { database, transport, - maxTransientsPerSecond = 20, + maxIntermediatesPerSecond = 20, priorSessionId, initialWatermark = 0, onWelcome, @@ -139,8 +139,8 @@ export const createSyncService = (options: SyncServiceOptions): SyncService => { ); } - const minIntervalMs = 1000 / maxTransientsPerSecond; - let lastTransientSentAt = 0; + const minIntervalMs = 1000 / maxIntermediatesPerSecond; + let lastIntermediateSentAt = 0; let lastAppliedTime = initialWatermark; let serverSessionId: string | undefined; let lastInboundAt = Date.now(); @@ -204,18 +204,18 @@ export const createSyncService = (options: SyncServiceOptions): SyncService => { }); const unsubscribeOutbound = database.observe.envelopes(({ envelope, result, intent }) => { - // Skip envelopes whose only effect was on ephemeral entities/ + // Skip envelopes whose only effect was on non-persistent entities/ // resources — those stay local-only by design. - if (result?.ephemeral) return; + if (result !== undefined && !result.persistent) return; if (intent === "commit") { log(`propose out (id=${envelope.id}, name=${envelope.name})`); transport.send({ kind: "propose", envelope }); - } else if (intent === "transient") { + } else if (intent === "intermediate") { const now = Date.now(); - if (now - lastTransientSentAt < minIntervalMs) return; - lastTransientSentAt = now; - transport.send({ kind: "transient", envelope }); + if (now - lastIntermediateSentAt < minIntervalMs) return; + lastIntermediateSentAt = now; + transport.send({ kind: "intermediate", envelope }); } else { log(`cancel out (id=${envelope.id})`); transport.send({ kind: "cancel", id: envelope.id }); diff --git a/packages/data-sync/src/transport.ts b/packages/data-sync/src/transport.ts index e8038be7..9b608b5d 100644 --- a/packages/data-sync/src/transport.ts +++ b/packages/data-sync/src/transport.ts @@ -30,7 +30,7 @@ export type ServerMessage = * * - `propose` — client wants to commit an operation. The envelope's `time` * must be negative (transient) when sent; the server assigns the final time. - * - `transient` — speculative state the client wants other clients to see + * - `intermediate` — speculative state the client wants other clients to see * (e.g. cursor position, dragging). Servers may broadcast these as-is or * drop/throttle them; they are never persisted. * - `cancel` — client withdraws a previously proposed envelope. @@ -44,7 +44,7 @@ export type ServerMessage = */ export type ClientMessage = | { readonly kind: "propose"; readonly envelope: TransactionEnvelope } - | { readonly kind: "transient"; readonly envelope: TransactionEnvelope } + | { readonly kind: "intermediate"; readonly envelope: TransactionEnvelope } | { readonly kind: "cancel"; readonly id: number } | { readonly kind: "hello"; readonly sessionId?: string; readonly lastAppliedTime: number } | { readonly kind: "ping" }; diff --git a/packages/data/src/ecs/database/concurrency/concurrency-strategy.ts b/packages/data/src/ecs/database/concurrency/concurrency-strategy.ts index 5f74b270..7bf2e04c 100644 --- a/packages/data/src/ecs/database/concurrency/concurrency-strategy.ts +++ b/packages/data/src/ecs/database/concurrency/concurrency-strategy.ts @@ -18,12 +18,12 @@ import type { TransactionEnvelope } from "../reconciling/reconciling-database.js * (e.g. a collaborative-editing layer that manages its own replay buffer). * * - `createRebaseReplayConcurrency(userId)` — commits apply locally as - * transients and wait for the sync server's echoed committed envelope to + * intermediate steps and wait for the sync server's echoed committed envelope to * promote them. Full rollback-and-replay queue, cross-peer id-determinism. */ export interface ConcurrencyStrategy { /** - * When true, the dispatcher applies "commit" intents as transients + * When true, the dispatcher applies "commit" intents as intermediate steps * (negative time) and waits for a server echo to promote them. * When false, commits apply immediately with positive time. */ @@ -43,20 +43,20 @@ export interface ConcurrencyStrategy { readonly apply: (envelope: TransactionEnvelope) => TransactionResult | undefined; /** - * Cancel a pending transient by compound (userId, id) key. - * No-op for strategies that have no transient queue. + * Cancel a pending intermediate step by compound (userId, id) key. + * No-op for strategies that have no intermediate queue. */ readonly cancel: (id: number, userId?: number | string) => void; /** * Called by `db.reset()` before the store is cleared. - * Strategies with a transient queue should clear it here. + * Strategies with an intermediate queue should clear it here. */ readonly onReset: () => void; /** * Called immediately before `db.toData()` serializes the store. - * Strategies with a transient queue should roll back transients here so + * Strategies with an intermediate queue should roll back intermediate steps here so * the snapshot contains only committed state. * * When this hook (together with `onAfterToData`) is present, `db.toData()` @@ -69,14 +69,14 @@ export interface ConcurrencyStrategy { /** * Called immediately after `db.toData()` serializes the store. * Strategies that roll back in `onBeforeToData` should replay here to - * restore the in-progress transient state. + * restore the in-progress intermediate state. */ readonly onAfterToData?: () => void; /** * Called immediately after `db.fromData()` loads a snapshot into the - * store. Strategies with a transient queue should replay pending - * transients here so in-progress work is visible after a data load. + * store. Strategies with an intermediate queue should replay pending + * intermediate steps here so in-progress work is visible after a data load. */ readonly onAfterFromData?: () => void; } @@ -94,7 +94,7 @@ export interface ConcurrencyStrategy { export type ConcurrencyStrategyFactory = ( execute: ( fn: (ctx: TransactionContext) => void | Entity, - options?: { transient?: boolean; userId?: number | string }, + options?: { intermediate?: boolean; userId?: number | string }, ) => TransactionResult, getTransaction: ( name: string, diff --git a/packages/data/src/ecs/database/concurrency/immediate-concurrency.ts b/packages/data/src/ecs/database/concurrency/immediate-concurrency.ts index 4ab248a3..4edd6177 100644 --- a/packages/data/src/ecs/database/concurrency/immediate-concurrency.ts +++ b/packages/data/src/ecs/database/concurrency/immediate-concurrency.ts @@ -5,7 +5,7 @@ import type { TransactionResult } from "../transactional-store/index.js"; import type { ConcurrencyStrategy, ConcurrencyStrategyFactory } from "./concurrency-strategy.js"; /** - * Lighter-weight concurrency strategy that applies commits and transients + * Lighter-weight concurrency strategy that applies commits and intermediate steps * immediately with no global rollback-and-replay cycle. * * Key differences from {@link createRebaseReplayConcurrency}: @@ -18,17 +18,17 @@ import type { ConcurrencyStrategy, ConcurrencyStrategyFactory } from "./concurre * concurrent edits — use this only when there is a single writer or * when an external layer manages replay (e.g. a collaborative-editing * wrapper). - * - Async generator transients are still supported: each yield applies + * - Async generator intermediate steps are still supported: each yield applies * the intermediate state immediately and rolls it back when the next * yield or the final commit arrives. Cancel rolls back the last - * applied transient for that transaction. Transients from different + * applied intermediate step for that transaction. Intermediate steps from different * concurrent transactions do NOT interact (no cross-transaction rebase). */ export const createImmediateConcurrency = (): ConcurrencyStrategyFactory => (execute, getTransaction): ConcurrencyStrategy => { - // Per-transaction store of the last-applied transient result, keyed - // by compound `"userId:id"`. Used to roll back a transient when a - // newer transient, commit, or cancel arrives for the same transaction. + // Per-transaction store of the last-applied intermediate result, keyed + // by compound `"userId:id"`. Used to roll back an intermediate step when a + // newer intermediate step, commit, or cancel arrives for the same transaction. const pending = new Map>(); const key = (id: number, userId?: number | string) => `${userId}:${id}`; @@ -37,7 +37,7 @@ export const createImmediateConcurrency = (): ConcurrencyStrategyFactory => const k = key(id, userId); const prior = pending.get(k); if (prior) { - execute(t => applyOperations(t, prior.undo), { transient: true, userId: undefined }); + execute(t => applyOperations(t, prior.undo), { intermediate: true, userId: undefined }); pending.delete(k); } }; @@ -58,7 +58,7 @@ export const createImmediateConcurrency = (): ConcurrencyStrategyFactory => rollbackPending(id, userId); const fn = getTransaction(envelope.name); if (!fn) throw new Error(`Unknown transaction: ${envelope.name}`); - const result = execute(t => fn(t, args), { transient: true, userId }); + const result = execute(t => fn(t, args), { intermediate: true, userId }); const isNoOp = result.redo.length === 0 && result.undo.length === 0; if (!isNoOp) pending.set(key(id, userId), result); return result; @@ -69,7 +69,7 @@ export const createImmediateConcurrency = (): ConcurrencyStrategyFactory => rollbackPending(id, userId); const fn = getTransaction(envelope.name); if (!fn) throw new Error(`Unknown transaction: ${envelope.name}`); - return execute(t => fn(t, args), { transient: false, userId }); + return execute(t => fn(t, args), { intermediate: false, userId }); }, cancel(id, userId) { rollbackPending(id, userId); diff --git a/packages/data/src/ecs/database/concurrency/roll-forward-concurrency.ts b/packages/data/src/ecs/database/concurrency/roll-forward-concurrency.ts index d524b7d8..490a7d5e 100644 --- a/packages/data/src/ecs/database/concurrency/roll-forward-concurrency.ts +++ b/packages/data/src/ecs/database/concurrency/roll-forward-concurrency.ts @@ -68,7 +68,7 @@ export const createRollForwardConcurrency = (userId: number | string): Concurren const rollbackAll = () => { for (let i = pending.length - 1; i >= 0; i--) { - execute(t => applyOperations(t, pending[i].undo), { transient: true, userId: undefined }); + execute(t => applyOperations(t, pending[i].undo), { intermediate: true, userId: undefined }); } }; @@ -78,7 +78,7 @@ export const createRollForwardConcurrency = (userId: number | string): Concurren for (const entry of pending) { const result = execute( t => applyOperations(t, entry.redo), - { transient: true, userId: entry.userId }, + { intermediate: true, userId: entry.userId }, ); entry.undo = result.undo; } @@ -92,7 +92,7 @@ export const createRollForwardConcurrency = (userId: number | string): Concurren if (!fn) throw new Error(`Unknown transaction: ${envelope.name}`); return execute( t => fn(t, envelope.args), - { transient: envelope.time < 0, userId: envelope.userId }, + { intermediate: envelope.time < 0, userId: envelope.userId }, ); }; @@ -116,7 +116,7 @@ export const createRollForwardConcurrency = (userId: number | string): Concurren return undefined; } - // Optimistic local transient: roll back, drop any prior version + // Optimistic local intermediate: roll back, drop any prior version // of this transaction, replay the survivors, then run the fn on // top and capture its post-image tuples. if (time < 0) { diff --git a/packages/data/src/ecs/database/database.ts b/packages/data/src/ecs/database/database.ts index b1b93da4..f1c343b2 100644 --- a/packages/data/src/ecs/database/database.ts +++ b/packages/data/src/ecs/database/database.ts @@ -135,18 +135,18 @@ export interface Database< * transaction wrappers (`db.transactions.X(args)`). Replays inside the * reconciler and inbound `db.apply()` calls do NOT fire it. The * `intent` reflects the wrapper's decision regardless of whether the - * envelope was applied locally as a transient (deferred-commit / sync + * envelope was applied locally as an intermediate step (deferred-commit / sync * mode) or as a commit (local-only mode). * * Sync services route forwarding by `intent`: - * - `"commit"` → propose to the server (reliable) - * - `"transient"` → relay to peers (lossy) - * - `"cancel"` → cancel a pending transient (reliable) + * - `"commit"` → propose to the server (reliable) + * - `"intermediate"` → relay to peers (lossy) + * - `"cancel"` → cancel a pending intermediate step (reliable) */ readonly envelopes: Observe<{ envelope: TransactionEnvelope; result: TransactionResult | undefined; - intent: "commit" | "transient" | "cancel"; + intent: "commit" | "intermediate" | "cancel"; }>; entity(id: Entity, minArchetype?: ReadonlyArchetype | Archetype): Observe & EntityReadValues | null>; entity(id: Entity): Observe | null>; diff --git a/packages/data/src/ecs/database/observed/create-observed-database.ts b/packages/data/src/ecs/database/observed/create-observed-database.ts index 265cbbd0..c921c01d 100644 --- a/packages/data/src/ecs/database/observed/create-observed-database.ts +++ b/packages/data/src/ecs/database/observed/create-observed-database.ts @@ -115,9 +115,9 @@ export function createObservedDatabase< const observeComponent = mapEntries(store.componentSchemas, ([component]) => addToMapSet(component, componentObservers)); const resourceArchetypeComponents = (resource: string): StringKeyof[] => { - const isEphemeral = (store.componentSchemas as any)[resource]?.ephemeral; - return isEphemeral - ? ["id" as StringKeyof, resource as unknown as StringKeyof, "ephemeral" as StringKeyof] + const isNonPersistent = (store.componentSchemas as any)[resource]?.nonPersistent ?? (store.componentSchemas as any)[resource]?.ephemeral; + return isNonPersistent + ? ["id" as StringKeyof, resource as unknown as StringKeyof, "nonPersistent" as StringKeyof] : ["id" as StringKeyof, resource as unknown as StringKeyof]; }; @@ -161,8 +161,8 @@ export function createObservedDatabase< updateValues ]; })), - transient: false, - ephemeral: false, + intermediate: false, + persistent: true, value: undefined, undo: [], redo: [], diff --git a/packages/data/src/ecs/database/public/create-transaction-dispatcher.ts b/packages/data/src/ecs/database/public/create-transaction-dispatcher.ts index 2bc46348..4d8908e1 100644 --- a/packages/data/src/ecs/database/public/create-transaction-dispatcher.ts +++ b/packages/data/src/ecs/database/public/create-transaction-dispatcher.ts @@ -11,9 +11,9 @@ import { TransactionResult } from "../transactional-store/index.js"; * commit and an async-generator yield apply locally with `time < 0`. The * intent records the wrapper's reason for emitting the envelope so a sync * service can route each one correctly without re-deriving it from the time - * sign. + * sign. `"intermediate"` means a non-final step in an async sequence. */ -export type TransactionIntent = "commit" | "transient" | "cancel"; +export type TransactionIntent = "commit" | "intermediate" | "cancel"; export type TransactionEnvelopeEvent = { readonly envelope: TransactionEnvelope; @@ -39,10 +39,10 @@ export type DispatcherTarget = ( * * - The per-database transaction id counter. * - Resolving the `args` argument shape (plain value, promise, or - * async-generator factory) into a sequence of `applyTransient` / + * async-generator factory) into a sequence of `applyIntermediate` / * `applyCommit` / `cancelPending` calls. * - Whether a "commit" intent should be applied as a positive-time commit - * (local-only mode) or a negative-time transient pending server echo + * (local-only mode) or a negative-time intermediate step pending server echo * (sync mode). Decided once at construction by the presence of * `sync` — never mutated at runtime. * - Stamping `userId` onto every envelope (sync mode only). @@ -94,18 +94,18 @@ export const createTransactionDispatcher = ( const wrap: TransactionDispatcher["wrap"] = (name) => (args) => { const transactionId = nextTransactionId++; - let hasTransient = false; + let hasIntermediate = false; - const applyTransient = (payload: unknown) => { - hasTransient = true; + const applyIntermediate = (payload: unknown) => { + hasIntermediate = true; dispatchEnvelope( { id: transactionId, userId, name, args: payload, time: -Date.now() }, - "transient", + "intermediate", ); }; const applyCommit = (payload: unknown) => { - // In sync mode the commit is applied locally as a transient + // In sync mode the commit is applied locally as an intermediate step // and the server's echoed `committed` envelope will promote it. // In local-only mode it commits immediately with positive time. const time = deferredCommit ? -Date.now() : Date.now(); @@ -113,25 +113,25 @@ export const createTransactionDispatcher = ( { id: transactionId, userId, name, args: payload, time }, "commit", ); - // Only mark a pending transient if the envelope was effective (not + // Only mark a pending intermediate if the envelope was effective (not // a no-op). A no-op envelope is suppressed by dispatchEnvelope and // will never receive a server promotion, so we must not set - // hasTransient — doing so would cause a spurious cancel later. + // hasIntermediate — doing so would cause a spurious cancel later. const effective = result === undefined || result.redo.length > 0 || result.undo.length > 0; - hasTransient = deferredCommit && effective; + hasIntermediate = deferredCommit && effective; return result?.value; }; const cancelPending = () => { - if (!hasTransient) return; + if (!hasIntermediate) return; dispatchEnvelope( { id: transactionId, userId, name, args: undefined, time: 0 }, "cancel", ); - hasTransient = false; + hasIntermediate = false; }; - return runTransaction(args, applyTransient, applyCommit, cancelPending); + return runTransaction(args, applyIntermediate, applyCommit, cancelPending); }; return { wrap, envelopes }; @@ -139,7 +139,7 @@ export const createTransactionDispatcher = ( /** * Resolves the `args` shape passed to `db.transactions.X(args)` into the - * appropriate `applyTransient` / `applyCommit` / `cancelPending` sequence. + * appropriate `applyIntermediate` / `applyCommit` / `cancelPending` sequence. * * Three argument shapes are supported: * @@ -147,7 +147,7 @@ export const createTransactionDispatcher = ( * - Function returning a `Promise`: `applyCommit(await promise)`, with * `cancelPending` on rejection. * - Function returning an `AsyncGenerator`: each yielded value becomes an - * `applyTransient`; on `done`, the final yielded value (or the explicit + * `applyIntermediate`; on `done`, the final yielded value (or the explicit * return value if defined) becomes `applyCommit`. If no value was ever * yielded and none returned, `cancelPending` runs instead. A thrown * iterator results in `cancelPending` plus a rejected promise — this is @@ -156,7 +156,7 @@ export const createTransactionDispatcher = ( */ const runTransaction = ( args: unknown, - applyTransient: (payload: unknown) => void, + applyIntermediate: (payload: unknown) => void, applyCommit: (payload: unknown) => unknown, cancelPending: () => void, ): unknown => { @@ -174,7 +174,7 @@ const runTransaction = ( let iteration = await providerResult.next(); while (!iteration.done) { lastArgs = iteration.value; - applyTransient(iteration.value); + applyIntermediate(iteration.value); iteration = await providerResult.next(); } const finalArgs = iteration.value !== undefined ? iteration.value : lastArgs; diff --git a/packages/data/src/ecs/database/reconciling/create-rebase-replay-applier.ts b/packages/data/src/ecs/database/reconciling/create-rebase-replay-applier.ts index 6cc5643f..6ca179f9 100644 --- a/packages/data/src/ecs/database/reconciling/create-rebase-replay-applier.ts +++ b/packages/data/src/ecs/database/reconciling/create-rebase-replay-applier.ts @@ -41,7 +41,7 @@ export function createRebaseReplayApplier( const rollbackEntryResult = (entry: ReconcilingEntry) => { if (entry.result) { - execute(t => applyOperations(t, entry.result!.undo), { transient: true, userId: undefined }); + execute(t => applyOperations(t, entry.result!.undo), { intermediate: true, userId: undefined }); entry.result = undefined; } }; @@ -55,7 +55,7 @@ export function createRebaseReplayApplier( const executeEntry = (entry: ReconcilingEntry) => { const fn = getTransaction(entry.name); if (!fn) throw new Error(`Unknown transaction during replay: ${entry.name}`); - const result = execute(t => fn(t, entry.args), { transient: true, userId: entry.userId }); + const result = execute(t => fn(t, entry.args), { intermediate: true, userId: entry.userId }); const isNoOp = result.redo.length === 0 && result.undo.length === 0; entry.result = isNoOp ? undefined : result; return result; @@ -123,7 +123,7 @@ export function createRebaseReplayApplier( if (!fn) throw new Error(`Unknown transaction: ${envelope.name}`); rollbackAllTransients(); spliceTransientEntry(id, userId); - const result = execute(t => fn(t, args), { transient: false, userId }); + const result = execute(t => fn(t, args), { intermediate: false, userId }); replayAllTransients(); return result; }; diff --git a/packages/data/src/ecs/database/transactional-store/coalesce-actions.test.ts b/packages/data/src/ecs/database/transactional-store/coalesce-actions.test.ts index 892940bf..3030e947 100644 --- a/packages/data/src/ecs/database/transactional-store/coalesce-actions.test.ts +++ b/packages/data/src/ecs/database/transactional-store/coalesce-actions.test.ts @@ -8,8 +8,8 @@ describe("shouldCoalesceTransactions", () => { it("should return true for actions with same coalesce values", () => { const previous: TransactionResult = { value: 1, - transient: false, - ephemeral: false, + intermediate: false, + persistent: true, undoable: { coalesce: { id: "updateEntity", entity: 123 } }, redo: [], undo: [], @@ -20,8 +20,8 @@ describe("shouldCoalesceTransactions", () => { const current: TransactionResult = { value: 2, - transient: false, - ephemeral: false, + intermediate: false, + persistent: true, undoable: { coalesce: { id: "updateEntity", entity: 123 } }, redo: [], undo: [], @@ -36,8 +36,8 @@ describe("shouldCoalesceTransactions", () => { it("should return false for actions with different coalesce values", () => { const previous: TransactionResult = { value: 1, - transient: false, - ephemeral: false, + intermediate: false, + persistent: true, undoable: { coalesce: { id: "updateEntity", entity: 123 } }, redo: [], undo: [], @@ -48,8 +48,8 @@ describe("shouldCoalesceTransactions", () => { const current: TransactionResult = { value: 2, - transient: false, - ephemeral: false, + intermediate: false, + persistent: true, undoable: { coalesce: { id: "updateEntity", entity: 456 } }, redo: [], undo: [], @@ -64,8 +64,8 @@ describe("shouldCoalesceTransactions", () => { it("should return false when previous transaction has coalesce: false", () => { const previous: TransactionResult = { value: 1, - transient: false, - ephemeral: false, + intermediate: false, + persistent: true, undoable: { coalesce: false }, redo: [], undo: [], @@ -76,8 +76,8 @@ describe("shouldCoalesceTransactions", () => { const current: TransactionResult = { value: 2, - transient: false, - ephemeral: false, + intermediate: false, + persistent: true, undoable: { coalesce: { id: "updateEntity", entity: 123 } }, redo: [], undo: [], @@ -92,8 +92,8 @@ describe("shouldCoalesceTransactions", () => { it("should return false when current transaction has coalesce: false", () => { const previous: TransactionResult = { value: 1, - transient: false, - ephemeral: false, + intermediate: false, + persistent: true, undoable: { coalesce: { id: "updateEntity", entity: 123 } }, redo: [], undo: [], @@ -104,8 +104,8 @@ describe("shouldCoalesceTransactions", () => { const current: TransactionResult = { value: 2, - transient: false, - ephemeral: false, + intermediate: false, + persistent: true, undoable: { coalesce: false }, redo: [], undo: [], @@ -120,8 +120,8 @@ describe("shouldCoalesceTransactions", () => { it("should return false when previous transaction has no undoable", () => { const previous: TransactionResult = { value: 1, - transient: false, - ephemeral: false, + intermediate: false, + persistent: true, undoable: null, redo: [], undo: [], @@ -132,8 +132,8 @@ describe("shouldCoalesceTransactions", () => { const current: TransactionResult = { value: 2, - transient: false, - ephemeral: false, + intermediate: false, + persistent: true, undoable: { coalesce: { id: "updateEntity", entity: 123 } }, redo: [], undo: [], @@ -148,8 +148,8 @@ describe("shouldCoalesceTransactions", () => { it("should return false when current transaction has no undoable", () => { const previous: TransactionResult = { value: 1, - transient: false, - ephemeral: false, + intermediate: false, + persistent: true, undoable: { coalesce: { id: "updateEntity", entity: 123 } }, redo: [], undo: [], @@ -160,8 +160,8 @@ describe("shouldCoalesceTransactions", () => { const current: TransactionResult = { value: 2, - transient: false, - ephemeral: false, + intermediate: false, + persistent: true, undoable: null, redo: [], undo: [], @@ -178,8 +178,8 @@ describe("coalesceTransactions", () => { it("should combine redo and undo operations", () => { const previous: TransactionResult = { value: 1, - transient: false, - ephemeral: false, + intermediate: false, + persistent: true, undoable: { coalesce: { id: "updateEntity", entity: 123 } }, redo: [{ type: "update", entity: 123, values: { position: { x: 1 } } }], undo: [{ type: "update", entity: 123, values: { position: { x: 0 } } }], @@ -190,8 +190,8 @@ describe("coalesceTransactions", () => { const current: TransactionResult = { value: 2, - transient: false, - ephemeral: false, + intermediate: false, + persistent: true, undoable: { coalesce: { id: "updateEntity", entity: 123 } }, redo: [{ type: "update", entity: 123, values: { position: { y: 2 } } }], undo: [{ type: "update", entity: 123, values: { position: { y: 0 } } }], @@ -220,8 +220,8 @@ describe("coalesceTransactions", () => { it("should merge changed entities, components, and archetypes", () => { const previous: TransactionResult = { value: 1, - transient: false, - ephemeral: false, + intermediate: false, + persistent: true, undoable: { coalesce: { id: "updateEntity", entity: 123 } }, redo: [], undo: [], @@ -232,8 +232,8 @@ describe("coalesceTransactions", () => { const current: TransactionResult = { value: 2, - transient: false, - ephemeral: false, + intermediate: false, + persistent: true, undoable: { coalesce: { id: "updateEntity", entity: 123 } }, redo: [], undo: [], @@ -260,8 +260,8 @@ describe("coalesceTransactions", () => { it("should use current transaction's value, transient, and undoable", () => { const previous: TransactionResult = { value: 1, - transient: false, - ephemeral: false, + intermediate: false, + persistent: true, undoable: { coalesce: { id: "updateEntity", entity: 123 } }, redo: [], undo: [], @@ -272,8 +272,8 @@ describe("coalesceTransactions", () => { const current: TransactionResult = { value: 2, - transient: true, - ephemeral: false, + intermediate: true, + persistent: true, undoable: { coalesce: { id: "updateEntity", entity: 123 } }, redo: [], undo: [], @@ -285,15 +285,15 @@ describe("coalesceTransactions", () => { const result = coalesceTransactions(previous, current); expect(result.value).toBe(2); - expect(result.transient).toBe(true); + expect(result.intermediate).toBe(true); expect(result.undoable).toEqual({ coalesce: { id: "updateEntity", entity: 123 } }); }); it("should NOT optimize insert + update operations (cannot verify same entity)", () => { const previous: TransactionResult = { value: 1, - transient: false, - ephemeral: false, + intermediate: false, + persistent: true, undoable: { coalesce: { id: "createEntity", entity: 123 } }, redo: [{ type: "insert", values: { position: { x: 1 } } }], undo: [{ type: "delete", entity: 123 }], @@ -304,8 +304,8 @@ describe("coalesceTransactions", () => { const current: TransactionResult = { value: 2, - transient: false, - ephemeral: false, + intermediate: false, + persistent: true, undoable: { coalesce: { id: "updateEntity", entity: 123 } }, redo: [{ type: "update", entity: 123, values: { position: { y: 2 } } }], undo: [{ type: "update", entity: 123, values: { position: { y: 0 } } }], @@ -332,8 +332,8 @@ describe("coalesceTransactions", () => { it("should NOT optimize insert + delete operations (cannot verify same entity)", () => { const previous: TransactionResult = { value: 1, - transient: false, - ephemeral: false, + intermediate: false, + persistent: true, undoable: { coalesce: { id: "createEntity", entity: 123 } }, redo: [{ type: "insert", values: { position: { x: 1 } } }], undo: [{ type: "delete", entity: 123 }], @@ -344,8 +344,8 @@ describe("coalesceTransactions", () => { const current: TransactionResult = { value: 2, - transient: false, - ephemeral: false, + intermediate: false, + persistent: true, undoable: { coalesce: { id: "deleteEntity", entity: 123 } }, redo: [{ type: "delete", entity: 123 }], undo: [{ type: "insert", values: { position: { x: 1 } } }], @@ -370,8 +370,8 @@ describe("coalesceTransactions", () => { it("should handle multiple update operations on same entity", () => { const previous: TransactionResult = { value: 1, - transient: false, - ephemeral: false, + intermediate: false, + persistent: true, undoable: { coalesce: { id: "updateEntity", entity: 123 } }, redo: [{ type: "update", entity: 123, values: { position: { x: 1 } } }], undo: [{ type: "update", entity: 123, values: { position: { x: 0 } } }], @@ -382,8 +382,8 @@ describe("coalesceTransactions", () => { const current: TransactionResult = { value: 2, - transient: false, - ephemeral: false, + intermediate: false, + persistent: true, undoable: { coalesce: { id: "updateEntity", entity: 123 } }, redo: [ { type: "update", entity: 123, values: { position: { y: 2 } } }, @@ -419,8 +419,8 @@ describe("coalesceTransactions", () => { it("should optimize update + delete operations on same entity", () => { const previous: TransactionResult = { value: 1, - transient: false, - ephemeral: false, + intermediate: false, + persistent: true, undoable: { coalesce: { id: "updateEntity", entity: 123 } }, redo: [{ type: "update", entity: 123, values: { position: { x: 1 } } }], undo: [{ type: "update", entity: 123, values: { position: { x: 0 } } }], @@ -431,8 +431,8 @@ describe("coalesceTransactions", () => { const current: TransactionResult = { value: 2, - transient: false, - ephemeral: false, + intermediate: false, + persistent: true, undoable: { coalesce: { id: "deleteEntity", entity: 123 } }, redo: [{ type: "delete", entity: 123 }], undo: [{ type: "insert", values: { position: { x: 1 } } }], @@ -467,8 +467,8 @@ describe("coalesceTransactions", () => { const previous: TransactionResult = { value: 1, - transient: false, - ephemeral: false, + intermediate: false, + persistent: true, undoable: { coalesce: { id: "createEntity", entity: 123 } }, redo: [{ type: "insert", values: { position: { x: 1 } } }], undo: [{ type: "delete", entity: 123 }], @@ -479,8 +479,8 @@ describe("coalesceTransactions", () => { const current: TransactionResult = { value: 2, - transient: false, - ephemeral: false, + intermediate: false, + persistent: true, undoable: { coalesce: { id: "updateEntity", entity: 456 } }, redo: [{ type: "update", entity: 456, values: { position: { y: 2 } } }], undo: [{ type: "update", entity: 456, values: { position: { y: 0 } } }], diff --git a/packages/data/src/ecs/database/transactional-store/coalesce-actions.ts b/packages/data/src/ecs/database/transactional-store/coalesce-actions.ts index 2ef01b52..fccbcb96 100644 --- a/packages/data/src/ecs/database/transactional-store/coalesce-actions.ts +++ b/packages/data/src/ecs/database/transactional-store/coalesce-actions.ts @@ -99,8 +99,8 @@ export function coalesceTransactions( return { value: current.value, - transient: current.transient, - ephemeral: previous.ephemeral && current.ephemeral, + intermediate: current.intermediate, + persistent: previous.persistent || current.persistent, undoable: current.undoable, redo: combinedRedo, undo: combinedUndo, diff --git a/packages/data/src/ecs/database/transactional-store/create-transactional-store.test.ts b/packages/data/src/ecs/database/transactional-store/create-transactional-store.test.ts index 75a7c19e..60f5c6af 100644 --- a/packages/data/src/ecs/database/transactional-store/create-transactional-store.test.ts +++ b/packages/data/src/ecs/database/transactional-store/create-transactional-store.test.ts @@ -215,15 +215,15 @@ describe("createTransactionalStore", () => { archetype.insert({ position: { x: 1, y: 2, z: 3 } }); }); - expect(regularResult.transient).toBe(false); + expect(regularResult.intermediate).toBe(false); - // Execute a transient transaction + // Execute an intermediate transaction const transientResult = store.execute((t) => { const archetype = t.ensureArchetype(["id", "position"]); archetype.insert({ position: { x: 10, y: 20, z: 30 } }); - }, { transient: true }); + }, { intermediate: true }); - expect(transientResult.transient).toBe(true); + expect(transientResult.intermediate).toBe(true); }); it("should track specific components and archetypes when creating entities", () => { @@ -550,8 +550,8 @@ describe("createTransactionalStore", () => { expect(extended).toBe(transactionalStore); }); - describe("ephemeral", () => { - it("should mark transaction as ephemeral when only ephemeral entities are changed", () => { + describe("persistent", () => { + it("should not mark transaction as persistent when only non-persistent entities are changed", () => { const store = createTransactionalStore(Store.create({ components: { position: positionSchema }, resources: {}, @@ -559,15 +559,15 @@ describe("createTransactionalStore", () => { })); const result = store.execute(t => { - const archetype = t.ensureArchetype(["id", "position", "ephemeral"]); - const entity = archetype.insert({ position: { x: 1, y: 2, z: 3 }, ephemeral: true }); - expect(Entity.isEphemeral(entity)).toBe(true); + const archetype = t.ensureArchetype(["id", "position", "nonPersistent"]); + const entity = archetype.insert({ position: { x: 1, y: 2, z: 3 }, nonPersistent: true }); + expect(Entity.isNonPersistent(entity)).toBe(true); }); - expect(result.ephemeral).toBe(true); + expect(result.persistent).toBe(false); }); - it("should not mark transaction as ephemeral when persistent entities are changed", () => { + it("should mark transaction as persistent when persistent entities are changed", () => { const store = createTransactionalStore(Store.create({ components: { position: positionSchema }, resources: {}, @@ -579,10 +579,10 @@ describe("createTransactionalStore", () => { archetype.insert({ position: { x: 1, y: 2, z: 3 } }); }); - expect(result.ephemeral).toBe(false); + expect(result.persistent).toBe(true); }); - it("should not mark transaction as ephemeral when mixing ephemeral and persistent entities", () => { + it("should mark transaction as persistent when mixing non-persistent and persistent entities", () => { const store = createTransactionalStore(Store.create({ components: { position: positionSchema }, resources: {}, @@ -593,18 +593,18 @@ describe("createTransactionalStore", () => { const persistent = t.ensureArchetype(["id", "position"]); persistent.insert({ position: { x: 1, y: 2, z: 3 } }); - const ephemeral = t.ensureArchetype(["id", "position", "ephemeral"]); - ephemeral.insert({ position: { x: 4, y: 5, z: 6 }, ephemeral: true }); + const nonPersistent = t.ensureArchetype(["id", "position", "nonPersistent"]); + nonPersistent.insert({ position: { x: 4, y: 5, z: 6 }, nonPersistent: true }); }); - expect(result.ephemeral).toBe(false); + expect(result.persistent).toBe(true); }); - it("should mark transaction as ephemeral when only ephemeral resources are updated", () => { + it("should not mark transaction as persistent when only non-persistent resources are updated", () => { const store = createTransactionalStore(Store.create({ components: {}, resources: { - cursor: { type: "object", properties: { x: F32.schema, y: F32.schema }, default: { x: 0, y: 0 }, ephemeral: true } as const satisfies Schema & { default: unknown }, + cursor: { type: "object", properties: { x: F32.schema, y: F32.schema }, default: { x: 0, y: 0 }, nonPersistent: true } as const satisfies Schema & { default: unknown }, }, archetypes: {} })); @@ -613,29 +613,29 @@ describe("createTransactionalStore", () => { t.resources.cursor = { x: 10, y: 20 }; }); - expect(result.ephemeral).toBe(true); + expect(result.persistent).toBe(false); }); - it("should give ephemeral resources negative entity IDs", () => { + it("should give non-persistent resources negative entity IDs", () => { const baseStore = Store.create({ components: {}, resources: { - cursor: { type: "object", properties: { x: F32.schema, y: F32.schema }, default: { x: 0, y: 0 }, ephemeral: true } as const satisfies Schema & { default: unknown }, + cursor: { type: "object", properties: { x: F32.schema, y: F32.schema }, default: { x: 0, y: 0 }, nonPersistent: true } as const satisfies Schema & { default: unknown }, score: { type: "number", default: 0 } as const satisfies Schema & { default: unknown }, }, archetypes: {} }); - const cursorArchetype = (baseStore as any).ensureArchetype(["id", "cursor", "ephemeral"]); + const cursorArchetype = (baseStore as any).ensureArchetype(["id", "cursor", "nonPersistent"]); const cursorEntityId = cursorArchetype.columns.id.get(0); - expect(Entity.isEphemeral(cursorEntityId)).toBe(true); + expect(Entity.isNonPersistent(cursorEntityId)).toBe(true); const scoreArchetype = (baseStore as any).ensureArchetype(["id", "score"]); const scoreEntityId = scoreArchetype.columns.id.get(0); - expect(Entity.isEphemeral(scoreEntityId)).toBe(false); + expect(Entity.isNonPersistent(scoreEntityId)).toBe(false); }); - it("should not mark transaction as ephemeral when persistent resources are updated", () => { + it("should mark transaction as persistent when persistent resources are updated", () => { const store = createTransactionalStore(Store.create({ components: {}, resources: { @@ -648,14 +648,14 @@ describe("createTransactionalStore", () => { t.resources.score = 42; }); - expect(result.ephemeral).toBe(false); + expect(result.persistent).toBe(true); }); - it("should not mark transaction as ephemeral when mixing ephemeral and persistent resources", () => { + it("should mark transaction as persistent when mixing non-persistent and persistent resources", () => { const store = createTransactionalStore(Store.create({ components: {}, resources: { - cursor: { type: "object", properties: { x: F32.schema, y: F32.schema }, default: { x: 0, y: 0 }, ephemeral: true } as const satisfies Schema & { default: unknown }, + cursor: { type: "object", properties: { x: F32.schema, y: F32.schema }, default: { x: 0, y: 0 }, nonPersistent: true } as const satisfies Schema & { default: unknown }, score: { type: "number", default: 0 } as const satisfies Schema & { default: unknown }, }, archetypes: {} @@ -666,10 +666,10 @@ describe("createTransactionalStore", () => { t.resources.score = 42; }); - expect(result.ephemeral).toBe(false); + expect(result.persistent).toBe(true); }); - it("should be ephemeral: false when no entities are changed", () => { + it("should be persistent: false when no entities are changed", () => { const store = createTransactionalStore(Store.create({ components: { position: positionSchema }, resources: {}, @@ -678,10 +678,10 @@ describe("createTransactionalStore", () => { const result = store.execute(() => {}); - expect(result.ephemeral).toBe(false); + expect(result.persistent).toBe(false); }); - it("should mark update of ephemeral entity as ephemeral", () => { + it("should not mark update of non-persistent entity as persistent", () => { const store = createTransactionalStore(Store.create({ components: { position: positionSchema }, resources: {}, @@ -690,18 +690,18 @@ describe("createTransactionalStore", () => { let entity: number; store.execute(t => { - const archetype = t.ensureArchetype(["id", "position", "ephemeral"]); - entity = archetype.insert({ position: { x: 1, y: 2, z: 3 }, ephemeral: true }); + const archetype = t.ensureArchetype(["id", "position", "nonPersistent"]); + entity = archetype.insert({ position: { x: 1, y: 2, z: 3 }, nonPersistent: true }); }); const result = store.execute(t => { t.update(entity!, { position: { x: 10, y: 20, z: 30 } }); }); - expect(result.ephemeral).toBe(true); + expect(result.persistent).toBe(false); }); - it("should mark delete of ephemeral entity as ephemeral", () => { + it("should not mark delete of non-persistent entity as persistent", () => { const store = createTransactionalStore(Store.create({ components: { position: positionSchema }, resources: {}, @@ -710,18 +710,18 @@ describe("createTransactionalStore", () => { let entity: number; store.execute(t => { - const archetype = t.ensureArchetype(["id", "position", "ephemeral"]); - entity = archetype.insert({ position: { x: 1, y: 2, z: 3 }, ephemeral: true }); + const archetype = t.ensureArchetype(["id", "position", "nonPersistent"]); + entity = archetype.insert({ position: { x: 1, y: 2, z: 3 }, nonPersistent: true }); }); const result = store.execute(t => { t.delete(entity!); }); - expect(result.ephemeral).toBe(true); + expect(result.persistent).toBe(false); }); - it("should preserve ephemeral when coalescing two ephemeral transactions", () => { + it("should preserve non-persistent when coalescing two non-persistent transactions", () => { const store = createTransactionalStore(Store.create({ components: { position: positionSchema }, resources: {}, @@ -730,31 +730,31 @@ describe("createTransactionalStore", () => { let entity: number; const first = store.execute(t => { - const archetype = t.ensureArchetype(["id", "position", "ephemeral"]); - entity = archetype.insert({ position: { x: 1, y: 2, z: 3 }, ephemeral: true }); + const archetype = t.ensureArchetype(["id", "position", "nonPersistent"]); + entity = archetype.insert({ position: { x: 1, y: 2, z: 3 }, nonPersistent: true }); }); const second = store.execute(t => { t.update(entity!, { position: { x: 10, y: 20, z: 30 } }); }); - expect(first.ephemeral).toBe(true); - expect(second.ephemeral).toBe(true); + expect(first.persistent).toBe(false); + expect(second.persistent).toBe(false); const coalesced = coalesceTransactions(first, second); - expect(coalesced.ephemeral).toBe(true); + expect(coalesced.persistent).toBe(false); }); - it("should not be ephemeral when coalescing ephemeral with non-ephemeral", () => { + it("should be persistent when coalescing non-persistent with persistent", () => { const store = createTransactionalStore(Store.create({ components: { position: positionSchema }, resources: {}, archetypes: {} })); - const ephemeralResult = store.execute(t => { - const archetype = t.ensureArchetype(["id", "position", "ephemeral"]); - archetype.insert({ position: { x: 1, y: 2, z: 3 }, ephemeral: true }); + const nonPersistentResult = store.execute(t => { + const archetype = t.ensureArchetype(["id", "position", "nonPersistent"]); + archetype.insert({ position: { x: 1, y: 2, z: 3 }, nonPersistent: true }); }); const persistentResult = store.execute(t => { @@ -762,8 +762,8 @@ describe("createTransactionalStore", () => { archetype.insert({ position: { x: 4, y: 5, z: 6 } }); }); - const coalesced = coalesceTransactions(ephemeralResult, persistentResult); - expect(coalesced.ephemeral).toBe(false); + const coalesced = coalesceTransactions(nonPersistentResult, persistentResult); + expect(coalesced.persistent).toBe(true); }); }); }); \ No newline at end of file diff --git a/packages/data/src/ecs/database/transactional-store/create-transactional-store.ts b/packages/data/src/ecs/database/transactional-store/create-transactional-store.ts index f4edb576..617832b1 100644 --- a/packages/data/src/ecs/database/transactional-store/create-transactional-store.ts +++ b/packages/data/src/ecs/database/transactional-store/create-transactional-store.ts @@ -33,9 +33,9 @@ export function createTransactionalStore< // Transaction state (mutable during transaction execution) let undoOperationsInReverseOrder: TransactionWriteOperation[] = []; let redoOperations: TransactionWriteOperation[] = []; - let nonEphemeral = false; + let hasPersistentChange = false; const trackEntity = (entity: Entity) => { - if (!Entity.isEphemeral(entity)) nonEphemeral = true; + if (Entity.isPersistent(entity)) hasPersistentChange = true; }; const changed = { entities: new Map | null>(), @@ -147,9 +147,9 @@ export function createTransactionalStore< const resources = {} as { [K in keyof R]: R[K] }; for (const name of Object.keys(store.resources)) { const resourceId = name as keyof C; - const isEphemeral = (store.componentSchemas as any)[name]?.ephemeral; - const componentNames = isEphemeral - ? ["id", resourceId, "ephemeral"] as StringKeyof[] + const isNonPersistent = (store.componentSchemas as any)[name]?.nonPersistent ?? (store.componentSchemas as any)[name]?.ephemeral; + const componentNames = isNonPersistent + ? ["id", resourceId, "nonPersistent"] as StringKeyof[] : ["id", resourceId] as StringKeyof[]; const archetype = store.ensureArchetype(componentNames); const entityId = archetype.columns.id.get(0); @@ -188,7 +188,7 @@ export function createTransactionalStore< const execute = ( transactionFunction: (t: TransactionContext) => Entity | void, options?: { - transient?: boolean; + intermediate?: boolean; userId?: number | string; } ): TransactionResult => { @@ -196,7 +196,7 @@ export function createTransactionalStore< transactionStore.userId = options?.userId; undoOperationsInReverseOrder = []; redoOperations = []; - nonEphemeral = false; + hasPersistentChange = false; changed.entities.clear(); changed.components.clear(); changed.archetypes.clear(); @@ -211,8 +211,8 @@ export function createTransactionalStore< const result: TransactionResult = { value: value ?? undefined, - transient: options?.transient ?? false, - ephemeral: !nonEphemeral && changed.entities.size > 0, + intermediate: options?.intermediate ?? false, + persistent: hasPersistentChange, undoable: transactionStore.undoable ?? null, redo: coalescedRedo, undo: coalescedUndo, @@ -230,7 +230,7 @@ export function createTransactionalStore< transactionStore.userId = undefined; undoOperationsInReverseOrder = []; redoOperations = []; - nonEphemeral = false; + hasPersistentChange = false; changed.entities.clear(); changed.components.clear(); changed.archetypes.clear(); @@ -255,9 +255,9 @@ export function createTransactionalStore< for (const name of Object.keys(store.resources)) { if (!Object.hasOwn(resources, name)) { const resourceId = name as keyof C; - const isEphemeral = (store.componentSchemas as any)[name]?.ephemeral; - const componentNames = isEphemeral - ? ["id", resourceId, "ephemeral"] as StringKeyof[] + const isNonPersistent = (store.componentSchemas as any)[name]?.nonPersistent ?? (store.componentSchemas as any)[name]?.ephemeral; + const componentNames = isNonPersistent + ? ["id", resourceId, "nonPersistent"] as StringKeyof[] : ["id", resourceId] as StringKeyof[]; const archetype = store.ensureArchetype(componentNames); const entityId = archetype.columns.id.get(0); diff --git a/packages/data/src/ecs/database/transactional-store/transaction-options.ts b/packages/data/src/ecs/database/transactional-store/transaction-options.ts index 2eae4dc2..70b6967b 100644 --- a/packages/data/src/ecs/database/transactional-store/transaction-options.ts +++ b/packages/data/src/ecs/database/transactional-store/transaction-options.ts @@ -4,10 +4,10 @@ import { Undoable } from "../undoable.js"; export type TransactionOptions = { /** - * If this is a transient operation then it should not be persisted. - * When an async sequence of operations is executed, they are all transient except the last one. + * If this is an intermediate operation then it should not be persisted. + * When an async sequence of operations is executed, they are all intermediate except the last one. */ - readonly transient?: boolean; + readonly intermediate?: boolean; /** * This value must be set fo undoable operations. */ diff --git a/packages/data/src/ecs/database/transactional-store/transactional-store.ts b/packages/data/src/ecs/database/transactional-store/transactional-store.ts index 8d16f05c..52b7ae62 100644 --- a/packages/data/src/ecs/database/transactional-store/transactional-store.ts +++ b/packages/data/src/ecs/database/transactional-store/transactional-store.ts @@ -46,7 +46,7 @@ export interface TransactionalStore< execute( transactionFunction: (t: TransactionContext) => Entity | void, options?: { - transient?: boolean; + intermediate?: boolean; userId?: number | string; } ): TransactionResult; @@ -89,9 +89,9 @@ export interface TransactionResult { */ readonly value: Entity | void; /** True when the transaction is a non-final intermediate operation within a sequence. */ - readonly transient: boolean; - /** True when all changed entities are ephemeral (entity id < 0). */ - readonly ephemeral: boolean; + readonly intermediate: boolean; + /** True when at least one changed entity is persistent (entity id >= 0). */ + readonly persistent: boolean; readonly undoable: null | Undoable; readonly redo: TransactionWriteOperation[]; readonly undo: TransactionWriteOperation[]; diff --git a/packages/data/src/ecs/entity/is-ephemeral.test.ts b/packages/data/src/ecs/entity/is-ephemeral.test.ts index 001200bb..529e7435 100644 --- a/packages/data/src/ecs/entity/is-ephemeral.test.ts +++ b/packages/data/src/ecs/entity/is-ephemeral.test.ts @@ -2,15 +2,35 @@ import { describe, it, expect } from "vitest"; import { Entity } from "./entity.js"; -describe("Entity.isEphemeral", () => { +describe("Entity.isNonPersistent", () => { it("should return true for negative entity IDs", () => { - expect(Entity.isEphemeral(-1)).toBe(true); - expect(Entity.isEphemeral(-100)).toBe(true); + expect(Entity.isNonPersistent(-1)).toBe(true); + expect(Entity.isNonPersistent(-100)).toBe(true); }); it("should return false for non-negative entity IDs", () => { - expect(Entity.isEphemeral(0)).toBe(false); + expect(Entity.isNonPersistent(0)).toBe(false); + expect(Entity.isNonPersistent(1)).toBe(false); + expect(Entity.isNonPersistent(100)).toBe(false); + }); +}); + +describe("Entity.isPersistent", () => { + it("should return true for non-negative entity IDs", () => { + expect(Entity.isPersistent(0)).toBe(true); + expect(Entity.isPersistent(1)).toBe(true); + expect(Entity.isPersistent(100)).toBe(true); + }); + + it("should return false for negative entity IDs", () => { + expect(Entity.isPersistent(-1)).toBe(false); + expect(Entity.isPersistent(-100)).toBe(false); + }); +}); + +describe("Entity.isEphemeral (deprecated)", () => { + it("should behave identically to isNonPersistent", () => { + expect(Entity.isEphemeral(-1)).toBe(true); expect(Entity.isEphemeral(1)).toBe(false); - expect(Entity.isEphemeral(100)).toBe(false); }); }); diff --git a/packages/data/src/ecs/entity/is-ephemeral.ts b/packages/data/src/ecs/entity/is-ephemeral.ts index 8a8cc399..a61f72ea 100644 --- a/packages/data/src/ecs/entity/is-ephemeral.ts +++ b/packages/data/src/ecs/entity/is-ephemeral.ts @@ -1,4 +1,8 @@ // © 2026 Adobe. MIT License. See /LICENSE for details. import type { Entity } from "./entity.js"; -export const isEphemeral = (entity: Entity): boolean => entity < 0; +export const isNonPersistent = (entity: Entity): boolean => entity < 0; +export const isPersistent = (entity: Entity): boolean => entity >= 0; + +/** @deprecated Use `isNonPersistent` instead */ +export const isEphemeral = isNonPersistent; diff --git a/packages/data/src/ecs/entity/public.ts b/packages/data/src/ecs/entity/public.ts index d323b3fc..789359e3 100644 --- a/packages/data/src/ecs/entity/public.ts +++ b/packages/data/src/ecs/entity/public.ts @@ -1,3 +1,3 @@ // © 2026 Adobe. MIT License. See /LICENSE for details. export { schema } from "./schema.js"; -export { isEphemeral } from "./is-ephemeral.js"; +export { isNonPersistent, isPersistent, isEphemeral } from "./is-ephemeral.js"; diff --git a/packages/data/src/ecs/optional-components.ts b/packages/data/src/ecs/optional-components.ts index da8feca9..b4ae9dda 100644 --- a/packages/data/src/ecs/optional-components.ts +++ b/packages/data/src/ecs/optional-components.ts @@ -2,4 +2,8 @@ import { Entity } from "./entity/entity.js"; // © 2026 Adobe. MIT License. See /LICENSE for details. -export type OptionalComponents = { ephemeral: true }; +export type OptionalComponents = { + nonPersistent: true; + /** @deprecated Use `"nonPersistent"` instead */ + ephemeral?: true; +}; diff --git a/packages/data/src/ecs/persistence-service/create-storage-persistence-service.ts b/packages/data/src/ecs/persistence-service/create-storage-persistence-service.ts index 1e440a8e..163ccdce 100644 --- a/packages/data/src/ecs/persistence-service/create-storage-persistence-service.ts +++ b/packages/data/src/ecs/persistence-service/create-storage-persistence-service.ts @@ -31,7 +31,7 @@ export const createStoragePersistenceService = async (options: { if (autoSave) { const debouncedSave = debounce(() => service.save(), 300); database.observe.transactions(t => { - if (!t.transient && !t.ephemeral) debouncedSave(); + if (!t.intermediate && t.persistent) debouncedSave(); }); } return service; diff --git a/packages/data/src/ecs/store/core/create-core.ts b/packages/data/src/ecs/store/core/create-core.ts index 0acb9cca..cb3ba9b8 100644 --- a/packages/data/src/ecs/store/core/create-core.ts +++ b/packages/data/src/ecs/store/core/create-core.ts @@ -18,12 +18,13 @@ export function createCore(newComponentSchemas: NC) const componentSchemas: { readonly [K in StringKeyof]: Schema } = { id: Entity.schema, + nonPersistent: True.schema, ephemeral: True.schema, ...newComponentSchemas }; const persistentLocationTable = createEntityLocationTable(16, false); - const ephemeralLocationTable = createEntityLocationTable(16, true); - const getLocationTable = (entity: Entity) => entity < 0 ? ephemeralLocationTable : persistentLocationTable; + const nonPersistentLocationTable = createEntityLocationTable(16, true); + const getLocationTable = (entity: Entity) => entity < 0 ? nonPersistentLocationTable : persistentLocationTable; const archetypes = [] as unknown as Archetype[] & { readonly [x: string]: Archetype }; const queryArchetypes = < @@ -56,13 +57,16 @@ export function createCore(newComponentSchemas: NC) const id = archetypes.length; const archetypeComponentSchemas: { [K in CC]: Schema } = {} as { [K in CC]: Schema }; let hasId = false; - let ephemeral = false; + let isNonPersistent = false; for (const comp of componentNames as Iterable) { if (comp === "id") { hasId = true; } - if (comp === "ephemeral") { - ephemeral = true; + if (comp === "nonPersistent") { + isNonPersistent = true; + } else if (comp === "ephemeral") { + console.warn('"ephemeral" component is deprecated, use "nonPersistent"'); + isNonPersistent = true; } archetypeComponentSchemas[comp] = componentSchemas[comp]; } @@ -72,14 +76,14 @@ export function createCore(newComponentSchemas: NC) const archetype = ARCHETYPE.createArchetype( archetypeComponentSchemas as any, id, - ephemeral ? ephemeralLocationTable : persistentLocationTable + isNonPersistent ? nonPersistentLocationTable : persistentLocationTable ); archetypes.push(archetype as unknown as Archetype); return archetype as unknown as Archetype; } const locateInternal = (entity: Entity) => { - return (entity < 0 ? ephemeralLocationTable : persistentLocationTable).locate(entity); + return (entity < 0 ? nonPersistentLocationTable : persistentLocationTable).locate(entity); } const readEntity = (entity: Entity, minArchetype?: ReadonlyArchetype | Archetype): any => { @@ -112,8 +116,8 @@ export function createCore(newComponentSchemas: NC) if (currentLocation === null) { throw new Error(`Entity not found ${entity}`); } - if ("ephemeral" in components) { - throw new Error("Cannot update ephemeral component"); + if ("nonPersistent" in components || "ephemeral" in components) { + throw new Error("Cannot update nonPersistent component"); } const currentArchetype = archetypes[currentLocation.archetype]; let newArchetype = currentArchetype; @@ -177,7 +181,7 @@ export function createCore(newComponentSchemas: NC) const resetCore = () => { persistentLocationTable.reset(); - ephemeralLocationTable.reset(); + nonPersistentLocationTable.reset(); for (const archetype of archetypes) { archetype.rowCount = 0; } @@ -220,13 +224,15 @@ export function createCore(newComponentSchemas: NC) type TestType = ReturnType> type CheckTestType = Assert>> type TestTypeComponents = TestType["componentSchemas"] type CheckComponents = Assert { const resourceId = name as StringKeyof; - const isEphemeral = resourceSchema.ephemeral; - const componentNames: StringKeyof[] = isEphemeral - ? ["id" as StringKeyof, resourceId, "ephemeral" as StringKeyof] + const isNonPersistent = resourceSchema.nonPersistent ?? resourceSchema.ephemeral; + const componentNames: StringKeyof[] = isNonPersistent + ? ["id" as StringKeyof, resourceId, "nonPersistent" as StringKeyof] : ["id" as StringKeyof, resourceId]; const archetype = core.ensureArchetype(componentNames); if (archetype.rowCount === 0) { - const insertValues = isEphemeral - ? { [resourceId]: resourceSchema.default, ephemeral: true } + const insertValues = isNonPersistent + ? { [resourceId]: resourceSchema.default, nonPersistent: true } : { [resourceId]: resourceSchema.default }; // Resource singleton inserts bypass index pre-check because // resources are not typically indexed by their schema name and diff --git a/packages/data/src/ecs/undo-redo-service/create-undo-redo-service.ts b/packages/data/src/ecs/undo-redo-service/create-undo-redo-service.ts index 9997045f..c953f29b 100644 --- a/packages/data/src/ecs/undo-redo-service/create-undo-redo-service.ts +++ b/packages/data/src/ecs/undo-redo-service/create-undo-redo-service.ts @@ -12,7 +12,7 @@ export const createUndoRedoService = (database: Database[]>([]); const [observeStackIndex, setObserveStackIndex] = Observe.createState(0); database.observe.transactions(t => { - if (t.undoable && !t.transient) { + if (t.undoable && !t.intermediate) { // Check if we should coalesce with the previous transaction const shouldCoalesce = stackIndex > 0 && shouldCoalesceTransactions(undoStack[stackIndex - 1], t); diff --git a/packages/data/src/schema/schema.ts b/packages/data/src/schema/schema.ts index 0967587f..d263587e 100644 --- a/packages/data/src/schema/schema.ts +++ b/packages/data/src/schema/schema.ts @@ -25,6 +25,8 @@ export interface Schema { title?: string; description?: string; conditionals?: readonly Conditional[]; + nonPersistent?: boolean; + /** @deprecated Use `nonPersistent` instead */ ephemeral?: boolean; mutable?: boolean; // defaults to false default?: any; diff --git a/packages/data/src/typed-buffer/register-typed-buffer-codecs.ts b/packages/data/src/typed-buffer/register-typed-buffer-codecs.ts index 93605ed0..bcb1f104 100644 --- a/packages/data/src/typed-buffer/register-typed-buffer-codecs.ts +++ b/packages/data/src/typed-buffer/register-typed-buffer-codecs.ts @@ -19,7 +19,7 @@ export function registerTypedBufferCodecs() { serialize: (data: TypedBuffer) => { const { type, schema, capacity } = data; try { - if (type === "const" || schema.ephemeral) { + if (type === "const" || (schema.nonPersistent ?? schema.ephemeral)) { return { json: { type, schema, capacity } }; } else if (type === "array") { @@ -50,7 +50,7 @@ export function registerTypedBufferCodecs() { const buffer = isEnum ? createEnumBuffer(schema, capacity) : createArrayBuffer(schema, capacity); - if (schema.ephemeral) { + if ((schema.nonPersistent ?? schema.ephemeral)) { if (schema.default !== undefined && schema.default !== 0) { for (let i = 0; i < capacity; i++) { buffer.set(i, schema.default); @@ -66,7 +66,7 @@ export function registerTypedBufferCodecs() { } else if (type === "enum") { const buffer = createEnumBuffer(schema, capacity); - if (!schema.ephemeral) { + if (!(schema.nonPersistent ?? schema.ephemeral)) { if (binary[0]) { copyViewBytes(binary[0], buffer.getTypedArray()); } else if (array) { @@ -79,7 +79,7 @@ export function registerTypedBufferCodecs() { } else if (type === "number" || type === "struct") { const buffer = type === "number" ? createNumberBuffer(schema, capacity) : createStructBuffer(schema, capacity); - if (schema.ephemeral) { + if ((schema.nonPersistent ?? schema.ephemeral)) { if (schema.default !== undefined && schema.default !== 0) { for (let i = 0; i < capacity; i++) { buffer.set(i, schema.default);