@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 +21 -0
- package/README.md +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/mirror.d.ts +71 -0
- package/dist/mirror.js +146 -0
- package/package.json +31 -4
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
|
-
#
|
|
1
|
+
# @aethervtt/shard-mirror
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Part of [Shard](https://github.com/aethervtt/shard), a WebGPU-first 2D/3D game engine in TypeScript. See the repository for documentation.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createMirror, Mirror, type MirrorCounts, type MirrorOptions } from './mirror.js';
|
package/dist/index.js
ADDED
package/dist/mirror.d.ts
ADDED
|
@@ -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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|