@adobe/ecs-persistence 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["header"],"sources":["../src/persist.ts","../src/program/program.ts","../src/values/values.ts","../src/format/format.ts","../src/program/contents.ts","../src/decode.ts","../src/encode.ts","../src/selection/selection.ts","../src/snapshot.ts","../src/spawn.ts"],"sourcesContent":["// © 2026 Adobe. MIT License. See /LICENSE for details.\nimport type { Component, Components } from \"@adobe/ecs\";\n\n/**\n * The trait marking a component or resource to persist, its value the stable name saved data names it by: unlike its\n * key's id, the name stays the same across versions and moves of its module. Not on a derived component, which its\n * rule derives again once its entity is loaded.\n */\nexport const persist = Symbol();\n\ndeclare module \"@adobe/ecs\" {\n interface Traits<N> {\n readonly [persist]: N extends Component.Key ? Components[N] extends { readonly derived: true } ? never : string : string;\n }\n}\n","// © 2026 Adobe. MIT License. See /LICENSE for details.\nimport { Component, Resource, type Schema } from \"@adobe/ecs\";\nimport { persist } from \"../persist.js\";\n\n/** A key persisted, with its name and schema. */\nexport type Persisted<K extends symbol = symbol> = { readonly name: string; readonly key: K; readonly schema: Schema };\n\n/** What the program persists: its components and resources by name. */\nexport type Program = {\n readonly components: ReadonlyMap<string, Persisted<Component.Key>>;\n readonly resources: ReadonlyMap<string, Persisted<Resource.Key>>;\n};\n\n/** The keys of `declarations` with a `persist` trait by name, or why one can't persist: it isn't data, or a name is taken. */\nconst namedIn = <K extends symbol>(declarations: ReadonlyMap<K, Component.Declared>, kind: string): ReadonlyMap<string, Persisted<K>> | string => {\n const named = new Map<string, Persisted<K>>();\n for (const [key, declaration] of declarations) {\n const name: unknown = declaration[persist];\n if (typeof name !== \"string\") continue;\n if (declaration.type === undefined) return `The ${kind} persisted as \"${name}\" isn't data: it has no schema.`;\n if (named.has(name)) return `Two ${kind}s are persisted as \"${name}\".`;\n named.set(name, { name, key, schema: declaration.type });\n }\n return named;\n};\n\n/** What the program persists, as its modules registered so far declare it, or why it can't. */\nexport const programOf = (): Program | string => {\n const components = namedIn(Component.declarations(), \"component\");\n if (typeof components === \"string\") return components;\n const resources = namedIn(Resource.declarations(), \"resource\");\n if (typeof resources === \"string\") return resources;\n return { components, resources };\n};\n","// © 2026 Adobe. MIT License. See /LICENSE for details.\nimport type { Schema } from \"@adobe/ecs\";\n\n// Values of components stored as values, strings and other data that isn't a fixed layout, as JSON their schema reads\n// back: a map as its entries, a set as its items, numbers JSON lacks as text, and a union's value as its member's\n// index and its JSON.\n\n/** A number JSON can't hold, as text: `NaN`, `Infinity`, `-Infinity` or `-0`. */\nconst numberToJson = (value: number): number | string =>\n Number.isNaN(value) ? \"NaN\" : value === Infinity ? \"Infinity\" : value === -Infinity ? \"-Infinity\" : Object.is(value, -0) ? \"-0\" : value;\n\nconst numberFromJson = (json: unknown): unknown => {\n if (typeof json !== \"string\") return json;\n switch (json) {\n case \"NaN\": return NaN;\n case \"Infinity\": return Infinity;\n case \"-Infinity\": return -Infinity;\n case \"-0\": return -0;\n default: return json;\n }\n};\n\n/** Whether a value is of the JSON kind a schema's values are, to pick a union's member. */\nconst fits = (schema: Schema, value: unknown): boolean => {\n if (\"const\" in schema) return Object.is(schema.const, value) || JSON.stringify(schema.const) === JSON.stringify(value);\n if (\"anyOf\" in schema) return schema.anyOf.some(member => fits(member, value));\n if (\"allOf\" in schema) return schema.allOf.every(member => fits(member, value));\n if (!(\"type\" in schema)) return true;\n switch (schema.type) {\n case \"number\": case \"integer\": return typeof value === \"number\";\n case \"string\": return typeof value === \"string\";\n case \"boolean\": return typeof value === \"boolean\";\n case \"null\": return value === null;\n case \"array\": return Array.isArray(value);\n case \"set\": return value instanceof Set;\n case \"map\": return value instanceof Map;\n case \"object\": return typeof value === \"object\" && value !== null && !Array.isArray(value) && !(value instanceof Set) && !(value instanceof Map);\n default: return true;\n }\n};\n\n/** A value of `schema` as JSON. */\nexport const toJson = (schema: Schema, value: unknown): unknown => {\n if (\"anyOf\" in schema) {\n const index = schema.anyOf.findIndex(candidate => fits(candidate, value));\n const member = schema.anyOf[index];\n return member === undefined ? [-1, value] : [index, toJson(member, value)];\n }\n if (typeof value === \"number\") return numberToJson(value);\n if (!(\"type\" in schema)) return value;\n switch (schema.type) {\n case \"array\": return Array.isArray(value) ? value.map(item => toJson(schema.items, item)) : value;\n case \"set\": return value instanceof Set ? Array.from(value, item => toJson(schema.items, item)) : value;\n case \"map\": return value instanceof Map ? Array.from(value, ([key, item]) => [toJson(schema.keys, key), toJson(schema.values, item)]) : value;\n case \"object\": {\n if (typeof value !== \"object\" || value === null) return value;\n const json: { [name: string]: unknown } = {};\n for (const name of Object.keys(value)) {\n const field = schema.properties[name];\n const item: unknown = Reflect.get(value, name);\n if (item !== undefined) json[name] = field === undefined ? item : toJson(field, item);\n }\n return json;\n }\n default: return value;\n }\n};\n\n/** A value of `schema` read back from its JSON. */\nexport const fromJson = (schema: Schema, json: unknown): unknown => {\n if (\"anyOf\" in schema) {\n if (!Array.isArray(json) || typeof json[0] !== \"number\") return json;\n const member = schema.anyOf[json[0]];\n return member === undefined ? json[1] : fromJson(member, json[1]);\n }\n if (\"const\" in schema) return schema.const;\n if (!(\"type\" in schema)) return json;\n switch (schema.type) {\n case \"number\": case \"integer\": return numberFromJson(json);\n case \"array\": return Array.isArray(json) ? json.map(item => fromJson(schema.items, item)) : json;\n case \"set\": return Array.isArray(json) ? new Set(json.map(item => fromJson(schema.items, item))) : json;\n case \"map\": return Array.isArray(json)\n ? new Map(json.map(entry => Array.isArray(entry) ? [fromJson(schema.keys, entry[0]), fromJson(schema.values, entry[1])] : [entry, undefined]))\n : json;\n case \"object\": {\n if (typeof json !== \"object\" || json === null || Array.isArray(json)) return json;\n const value: { [name: string]: unknown } = {};\n for (const name of Object.keys(json)) {\n const field = schema.properties[name];\n const item: unknown = Reflect.get(json, name);\n value[name] = field === undefined ? item : fromJson(field, item);\n }\n return value;\n }\n default: return json;\n }\n};\n","// © 2026 Adobe. MIT License. See /LICENSE for details.\nimport type { Component, Resource, Schema, World } from \"@adobe/ecs\";\nimport type { Failure } from \"../failure.js\";\nimport type { Contents } from \"../program/contents.js\";\nimport { programOf, type Persisted } from \"../program/program.js\";\nimport { fromJson, toJson } from \"../values/values.js\";\n\n/**\n * The binary form of a snapshot, little-endian:\n * - 16 bytes: `ECSP`, the format's version as a u32, the header's length in bytes as a u32, and 4 bytes of zeros;\n * - the header, JSON text padded with spaces to a multiple of 8 bytes: each component and resource by name with its\n * schema, and each table's count and where its entities and columns are;\n * - blocks, each starting at a multiple of 8 bytes: a table's entities as f64s, a column's bytes as its components'\n * layout holds them, and a column of values as JSON text.\n * A loader views the blocks of bytes where they are, in the buffer it's given.\n */\n\nconst magic = [0x45, 0x43, 0x53, 0x50];\nconst version = 1;\nconst prefix = 16;\n\n/** Where a column's data is. */\ntype ColumnJson =\n | { readonly kind: \"bytes\"; readonly offset: number; readonly length: number; readonly stride: number }\n | { readonly kind: \"values\"; readonly offset: number; readonly length: number }\n | { readonly kind: \"value\"; readonly value: unknown };\n\ntype Header = {\n readonly components: readonly { readonly name: string; readonly schema: Schema }[];\n readonly resources: readonly { readonly name: string; readonly schema: Schema; readonly value: unknown }[];\n readonly tables: readonly { readonly count: number; readonly entities: number; readonly columns: readonly ({ readonly component: number } & ColumnJson)[] }[];\n};\n\nconst aligned = (offset: number): number => Math.ceil(offset / 8) * 8;\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\n/** A snapshot's contents as bytes. */\nexport const encodeContents = (snapshot: Contents): Uint8Array => {\n const blocks: Uint8Array[] = [];\n let offset = 0;\n /** Places a block, returning its offset from the first block. */\n const place = (bytes: Uint8Array): number => {\n const at = offset;\n blocks.push(bytes);\n offset = aligned(offset + bytes.byteLength);\n return at;\n };\n const indexOf = new Map(snapshot.components.map((persisted, index) => [persisted.key, index]));\n const tables = snapshot.tables.map(table => {\n const entities = place(new Uint8Array(table.entities.buffer, table.entities.byteOffset, table.entities.byteLength));\n const columns = snapshot.components.flatMap((persisted): ({ readonly component: number } & ColumnJson)[] => {\n const key: symbol = persisted.key;\n const column: World.Table.Column | undefined = Reflect.get(table.columns, key);\n const component = indexOf.get(persisted.key) ?? -1;\n if (column === undefined) return [];\n if (column.kind === \"bytes\") return [{ component, kind: \"bytes\", offset: place(column.bytes), length: column.bytes.byteLength, stride: column.stride }];\n if (column.kind === \"value\") return [{ component, kind: \"value\", value: toJson(persisted.schema, column.value) }];\n const text = encoder.encode(JSON.stringify(column.values.map(value => toJson(persisted.schema, value))));\n return [{ component, kind: \"values\", offset: place(text), length: text.byteLength }];\n });\n return { count: table.count, entities, columns };\n });\n const header: Header = {\n components: snapshot.components.map(({ name, schema }) => ({ name, schema })),\n resources: snapshot.resources.map(({ persisted, value }) => ({ name: persisted.name, schema: persisted.schema, value: toJson(persisted.schema, value) })),\n tables,\n };\n const text = encoder.encode(JSON.stringify(header));\n const headerLength = aligned(text.byteLength);\n const bytes = new Uint8Array(prefix + headerLength + offset);\n bytes.set(magic, 0);\n const view = new DataView(bytes.buffer);\n view.setUint32(4, version, true);\n view.setUint32(8, headerLength, true);\n bytes.set(text, prefix);\n bytes.fill(0x20, prefix + text.byteLength, prefix + headerLength);\n let at = prefix + headerLength;\n for (const block of blocks) {\n bytes.set(block, at);\n at = aligned(at + block.byteLength);\n }\n return bytes;\n};\n\nconst isObject = (value: unknown): value is object => typeof value === \"object\" && value !== null;\n\n/** The header's JSON, checked enough to read it without casts, or null. */\nconst headerOf = (json: unknown): Header | null => {\n if (!isObject(json)) return null;\n const components: unknown = Reflect.get(json, \"components\");\n const resources: unknown = Reflect.get(json, \"resources\");\n const tables: unknown = Reflect.get(json, \"tables\");\n if (!Array.isArray(components) || !Array.isArray(resources) || !Array.isArray(tables)) return null;\n const named = (item: unknown): boolean => isObject(item) && typeof Reflect.get(item, \"name\") === \"string\" && isObject(Reflect.get(item, \"schema\"));\n if (!components.every(named) || !resources.every(named)) return null;\n const isColumn = (item: unknown): boolean => {\n if (!isObject(item) || typeof Reflect.get(item, \"component\") !== \"number\") return false;\n const kind: unknown = Reflect.get(item, \"kind\");\n if (kind === \"value\") return true;\n const numbers = kind === \"bytes\" ? [\"offset\", \"length\", \"stride\"] : kind === \"values\" ? [\"offset\", \"length\"] : null;\n return numbers !== null && numbers.every(name => typeof Reflect.get(item, name) === \"number\");\n };\n const isTable = (item: unknown): boolean => {\n if (!isObject(item) || typeof Reflect.get(item, \"count\") !== \"number\" || typeof Reflect.get(item, \"entities\") !== \"number\") return false;\n const columns: unknown = Reflect.get(item, \"columns\");\n return Array.isArray(columns) && columns.every(isColumn);\n };\n if (!tables.every(isTable)) return null;\n // Checked above: each item has the fields the header's type names, of their types.\n const header = json as Header;\n return header;\n};\n\n/** Whether two schemas are the same, by their JSON in their properties' order, which places a struct's fields. */\nconst sameSchema = (a: Schema, b: Schema): boolean => JSON.stringify(a) === JSON.stringify(b);\n\n/**\n * The snapshot contents `bytes` holds, its components and resources by name as this program persists them, its blocks of\n * bytes viewed where they are; or why it can't be read: it isn't one, or names what this program doesn't persist, or\n * persists with another schema.\n */\nexport const decodeContents = (bytes: Uint8Array): Contents | Failure => {\n if (bytes.byteLength < prefix || magic.some((byte, at) => bytes[at] !== byte)) return { failure: \"These bytes aren't a snapshot.\" };\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n const found = view.getUint32(4, true);\n if (found !== version) return { failure: `This snapshot's format is version ${found}; this package reads version ${version}.` };\n const headerLength = view.getUint32(8, true);\n let json: unknown;\n try {\n json = JSON.parse(decoder.decode(bytes.subarray(prefix, prefix + headerLength)));\n }\n catch {\n return { failure: \"This snapshot's header isn't JSON.\" };\n }\n const header = headerOf(json);\n if (header === null) return { failure: \"This snapshot's header isn't one this package writes.\" };\n const program = programOf();\n if (typeof program === \"string\") return { failure: program };\n const components: Persisted<Component.Key>[] = [];\n for (const { name, schema } of header.components) {\n const persisted = program.components.get(name);\n if (persisted === undefined) return { failure: `This snapshot holds component \"${name}\", which this program doesn't persist.` };\n if (!sameSchema(persisted.schema, schema)) return { failure: `Component \"${name}\" has another schema here than in this snapshot.` };\n components.push(persisted);\n }\n const resources: { persisted: Persisted<Resource.Key>; value: unknown }[] = [];\n for (const { name, schema, value } of header.resources) {\n const persisted = program.resources.get(name);\n if (persisted === undefined) return { failure: `This snapshot holds resource \"${name}\", which this program doesn't persist.` };\n if (!sameSchema(persisted.schema, schema)) return { failure: `Resource \"${name}\" has another schema here than in this snapshot.` };\n resources.push({ persisted, value: fromJson(schema, value) });\n }\n // Blocks are 8-byte aligned in the buffer the format was written to; one given at another offset is copied once.\n const blocks = (bytes.byteOffset + prefix + headerLength) % 8 === 0 ? bytes : bytes.slice();\n const base = blocks.byteOffset + prefix + headerLength;\n const end = blocks.byteOffset + blocks.byteLength;\n const inBounds = (offset: number, length: number): boolean => offset >= 0 && length >= 0 && base + offset + length <= end;\n const tables: World.Table[] = [];\n for (const table of header.tables) {\n if (!inBounds(table.entities, table.count * 8)) return { failure: \"This snapshot's blocks are cut short.\" };\n const columns: { [key: symbol]: World.Table.Column } = {};\n for (const column of table.columns) {\n const persisted = components[column.component];\n if (persisted === undefined) return { failure: \"This snapshot names a component it doesn't hold.\" };\n if (column.kind === \"value\") columns[persisted.key] = { kind: \"value\", value: fromJson(persisted.schema, column.value) };\n else if (!inBounds(column.offset, column.length)) return { failure: \"This snapshot's blocks are cut short.\" };\n else if (column.kind === \"bytes\") columns[persisted.key] = { kind: \"bytes\", bytes: new Uint8Array(blocks.buffer, base + column.offset, column.length), stride: column.stride };\n else {\n let values: unknown;\n try {\n values = JSON.parse(decoder.decode(new Uint8Array(blocks.buffer, base + column.offset, column.length)));\n }\n catch {\n return { failure: `Component \"${persisted.name}\"'s values in this snapshot aren't JSON.` };\n }\n if (!Array.isArray(values)) return { failure: `Component \"${persisted.name}\"'s values in this snapshot aren't a list.` };\n columns[persisted.key] = { kind: \"values\", values: values.map(value => fromJson(persisted.schema, value)) };\n }\n }\n tables.push({ count: table.count, entities: new Float64Array(blocks.buffer, base + table.entities, table.count), columns });\n }\n return { components, tables, resources };\n};\n","// © 2026 Adobe. MIT License. See /LICENSE for details.\nimport type { Component, Resource, World } from \"@adobe/ecs\";\nimport type { Persisted } from \"./program.js\";\n\n/** Where a snapshot holds what it holds, a key only this package uses. */\nexport const contents: unique symbol = Symbol(\"snapshot contents\");\n\n/**\n * The persisted part of some of a world's entities: of each archetype, its entities and a column of each persisted\n * component it holds, keyed by this program's keys; and the values of persisted resources.\n */\nexport type Contents = {\n /** Each persisted component a table of it may hold. */\n readonly components: readonly Persisted<Component.Key>[];\n readonly tables: readonly World.Table[];\n readonly resources: readonly { readonly persisted: Persisted<Resource.Key>; readonly value: unknown }[];\n};\n","// © 2026 Adobe. MIT License. See /LICENSE for details.\nimport type { Failure } from \"./failure.js\";\nimport { decodeContents } from \"./format/format.js\";\nimport { contents } from \"./program/contents.js\";\nimport type { Snapshot } from \"./snapshot.js\";\n\n/**\n * The snapshot `bytes` holds, its columns of bytes viewed where they are in `bytes`, copied once only when `bytes`\n * doesn't start at a multiple of 8; or why it can't be read: it isn't one, or names a component or resource this\n * program doesn't persist, or persists with another schema.\n */\nexport const decode = (bytes: Uint8Array): Snapshot | Failure => {\n const decoded = decodeContents(bytes);\n return \"failure\" in decoded ? decoded : { [contents]: decoded };\n};\n","// © 2026 Adobe. MIT License. See /LICENSE for details.\nimport { encodeContents } from \"./format/format.js\";\nimport { contents } from \"./program/contents.js\";\nimport type { Snapshot } from \"./snapshot.js\";\n\n/**\n * A snapshot as bytes, little-endian: a header naming each persisted component and resource with its schema, then\n * 8-byte-aligned blocks of each table's entities and columns, its bytes columns copied whole.\n */\nexport const encode = (snapshot: Snapshot): Uint8Array => encodeContents(snapshot[contents]);\n","// © 2026 Adobe. MIT License. See /LICENSE for details.\nimport { Component, System, type Entity, type Resource, type World } from \"@adobe/ecs\";\nimport type { Failure } from \"../failure.js\";\nimport type { Contents } from \"../program/contents.js\";\nimport type { Program } from \"../program/program.js\";\n\n/**\n * Which entities a snapshot takes: those matching `with` and `not`, every one holding a persisted component by\n * default; or `root` and every entity it owns, directly or through others, by persisted owner references.\n */\nexport type Selection =\n | { readonly with?: readonly Component.Key[]; readonly not?: readonly Component.Key[]; readonly resources?: boolean }\n | { readonly root: Entity };\n\n/** Which tables a snapshot reads: those of its persisted components, matching `with` and `not`, and its resources. */\ntype Reading = {\n readonly components: readonly Component.Key[];\n readonly with?: readonly Component.Key[];\n readonly not?: readonly Component.Key[];\n readonly resources?: readonly Resource.Key[];\n};\n\n/** A readonly system reading the tables `declaration` names. */\nconst tablesSystemOf = (declaration: Reading) =>\n System.create({ readonly: true, tables: declaration }, ({ tables }) => tables);\n\n/** The rows of `table` whose entities `keep` takes, as a table of their own. */\nconst rowsOf = (table: World.Table, keep: (entity: number) => boolean): World.Table | null => {\n const rows: number[] = [];\n for (let row = 0; row < table.count; row++) if (keep(table.entities[row])) rows.push(row);\n if (rows.length === table.count) return table;\n if (rows.length === 0) return null;\n const columns: { [key: symbol]: World.Table.Column } = {};\n for (const key of Object.getOwnPropertySymbols(table.columns)) {\n const column: World.Table.Column | undefined = Reflect.get(table.columns, key);\n if (column === undefined) continue;\n if (column.kind === \"value\") columns[key] = column;\n else if (column.kind === \"values\") columns[key] = { kind: \"values\", values: rows.map(row => column.values[row]) };\n else {\n const { stride } = column;\n const bytes = new Uint8Array(rows.length * stride);\n rows.forEach((row, at) => bytes.set(column.bytes.subarray(row * stride, (row + 1) * stride), at * stride));\n columns[key] = { kind: \"bytes\", bytes, stride };\n }\n }\n return { count: rows.length, entities: Float64Array.from(rows, row => table.entities[row]), columns };\n};\n\n/** The entities `root` owns, itself included, through the owner references among `owners` in the tables. */\nconst subtreeOf = (tables: readonly World.Table[], owners: readonly Component.Key[], root: Entity): ReadonlySet<number> => {\n const children = new Map<number, number[]>();\n for (const table of tables) {\n for (const reference of owners) {\n const key: symbol = reference;\n const column: World.Table.Column | undefined = Reflect.get(table.columns, key);\n if (column?.kind !== \"bytes\") continue;\n const view = new DataView(column.bytes.buffer, column.bytes.byteOffset, column.bytes.byteLength);\n for (let row = 0; row < table.count; row++) {\n const owner = view.getFloat64(row * column.stride, true);\n const list = children.get(owner);\n if (list === undefined) children.set(owner, [table.entities[row]]);\n else list.push(table.entities[row]);\n }\n }\n }\n const taken = new Set<number>([root]);\n const waiting: number[] = [root];\n for (let next = waiting.pop(); next !== undefined; next = waiting.pop()) {\n for (const child of children.get(next) ?? []) {\n if (taken.has(child)) continue;\n taken.add(child);\n waiting.push(child);\n }\n }\n return taken;\n};\n\n/** The persisted part of the entities `selection` takes from `world`, on its thread, or why it can't be read. */\nexport const select = async (world: World, program: Program, selection: Selection): Promise<Contents | Failure> => {\n const components = [...program.components.values()];\n const keys = components.map(persisted => persisted.key);\n const subtree = \"root\" in selection;\n const resources = subtree || selection.resources === false ? [] : [...program.resources.values()];\n const declaration: Reading = subtree\n ? { components: keys }\n : { components: keys, with: selection.with ?? [], not: selection.not ?? [], resources: resources.map(persisted => persisted.key) };\n const read = await world.run(tablesSystemOf(declaration)).catch((error: unknown) => ({ failure: error instanceof Error ? error.message : String(error) }));\n if (\"failure\" in read) return read;\n let tables = read.archetypes;\n if (subtree) {\n const owners = keys.filter(key => Component.declarations().get(key)?.reference === \"owner\");\n const taken = subtreeOf(tables, owners, selection.root);\n tables = tables.flatMap(table => rowsOf(table, entity => taken.has(entity)) ?? []);\n }\n return { components, tables, resources: resources.map(persisted => ({ persisted, value: read.resources[persisted.key] })) };\n};\n","// © 2026 Adobe. MIT License. See /LICENSE for details.\nimport type { World } from \"@adobe/ecs\";\nimport type { Failure } from \"./failure.js\";\nimport { contents, type Contents } from \"./program/contents.js\";\nimport { programOf } from \"./program/program.js\";\nimport { select, type Selection } from \"./selection/selection.js\";\n\n/** The persisted part of some of a world's entities and its persisted resources, which `encode` and `spawn` take. */\nexport type Snapshot = { readonly failure?: undefined; readonly [contents]: Contents };\n\n/**\n * A snapshot of some of `world`'s entities, read on its thread: by default every entity holding a persisted\n * component, and the persisted resources; with `with` and `not`, those a query matches, and the resources unless\n * `resources` is false; with `root`, it and every entity it owns, through persisted owner references, without\n * resources. Fails, as data, when the program's persisted keys can't be saved or the world can't be read.\n */\nexport const snapshot = async (input: { readonly world: World } & Selection): Promise<Snapshot | Failure> => {\n const program = programOf();\n if (typeof program === \"string\") return { failure: program };\n const taken = await select(input.world, program, input);\n return \"failure\" in taken ? taken : { [contents]: taken };\n};\n","// © 2026 Adobe. MIT License. See /LICENSE for details.\nimport { System, type Entity, type World } from \"@adobe/ecs\";\nimport type { Failure } from \"./failure.js\";\nimport { contents } from \"./program/contents.js\";\nimport type { Snapshot } from \"./snapshot.js\";\n\n/**\n * What `spawn` made: how many of the snapshot's entities it didn't spawn, those owned by an entity outside it; and,\n * unless asked not to, `entities`, each of the snapshot's entities to the one spawned for it, or null for one it\n * didn't spawn.\n */\nexport type Spawned<Entities extends boolean = true> = { readonly failure?: undefined; readonly dropped: number }\n & (Entities extends false ? {} : { readonly entities: ReadonlyMap<Entity, Entity | null> });\n\nconst isTables = (value: unknown): value is readonly World.Table[] => Array.isArray(value) && value.every(table =>\n typeof table === \"object\" && table !== null && typeof Reflect.get(table, \"count\") === \"number\" && Reflect.get(table, \"entities\") instanceof Float64Array\n && typeof Reflect.get(table, \"columns\") === \"object\");\n\nconst isKeyed = (value: unknown): value is { readonly [key: symbol]: unknown } => typeof value === \"object\" && value !== null;\n\n/** Spawns its input's tables and sets its resources, a snapshot's, rewriting their entities. */\nconst spawnTables = System.create({}, ({ commands }, input: { readonly tables: unknown; readonly resources: unknown }) => {\n if (!isTables(input.tables) || !isKeyed(input.resources)) throw new Error(\"A snapshot holds tables and resources.\");\n return commands.spawnTables(input.tables, input.resources);\n});\n\n/**\n * Spawns an entity in `world` for each of the snapshot's, with its persisted components, and sets the persisted\n * resources, on the world's thread, as the world's references would leave them had the entities outside the snapshot\n * despawned: each entity value naming one of the snapshot's names the one spawned for it; an entity owned, directly\n * or through others, by an entity outside the snapshot isn't spawned, which the result counts; a weak reference to\n * one is left out; and any other entity value naming one is `Entity.none`. With `entities: false`, the result leaves\n * out its map of entities, which costs about 85 ms and 50 MB at a million. Fails, as data, for a column or\n * resource that doesn't fit, spawning nothing.\n */\nexport function spawn(input: { readonly world: World; readonly snapshot: Snapshot; readonly entities: false }): Promise<Spawned<false> | Failure>;\nexport function spawn(input: { readonly world: World; readonly snapshot: Snapshot; readonly entities?: true }): Promise<Spawned | Failure>;\nexport async function spawn(input: { readonly world: World; readonly snapshot: Snapshot; readonly entities?: boolean }): Promise<Spawned<boolean> | Failure> {\n const { tables, resources } = input.snapshot[contents];\n const values: { [key: symbol]: unknown } = {};\n for (const { persisted, value } of resources) values[persisted.key] = value;\n const spawned = await input.world.run(spawnTables, { tables, resources: values }).catch((error: unknown) => ({ failure: error instanceof Error ? error.message : String(error) }));\n if (\"failure\" in spawned) return spawned;\n if (input.entities === false) return { dropped: spawned.dropped };\n return { dropped: spawned.dropped, entities: entitiesOf(tables, spawned.entities) };\n}\n\n/** Each of the tables' entities to the one spawned for it, or null for one that wasn't. */\nconst entitiesOf = (tables: readonly World.Table[], spawned: readonly Float64Array[]): ReadonlyMap<Entity, Entity | null> => {\n const entities = new Map<Entity, Entity | null>();\n tables.forEach((table, t) => {\n const made = spawned[t];\n for (let row = 0; row < table.count; row++) {\n // A table's entities and `spawnTables`' are entities, `Entity.none`, 0, for a row it didn't spawn.\n const entity = made[row] as Entity;\n entities.set(table.entities[row] as Entity, entity === 0 ? null : entity);\n }\n });\n return entities;\n};\n"],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { Failure } from "./failure.js";
2
+ import type { Snapshot } from "./snapshot.js";
3
+ /**
4
+ * The snapshot `bytes` holds, its columns of bytes viewed where they are in `bytes`, copied once only when `bytes`
5
+ * doesn't start at a multiple of 8; or why it can't be read: it isn't one, or names a component or resource this
6
+ * program doesn't persist, or persists with another schema.
7
+ */
8
+ export declare const decode: (bytes: Uint8Array) => Snapshot | Failure;
@@ -0,0 +1,6 @@
1
+ import type { Snapshot } from "./snapshot.js";
2
+ /**
3
+ * A snapshot as bytes, little-endian: a header naming each persisted component and resource with its schema, then
4
+ * 8-byte-aligned blocks of each table's entities and columns, its bytes columns copied whole.
5
+ */
6
+ export declare const encode: (snapshot: Snapshot) => Uint8Array;
@@ -0,0 +1,4 @@
1
+ /** Why a snapshot, a decoding or a spawn didn't happen. */
2
+ export type Failure = {
3
+ readonly failure: string;
4
+ };
@@ -0,0 +1,10 @@
1
+ import type { Failure } from "../failure.js";
2
+ import type { Contents } from "../program/contents.js";
3
+ /** A snapshot's contents as bytes. */
4
+ export declare const encodeContents: (snapshot: Contents) => Uint8Array;
5
+ /**
6
+ * The snapshot contents `bytes` holds, its components and resources by name as this program persists them, its blocks of
7
+ * bytes viewed where they are; or why it can't be read: it isn't one, or names what this program doesn't persist, or
8
+ * persists with another schema.
9
+ */
10
+ export declare const decodeContents: (bytes: Uint8Array) => Contents | Failure;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Saving, loading and copying the entities of `@adobe/ecs` worlds: snapshots of their persisted components and
3
+ * resources, as binary columns.
4
+ * @module @adobe/ecs-persistence
5
+ */
6
+ export { decode } from "./decode.js";
7
+ export { encode } from "./encode.js";
8
+ export type { Failure } from "./failure.js";
9
+ export { persist } from "./persist.js";
10
+ export { snapshot, type Snapshot } from "./snapshot.js";
11
+ export { spawn, type Spawned } from "./spawn.js";
@@ -0,0 +1,14 @@
1
+ import type { Component, Components } from "@adobe/ecs";
2
+ /**
3
+ * The trait marking a component or resource to persist, its value the stable name saved data names it by: unlike its
4
+ * key's id, the name stays the same across versions and moves of its module. Not on a derived component, which its
5
+ * rule derives again once its entity is loaded.
6
+ */
7
+ export declare const persist: unique symbol;
8
+ declare module "@adobe/ecs" {
9
+ interface Traits<N> {
10
+ readonly [persist]: N extends Component.Key ? Components[N] extends {
11
+ readonly derived: true;
12
+ } ? never : string : string;
13
+ }
14
+ }
@@ -0,0 +1,17 @@
1
+ import type { Component, Resource, World } from "@adobe/ecs";
2
+ import type { Persisted } from "./program.js";
3
+ /** Where a snapshot holds what it holds, a key only this package uses. */
4
+ export declare const contents: unique symbol;
5
+ /**
6
+ * The persisted part of some of a world's entities: of each archetype, its entities and a column of each persisted
7
+ * component it holds, keyed by this program's keys; and the values of persisted resources.
8
+ */
9
+ export type Contents = {
10
+ /** Each persisted component a table of it may hold. */
11
+ readonly components: readonly Persisted<Component.Key>[];
12
+ readonly tables: readonly World.Table[];
13
+ readonly resources: readonly {
14
+ readonly persisted: Persisted<Resource.Key>;
15
+ readonly value: unknown;
16
+ }[];
17
+ };
@@ -0,0 +1,14 @@
1
+ import { Component, Resource, type Schema } from "@adobe/ecs";
2
+ /** A key persisted, with its name and schema. */
3
+ export type Persisted<K extends symbol = symbol> = {
4
+ readonly name: string;
5
+ readonly key: K;
6
+ readonly schema: Schema;
7
+ };
8
+ /** What the program persists: its components and resources by name. */
9
+ export type Program = {
10
+ readonly components: ReadonlyMap<string, Persisted<Component.Key>>;
11
+ readonly resources: ReadonlyMap<string, Persisted<Resource.Key>>;
12
+ };
13
+ /** What the program persists, as its modules registered so far declare it, or why it can't. */
14
+ export declare const programOf: () => Program | string;
@@ -0,0 +1,17 @@
1
+ import { Component, type Entity, type World } from "@adobe/ecs";
2
+ import type { Failure } from "../failure.js";
3
+ import type { Contents } from "../program/contents.js";
4
+ import type { Program } from "../program/program.js";
5
+ /**
6
+ * Which entities a snapshot takes: those matching `with` and `not`, every one holding a persisted component by
7
+ * default; or `root` and every entity it owns, directly or through others, by persisted owner references.
8
+ */
9
+ export type Selection = {
10
+ readonly with?: readonly Component.Key[];
11
+ readonly not?: readonly Component.Key[];
12
+ readonly resources?: boolean;
13
+ } | {
14
+ readonly root: Entity;
15
+ };
16
+ /** The persisted part of the entities `selection` takes from `world`, on its thread, or why it can't be read. */
17
+ export declare const select: (world: World, program: Program, selection: Selection) => Promise<Contents | Failure>;
@@ -0,0 +1,18 @@
1
+ import type { World } from "@adobe/ecs";
2
+ import type { Failure } from "./failure.js";
3
+ import { contents, type Contents } from "./program/contents.js";
4
+ import { type Selection } from "./selection/selection.js";
5
+ /** The persisted part of some of a world's entities and its persisted resources, which `encode` and `spawn` take. */
6
+ export type Snapshot = {
7
+ readonly failure?: undefined;
8
+ readonly [contents]: Contents;
9
+ };
10
+ /**
11
+ * A snapshot of some of `world`'s entities, read on its thread: by default every entity holding a persisted
12
+ * component, and the persisted resources; with `with` and `not`, those a query matches, and the resources unless
13
+ * `resources` is false; with `root`, it and every entity it owns, through persisted owner references, without
14
+ * resources. Fails, as data, when the program's persisted keys can't be saved or the world can't be read.
15
+ */
16
+ export declare const snapshot: (input: {
17
+ readonly world: World;
18
+ } & Selection) => Promise<Snapshot | Failure>;
@@ -0,0 +1,33 @@
1
+ import { type Entity, type World } from "@adobe/ecs";
2
+ import type { Failure } from "./failure.js";
3
+ import type { Snapshot } from "./snapshot.js";
4
+ /**
5
+ * What `spawn` made: how many of the snapshot's entities it didn't spawn, those owned by an entity outside it; and,
6
+ * unless asked not to, `entities`, each of the snapshot's entities to the one spawned for it, or null for one it
7
+ * didn't spawn.
8
+ */
9
+ export type Spawned<Entities extends boolean = true> = {
10
+ readonly failure?: undefined;
11
+ readonly dropped: number;
12
+ } & (Entities extends false ? {} : {
13
+ readonly entities: ReadonlyMap<Entity, Entity | null>;
14
+ });
15
+ /**
16
+ * Spawns an entity in `world` for each of the snapshot's, with its persisted components, and sets the persisted
17
+ * resources, on the world's thread, as the world's references would leave them had the entities outside the snapshot
18
+ * despawned: each entity value naming one of the snapshot's names the one spawned for it; an entity owned, directly
19
+ * or through others, by an entity outside the snapshot isn't spawned, which the result counts; a weak reference to
20
+ * one is left out; and any other entity value naming one is `Entity.none`. With `entities: false`, the result leaves
21
+ * out its map of entities, which costs about 85 ms and 50 MB at a million. Fails, as data, for a column or
22
+ * resource that doesn't fit, spawning nothing.
23
+ */
24
+ export declare function spawn(input: {
25
+ readonly world: World;
26
+ readonly snapshot: Snapshot;
27
+ readonly entities: false;
28
+ }): Promise<Spawned<false> | Failure>;
29
+ export declare function spawn(input: {
30
+ readonly world: World;
31
+ readonly snapshot: Snapshot;
32
+ readonly entities?: true;
33
+ }): Promise<Spawned | Failure>;
@@ -0,0 +1,5 @@
1
+ import type { Schema } from "@adobe/ecs";
2
+ /** A value of `schema` as JSON. */
3
+ export declare const toJson: (schema: Schema, value: unknown) => unknown;
4
+ /** A value of `schema` read back from its JSON. */
5
+ export declare const fromJson: (schema: Schema, json: unknown) => unknown;
package/package.json CHANGED
@@ -1,6 +1,42 @@
1
1
  {
2
2
  "name": "@adobe/ecs-persistence",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.0.1",
4
+ "description": "Saving, loading and copying the entities of @adobe/ecs worlds, as binary columns",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": true,
8
+ "private": false,
9
+ "independentVersion": true,
10
+ "publishConfig": {
11
+ "registry": "https://registry.npmjs.org/",
12
+ "access": "public"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src",
17
+ "!src/**/*.test.ts",
18
+ "!src/**/*.test-helper.ts",
19
+ "LICENSE"
20
+ ],
21
+ "dependencies": {
22
+ "@adobe/ecs": "0.2.3"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^25.6.0",
26
+ "typescript": "^5.8.3",
27
+ "vite": "^8.3.4",
28
+ "vitest": "^5.0.5"
29
+ },
30
+ "exports": {
31
+ ".": {
32
+ "import": "./dist/index.js",
33
+ "types": "./dist/types/index.d.ts"
34
+ }
35
+ },
36
+ "scripts": {
37
+ "build": "cp ../../LICENSE . && tsc -p tsconfig.build.json && vite build",
38
+ "typecheck": "tsc && tsc -p tsconfig.build.json",
39
+ "test": "vitest --run",
40
+ "test:perf": "vitest --run --config vitest.perf.config.ts"
41
+ }
6
42
  }
package/src/decode.ts ADDED
@@ -0,0 +1,15 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import type { Failure } from "./failure.js";
3
+ import { decodeContents } from "./format/format.js";
4
+ import { contents } from "./program/contents.js";
5
+ import type { Snapshot } from "./snapshot.js";
6
+
7
+ /**
8
+ * The snapshot `bytes` holds, its columns of bytes viewed where they are in `bytes`, copied once only when `bytes`
9
+ * doesn't start at a multiple of 8; or why it can't be read: it isn't one, or names a component or resource this
10
+ * program doesn't persist, or persists with another schema.
11
+ */
12
+ export const decode = (bytes: Uint8Array): Snapshot | Failure => {
13
+ const decoded = decodeContents(bytes);
14
+ return "failure" in decoded ? decoded : { [contents]: decoded };
15
+ };
package/src/encode.ts ADDED
@@ -0,0 +1,10 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { encodeContents } from "./format/format.js";
3
+ import { contents } from "./program/contents.js";
4
+ import type { Snapshot } from "./snapshot.js";
5
+
6
+ /**
7
+ * A snapshot as bytes, little-endian: a header naming each persisted component and resource with its schema, then
8
+ * 8-byte-aligned blocks of each table's entities and columns, its bytes columns copied whole.
9
+ */
10
+ export const encode = (snapshot: Snapshot): Uint8Array => encodeContents(snapshot[contents]);
package/src/failure.ts ADDED
@@ -0,0 +1,4 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+
3
+ /** Why a snapshot, a decoding or a spawn didn't happen. */
4
+ export type Failure = { readonly failure: string };
@@ -0,0 +1,185 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import type { Component, Resource, Schema, World } from "@adobe/ecs";
3
+ import type { Failure } from "../failure.js";
4
+ import type { Contents } from "../program/contents.js";
5
+ import { programOf, type Persisted } from "../program/program.js";
6
+ import { fromJson, toJson } from "../values/values.js";
7
+
8
+ /**
9
+ * The binary form of a snapshot, little-endian:
10
+ * - 16 bytes: `ECSP`, the format's version as a u32, the header's length in bytes as a u32, and 4 bytes of zeros;
11
+ * - the header, JSON text padded with spaces to a multiple of 8 bytes: each component and resource by name with its
12
+ * schema, and each table's count and where its entities and columns are;
13
+ * - blocks, each starting at a multiple of 8 bytes: a table's entities as f64s, a column's bytes as its components'
14
+ * layout holds them, and a column of values as JSON text.
15
+ * A loader views the blocks of bytes where they are, in the buffer it's given.
16
+ */
17
+
18
+ const magic = [0x45, 0x43, 0x53, 0x50];
19
+ const version = 1;
20
+ const prefix = 16;
21
+
22
+ /** Where a column's data is. */
23
+ type ColumnJson =
24
+ | { readonly kind: "bytes"; readonly offset: number; readonly length: number; readonly stride: number }
25
+ | { readonly kind: "values"; readonly offset: number; readonly length: number }
26
+ | { readonly kind: "value"; readonly value: unknown };
27
+
28
+ type Header = {
29
+ readonly components: readonly { readonly name: string; readonly schema: Schema }[];
30
+ readonly resources: readonly { readonly name: string; readonly schema: Schema; readonly value: unknown }[];
31
+ readonly tables: readonly { readonly count: number; readonly entities: number; readonly columns: readonly ({ readonly component: number } & ColumnJson)[] }[];
32
+ };
33
+
34
+ const aligned = (offset: number): number => Math.ceil(offset / 8) * 8;
35
+
36
+ const encoder = new TextEncoder();
37
+ const decoder = new TextDecoder();
38
+
39
+ /** A snapshot's contents as bytes. */
40
+ export const encodeContents = (snapshot: Contents): Uint8Array => {
41
+ const blocks: Uint8Array[] = [];
42
+ let offset = 0;
43
+ /** Places a block, returning its offset from the first block. */
44
+ const place = (bytes: Uint8Array): number => {
45
+ const at = offset;
46
+ blocks.push(bytes);
47
+ offset = aligned(offset + bytes.byteLength);
48
+ return at;
49
+ };
50
+ const indexOf = new Map(snapshot.components.map((persisted, index) => [persisted.key, index]));
51
+ const tables = snapshot.tables.map(table => {
52
+ const entities = place(new Uint8Array(table.entities.buffer, table.entities.byteOffset, table.entities.byteLength));
53
+ const columns = snapshot.components.flatMap((persisted): ({ readonly component: number } & ColumnJson)[] => {
54
+ const key: symbol = persisted.key;
55
+ const column: World.Table.Column | undefined = Reflect.get(table.columns, key);
56
+ const component = indexOf.get(persisted.key) ?? -1;
57
+ if (column === undefined) return [];
58
+ if (column.kind === "bytes") return [{ component, kind: "bytes", offset: place(column.bytes), length: column.bytes.byteLength, stride: column.stride }];
59
+ if (column.kind === "value") return [{ component, kind: "value", value: toJson(persisted.schema, column.value) }];
60
+ const text = encoder.encode(JSON.stringify(column.values.map(value => toJson(persisted.schema, value))));
61
+ return [{ component, kind: "values", offset: place(text), length: text.byteLength }];
62
+ });
63
+ return { count: table.count, entities, columns };
64
+ });
65
+ const header: Header = {
66
+ components: snapshot.components.map(({ name, schema }) => ({ name, schema })),
67
+ resources: snapshot.resources.map(({ persisted, value }) => ({ name: persisted.name, schema: persisted.schema, value: toJson(persisted.schema, value) })),
68
+ tables,
69
+ };
70
+ const text = encoder.encode(JSON.stringify(header));
71
+ const headerLength = aligned(text.byteLength);
72
+ const bytes = new Uint8Array(prefix + headerLength + offset);
73
+ bytes.set(magic, 0);
74
+ const view = new DataView(bytes.buffer);
75
+ view.setUint32(4, version, true);
76
+ view.setUint32(8, headerLength, true);
77
+ bytes.set(text, prefix);
78
+ bytes.fill(0x20, prefix + text.byteLength, prefix + headerLength);
79
+ let at = prefix + headerLength;
80
+ for (const block of blocks) {
81
+ bytes.set(block, at);
82
+ at = aligned(at + block.byteLength);
83
+ }
84
+ return bytes;
85
+ };
86
+
87
+ const isObject = (value: unknown): value is object => typeof value === "object" && value !== null;
88
+
89
+ /** The header's JSON, checked enough to read it without casts, or null. */
90
+ const headerOf = (json: unknown): Header | null => {
91
+ if (!isObject(json)) return null;
92
+ const components: unknown = Reflect.get(json, "components");
93
+ const resources: unknown = Reflect.get(json, "resources");
94
+ const tables: unknown = Reflect.get(json, "tables");
95
+ if (!Array.isArray(components) || !Array.isArray(resources) || !Array.isArray(tables)) return null;
96
+ const named = (item: unknown): boolean => isObject(item) && typeof Reflect.get(item, "name") === "string" && isObject(Reflect.get(item, "schema"));
97
+ if (!components.every(named) || !resources.every(named)) return null;
98
+ const isColumn = (item: unknown): boolean => {
99
+ if (!isObject(item) || typeof Reflect.get(item, "component") !== "number") return false;
100
+ const kind: unknown = Reflect.get(item, "kind");
101
+ if (kind === "value") return true;
102
+ const numbers = kind === "bytes" ? ["offset", "length", "stride"] : kind === "values" ? ["offset", "length"] : null;
103
+ return numbers !== null && numbers.every(name => typeof Reflect.get(item, name) === "number");
104
+ };
105
+ const isTable = (item: unknown): boolean => {
106
+ if (!isObject(item) || typeof Reflect.get(item, "count") !== "number" || typeof Reflect.get(item, "entities") !== "number") return false;
107
+ const columns: unknown = Reflect.get(item, "columns");
108
+ return Array.isArray(columns) && columns.every(isColumn);
109
+ };
110
+ if (!tables.every(isTable)) return null;
111
+ // Checked above: each item has the fields the header's type names, of their types.
112
+ const header = json as Header;
113
+ return header;
114
+ };
115
+
116
+ /** Whether two schemas are the same, by their JSON in their properties' order, which places a struct's fields. */
117
+ const sameSchema = (a: Schema, b: Schema): boolean => JSON.stringify(a) === JSON.stringify(b);
118
+
119
+ /**
120
+ * The snapshot contents `bytes` holds, its components and resources by name as this program persists them, its blocks of
121
+ * bytes viewed where they are; or why it can't be read: it isn't one, or names what this program doesn't persist, or
122
+ * persists with another schema.
123
+ */
124
+ export const decodeContents = (bytes: Uint8Array): Contents | Failure => {
125
+ if (bytes.byteLength < prefix || magic.some((byte, at) => bytes[at] !== byte)) return { failure: "These bytes aren't a snapshot." };
126
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
127
+ const found = view.getUint32(4, true);
128
+ if (found !== version) return { failure: `This snapshot's format is version ${found}; this package reads version ${version}.` };
129
+ const headerLength = view.getUint32(8, true);
130
+ let json: unknown;
131
+ try {
132
+ json = JSON.parse(decoder.decode(bytes.subarray(prefix, prefix + headerLength)));
133
+ }
134
+ catch {
135
+ return { failure: "This snapshot's header isn't JSON." };
136
+ }
137
+ const header = headerOf(json);
138
+ if (header === null) return { failure: "This snapshot's header isn't one this package writes." };
139
+ const program = programOf();
140
+ if (typeof program === "string") return { failure: program };
141
+ const components: Persisted<Component.Key>[] = [];
142
+ for (const { name, schema } of header.components) {
143
+ const persisted = program.components.get(name);
144
+ if (persisted === undefined) return { failure: `This snapshot holds component "${name}", which this program doesn't persist.` };
145
+ if (!sameSchema(persisted.schema, schema)) return { failure: `Component "${name}" has another schema here than in this snapshot.` };
146
+ components.push(persisted);
147
+ }
148
+ const resources: { persisted: Persisted<Resource.Key>; value: unknown }[] = [];
149
+ for (const { name, schema, value } of header.resources) {
150
+ const persisted = program.resources.get(name);
151
+ if (persisted === undefined) return { failure: `This snapshot holds resource "${name}", which this program doesn't persist.` };
152
+ if (!sameSchema(persisted.schema, schema)) return { failure: `Resource "${name}" has another schema here than in this snapshot.` };
153
+ resources.push({ persisted, value: fromJson(schema, value) });
154
+ }
155
+ // Blocks are 8-byte aligned in the buffer the format was written to; one given at another offset is copied once.
156
+ const blocks = (bytes.byteOffset + prefix + headerLength) % 8 === 0 ? bytes : bytes.slice();
157
+ const base = blocks.byteOffset + prefix + headerLength;
158
+ const end = blocks.byteOffset + blocks.byteLength;
159
+ const inBounds = (offset: number, length: number): boolean => offset >= 0 && length >= 0 && base + offset + length <= end;
160
+ const tables: World.Table[] = [];
161
+ for (const table of header.tables) {
162
+ if (!inBounds(table.entities, table.count * 8)) return { failure: "This snapshot's blocks are cut short." };
163
+ const columns: { [key: symbol]: World.Table.Column } = {};
164
+ for (const column of table.columns) {
165
+ const persisted = components[column.component];
166
+ if (persisted === undefined) return { failure: "This snapshot names a component it doesn't hold." };
167
+ if (column.kind === "value") columns[persisted.key] = { kind: "value", value: fromJson(persisted.schema, column.value) };
168
+ else if (!inBounds(column.offset, column.length)) return { failure: "This snapshot's blocks are cut short." };
169
+ else if (column.kind === "bytes") columns[persisted.key] = { kind: "bytes", bytes: new Uint8Array(blocks.buffer, base + column.offset, column.length), stride: column.stride };
170
+ else {
171
+ let values: unknown;
172
+ try {
173
+ values = JSON.parse(decoder.decode(new Uint8Array(blocks.buffer, base + column.offset, column.length)));
174
+ }
175
+ catch {
176
+ return { failure: `Component "${persisted.name}"'s values in this snapshot aren't JSON.` };
177
+ }
178
+ if (!Array.isArray(values)) return { failure: `Component "${persisted.name}"'s values in this snapshot aren't a list.` };
179
+ columns[persisted.key] = { kind: "values", values: values.map(value => fromJson(persisted.schema, value)) };
180
+ }
181
+ }
182
+ tables.push({ count: table.count, entities: new Float64Array(blocks.buffer, base + table.entities, table.count), columns });
183
+ }
184
+ return { components, tables, resources };
185
+ };
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ /**
3
+ * Saving, loading and copying the entities of `@adobe/ecs` worlds: snapshots of their persisted components and
4
+ * resources, as binary columns.
5
+ * @module @adobe/ecs-persistence
6
+ */
7
+ export { decode } from "./decode.js";
8
+ export { encode } from "./encode.js";
9
+ export type { Failure } from "./failure.js";
10
+ export { persist } from "./persist.js";
11
+ export { snapshot, type Snapshot } from "./snapshot.js";
12
+ export { spawn, type Spawned } from "./spawn.js";
package/src/persist.ts ADDED
@@ -0,0 +1,15 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import type { Component, Components } from "@adobe/ecs";
3
+
4
+ /**
5
+ * The trait marking a component or resource to persist, its value the stable name saved data names it by: unlike its
6
+ * key's id, the name stays the same across versions and moves of its module. Not on a derived component, which its
7
+ * rule derives again once its entity is loaded.
8
+ */
9
+ export const persist = Symbol();
10
+
11
+ declare module "@adobe/ecs" {
12
+ interface Traits<N> {
13
+ readonly [persist]: N extends Component.Key ? Components[N] extends { readonly derived: true } ? never : string : string;
14
+ }
15
+ }
@@ -0,0 +1,17 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import type { Component, Resource, World } from "@adobe/ecs";
3
+ import type { Persisted } from "./program.js";
4
+
5
+ /** Where a snapshot holds what it holds, a key only this package uses. */
6
+ export const contents: unique symbol = Symbol("snapshot contents");
7
+
8
+ /**
9
+ * The persisted part of some of a world's entities: of each archetype, its entities and a column of each persisted
10
+ * component it holds, keyed by this program's keys; and the values of persisted resources.
11
+ */
12
+ export type Contents = {
13
+ /** Each persisted component a table of it may hold. */
14
+ readonly components: readonly Persisted<Component.Key>[];
15
+ readonly tables: readonly World.Table[];
16
+ readonly resources: readonly { readonly persisted: Persisted<Resource.Key>; readonly value: unknown }[];
17
+ };