@aethervtt/shard-mirror 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Patrick Dias
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,3 @@
1
- # Temporary Holding Version
1
+ # @aethervtt/shard-mirror
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Part of [Shard](https://github.com/aethervtt/shard), a WebGPU-first 2D/3D game engine in TypeScript. See the repository for documentation.
@@ -0,0 +1 @@
1
+ export { createMirror, Mirror, type MirrorCounts, type MirrorOptions } from './mirror.js';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ import { createMirror, Mirror } from "./mirror.js";
2
+ export {
3
+ Mirror,
4
+ createMirror
5
+ };
@@ -0,0 +1,71 @@
1
+ import { type Entity, type World } from '@aethervtt/shard-core';
2
+ export interface MirrorOptions<D, K = string> {
3
+ /** The host's id for a document. Stable across revisions. */
4
+ key(doc: D): K;
5
+ /**
6
+ * The document's revision: a number that changes whenever the document does. An unchanged
7
+ * revision skips the document for one map lookup and one compare.
8
+ */
9
+ rev?(doc: D): number;
10
+ /** Without `rev`: whether a document changed since `prev` was applied. Never stringify here. */
11
+ equal?(prev: D, next: D): boolean;
12
+ /** Makes the entity (or the root of the entities) for a new document. `apply` runs next. */
13
+ spawn(doc: D, world: World): Entity;
14
+ /** Writes a new or changed document onto its entity. Write only the fields that moved. */
15
+ apply(entity: Entity, doc: D, world: World): void;
16
+ /**
17
+ * Takes a document's entity away (`key` is the document's). Default: `world.despawn` (the entity
18
+ * and its descendants).
19
+ */
20
+ despawn?(entity: Entity, world: World, key: K): void;
21
+ }
22
+ /** What one `sync`, `upsert` or `remove` did, for tests and `describe`. */
23
+ export interface MirrorCounts {
24
+ spawned: number;
25
+ applied: number;
26
+ removed: number;
27
+ }
28
+ /**
29
+ * Keyed sync from a host's documents to entities (0055). `sync` walks the host's full list once:
30
+ * a document whose revision is unchanged costs one map lookup and one number compare, and nothing
31
+ * allocates when nothing changed. A host that knows what changed calls `upsert` and `remove`.
32
+ */
33
+ export declare class Mirror<D, K = string> {
34
+ /** What the last call did. */
35
+ readonly last: MirrorCounts;
36
+ private readonly world;
37
+ private readonly options;
38
+ private readonly rows;
39
+ private readonly keys;
40
+ private pass;
41
+ /** Bound once, so the removal sweep allocates no closure. */
42
+ private readonly sweep;
43
+ constructor(world: World, options: MirrorOptions<D, K>);
44
+ /** Documents mirrored. */
45
+ get size(): number;
46
+ /**
47
+ * Applies the host's full list: spawns new documents, applies changed ones, and despawns the
48
+ * ones missing from `docs`.
49
+ */
50
+ sync(docs: readonly D[]): MirrorCounts;
51
+ /** Applies one document the host knows is new or changed (unchanged revisions are skipped). */
52
+ upsert(doc: D): Entity;
53
+ /** Despawns a document's entity. Returns whether it was mirrored. */
54
+ remove(key: K): boolean;
55
+ /** The entity mirroring a document. */
56
+ entity(key: K): Entity | undefined;
57
+ /**
58
+ * The host id for an entity or any of its descendants: a pick hit on a token's visual child
59
+ * resolves to the token's document.
60
+ */
61
+ keyOf(entity: Entity): K | undefined;
62
+ /** Every mirrored document's key, in the order they were first seen. */
63
+ keysInOrder(): K[];
64
+ /** Despawns every mirrored entity. */
65
+ clear(): void;
66
+ private create;
67
+ private update;
68
+ private drop;
69
+ }
70
+ /** A mirror of host documents onto `world` (0055). */
71
+ export declare function createMirror<D, K = string>(world: World, options: MirrorOptions<D, K>): Mirror<D, K>;
package/dist/mirror.js ADDED
@@ -0,0 +1,146 @@
1
+ import { ChildOf, ShardError } from "@aethervtt/shard-core";
2
+ class Mirror {
3
+ /** What the last call did. */
4
+ last = { spawned: 0, applied: 0, removed: 0 };
5
+ world;
6
+ options;
7
+ rows = /* @__PURE__ */ new Map();
8
+ keys = /* @__PURE__ */ new Map();
9
+ pass = 0;
10
+ /** Bound once, so the removal sweep allocates no closure. */
11
+ sweep = (row, key) => {
12
+ if (row.mark !== this.pass) this.drop(key, row);
13
+ };
14
+ constructor(world, options) {
15
+ if (!options.rev && !options.equal) {
16
+ throw new ShardError(
17
+ "mirror/no-diff",
18
+ "A mirror needs `rev` or `equal` to tell what changed",
19
+ {
20
+ hint: "Pass rev: (doc) => doc.rev when documents carry a revision (cheapest), or equal(prev, next)."
21
+ }
22
+ );
23
+ }
24
+ this.world = world;
25
+ this.options = options;
26
+ }
27
+ /** Documents mirrored. */
28
+ get size() {
29
+ return this.rows.size;
30
+ }
31
+ /**
32
+ * Applies the host's full list: spawns new documents, applies changed ones, and despawns the
33
+ * ones missing from `docs`.
34
+ */
35
+ sync(docs) {
36
+ const last = this.last;
37
+ last.spawned = last.applied = last.removed = 0;
38
+ const pass = ++this.pass;
39
+ const { key, rev } = this.options;
40
+ let seen = 0;
41
+ for (let i = 0; i < docs.length; i++) {
42
+ const doc = docs[i];
43
+ const k = key(doc);
44
+ const row = this.rows.get(k);
45
+ if (row) {
46
+ if (row.mark === pass) duplicate(k);
47
+ row.mark = pass;
48
+ seen++;
49
+ if (rev ? row.rev !== rev(doc) : !this.options.equal(row.doc, doc)) this.update(row, doc);
50
+ } else {
51
+ this.create(k, doc).mark = pass;
52
+ seen++;
53
+ }
54
+ }
55
+ if (seen !== this.rows.size) this.rows.forEach(this.sweep);
56
+ return last;
57
+ }
58
+ /** Applies one document the host knows is new or changed (unchanged revisions are skipped). */
59
+ upsert(doc) {
60
+ const last = this.last;
61
+ last.spawned = last.applied = last.removed = 0;
62
+ const k = this.options.key(doc);
63
+ const row = this.rows.get(k);
64
+ if (!row) return this.create(k, doc).entity;
65
+ const rev = this.options.rev;
66
+ if (rev ? row.rev !== rev(doc) : !this.options.equal(row.doc, doc)) this.update(row, doc);
67
+ return row.entity;
68
+ }
69
+ /** Despawns a document's entity. Returns whether it was mirrored. */
70
+ remove(key) {
71
+ const last = this.last;
72
+ last.spawned = last.applied = last.removed = 0;
73
+ const row = this.rows.get(key);
74
+ if (!row) return false;
75
+ this.drop(key, row);
76
+ return true;
77
+ }
78
+ /** The entity mirroring a document. */
79
+ entity(key) {
80
+ return this.rows.get(key)?.entity;
81
+ }
82
+ /**
83
+ * The host id for an entity or any of its descendants: a pick hit on a token's visual child
84
+ * resolves to the token's document.
85
+ */
86
+ keyOf(entity) {
87
+ const world = this.world;
88
+ let e = entity;
89
+ while (e !== null && world.isAlive(e)) {
90
+ const k = this.keys.get(e);
91
+ if (k !== void 0) return k;
92
+ e = world.tryGet(e, ChildOf)?.parent ?? null;
93
+ }
94
+ return void 0;
95
+ }
96
+ /** Every mirrored document's key, in the order they were first seen. */
97
+ keysInOrder() {
98
+ return [...this.rows.keys()];
99
+ }
100
+ /** Despawns every mirrored entity. */
101
+ clear() {
102
+ const last = this.last;
103
+ last.spawned = last.applied = last.removed = 0;
104
+ this.pass++;
105
+ this.rows.forEach(this.sweep);
106
+ }
107
+ create(k, doc) {
108
+ const { spawn, apply, rev, equal } = this.options;
109
+ const entity = spawn(doc, this.world);
110
+ const row = { entity, rev: rev ? rev(doc) : 0, doc: equal ? doc : void 0, mark: 0 };
111
+ this.rows.set(k, row);
112
+ this.keys.set(entity, k);
113
+ apply(entity, doc, this.world);
114
+ this.last.spawned++;
115
+ return row;
116
+ }
117
+ update(row, doc) {
118
+ const { apply, rev, equal } = this.options;
119
+ if (rev) row.rev = rev(doc);
120
+ if (equal) row.doc = doc;
121
+ apply(row.entity, doc, this.world);
122
+ this.last.applied++;
123
+ }
124
+ drop(k, row) {
125
+ this.rows.delete(k);
126
+ this.keys.delete(row.entity);
127
+ const world = this.world;
128
+ if (world.isAlive(row.entity)) {
129
+ if (this.options.despawn) this.options.despawn(row.entity, world, k);
130
+ else world.despawn(row.entity);
131
+ }
132
+ this.last.removed++;
133
+ }
134
+ }
135
+ function duplicate(key) {
136
+ throw new ShardError("mirror/duplicate-key", `Two documents in one sync share the key ${key}`, {
137
+ hint: "Keys are the host ids of documents: each may appear once per list."
138
+ });
139
+ }
140
+ function createMirror(world, options) {
141
+ return new Mirror(world, options);
142
+ }
143
+ export {
144
+ Mirror,
145
+ createMirror
146
+ };
package/package.json CHANGED
@@ -1,6 +1,33 @@
1
1
  {
2
2
  "name": "@aethervtt/shard-mirror",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "LICENSE",
16
+ "README.md"
17
+ ],
18
+ "dependencies": {
19
+ "@aethervtt/shard-core": "0.2.0"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/aethervtt/shard.git",
24
+ "directory": "packages/mirror"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public",
28
+ "provenance": true
29
+ },
30
+ "shard": {
31
+ "buildSha": "32c22a3944ed23f5d1baa90bf7cc9fae724f5fd8"
32
+ }
33
+ }