@fairfox/polly 0.20.1 → 0.22.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/README.md +83 -3
- package/dist/cli/polly.js +21 -1
- package/dist/cli/polly.js.map +3 -3
- package/dist/src/background/index.js.map +7 -7
- package/dist/src/background/message-router.js.map +7 -7
- package/dist/src/elysia/index.d.ts +2 -0
- package/dist/src/elysia/index.js +177 -17
- package/dist/src/elysia/index.js.map +8 -5
- package/dist/src/elysia/peer-repo-plugin.d.ts +79 -0
- package/dist/src/elysia/signaling-server-plugin.d.ts +121 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +90 -1
- package/dist/src/index.js.map +15 -13
- package/dist/src/mesh.d.ts +29 -0
- package/dist/src/mesh.js +1502 -0
- package/dist/src/mesh.js.map +22 -0
- package/dist/src/peer.d.ts +29 -0
- package/dist/src/peer.js +928 -0
- package/dist/src/peer.js.map +20 -0
- package/dist/src/shared/adapters/index.js.map +6 -6
- package/dist/src/shared/lib/_client-only.d.ts +38 -0
- package/dist/src/shared/lib/access.d.ts +124 -0
- package/dist/src/shared/lib/blob-ref.d.ts +72 -0
- package/dist/src/shared/lib/context-helpers.js.map +7 -7
- package/dist/src/shared/lib/crdt-specialised.d.ts +129 -0
- package/dist/src/shared/lib/crdt-state.d.ts +86 -0
- package/dist/src/shared/lib/encryption.d.ts +117 -0
- package/dist/src/shared/lib/mesh-network-adapter.d.ts +130 -0
- package/dist/src/shared/lib/mesh-signaling-client.d.ts +85 -0
- package/dist/src/shared/lib/mesh-state.d.ts +102 -0
- package/dist/src/shared/lib/mesh-webrtc-adapter.d.ts +132 -0
- package/dist/src/shared/lib/message-bus.js.map +7 -7
- package/dist/src/shared/lib/migrate-primitive.d.ts +100 -0
- package/dist/src/shared/lib/pairing.d.ts +170 -0
- package/dist/src/shared/lib/peer-relay-adapter.d.ts +80 -0
- package/dist/src/shared/lib/peer-repo-server.d.ts +83 -0
- package/dist/src/shared/lib/peer-state.d.ts +117 -0
- package/dist/src/shared/lib/primitive-registry.d.ts +88 -0
- package/dist/src/shared/lib/resource.js.map +4 -4
- package/dist/src/shared/lib/revocation.d.ts +126 -0
- package/dist/src/shared/lib/schema-version.d.ts +129 -0
- package/dist/src/shared/lib/signing.d.ts +118 -0
- package/dist/src/shared/lib/state.js.map +4 -4
- package/dist/src/shared/state/app-state.js.map +5 -5
- package/dist/tools/init/src/cli.js.map +1 -1
- package/dist/tools/quality/src/cli.js +162 -0
- package/dist/tools/quality/src/cli.js.map +11 -0
- package/dist/tools/test/src/adapters/index.js.map +2 -2
- package/dist/tools/test/src/browser/harness.d.ts +80 -0
- package/dist/tools/test/src/browser/index.d.ts +32 -0
- package/dist/tools/test/src/browser/index.js +243 -0
- package/dist/tools/test/src/browser/index.js.map +10 -0
- package/dist/tools/test/src/browser/run.d.ts +26 -0
- package/dist/tools/test/src/index.js.map +2 -2
- package/dist/tools/verify/specs/tla/MeshState.cfg +21 -0
- package/dist/tools/verify/specs/tla/MeshState.tla +247 -0
- package/dist/tools/verify/specs/tla/PeerState.cfg +27 -0
- package/dist/tools/verify/specs/tla/PeerState.tla +238 -0
- package/dist/tools/verify/specs/tla/README.md +27 -3
- package/dist/tools/verify/src/cli.js.map +8 -8
- package/dist/tools/visualize/src/cli.js.map +7 -7
- package/package.json +51 -5
package/dist/src/peer.js
ADDED
|
@@ -0,0 +1,928 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
function __accessProp(key) {
|
|
6
|
+
return this[key];
|
|
7
|
+
}
|
|
8
|
+
var __toCommonJS = (from) => {
|
|
9
|
+
var entry = (__moduleCache ??= new WeakMap).get(from), desc;
|
|
10
|
+
if (entry)
|
|
11
|
+
return entry;
|
|
12
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (var key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(entry, key))
|
|
16
|
+
__defProp(entry, key, {
|
|
17
|
+
get: __accessProp.bind(from, key),
|
|
18
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
__moduleCache.set(from, entry);
|
|
22
|
+
return entry;
|
|
23
|
+
};
|
|
24
|
+
var __moduleCache;
|
|
25
|
+
var __returnValue = (v) => v;
|
|
26
|
+
function __exportSetter(name, newValue) {
|
|
27
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
28
|
+
}
|
|
29
|
+
var __export = (target, all) => {
|
|
30
|
+
for (var name in all)
|
|
31
|
+
__defProp(target, name, {
|
|
32
|
+
get: all[name],
|
|
33
|
+
enumerable: true,
|
|
34
|
+
configurable: true,
|
|
35
|
+
set: __exportSetter.bind(all, name)
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
39
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
40
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
41
|
+
}) : x)(function(x) {
|
|
42
|
+
if (typeof require !== "undefined")
|
|
43
|
+
return require.apply(this, arguments);
|
|
44
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// src/shared/lib/crdt-specialised.ts
|
|
48
|
+
import { Counter, updateText } from "@automerge/automerge-repo";
|
|
49
|
+
import { effect, signal } from "@preact/signals";
|
|
50
|
+
|
|
51
|
+
// src/shared/lib/migrate-primitive.ts
|
|
52
|
+
class MigrationError extends Error {
|
|
53
|
+
code;
|
|
54
|
+
key;
|
|
55
|
+
primitive;
|
|
56
|
+
constructor(message, code, key, primitive) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.name = "MigrationError";
|
|
59
|
+
this.code = code;
|
|
60
|
+
this.key = key;
|
|
61
|
+
this.primitive = primitive;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class MigrationRegistry {
|
|
66
|
+
marks = new Set;
|
|
67
|
+
entryKey(key, primitive) {
|
|
68
|
+
return `${primitive}:${key}`;
|
|
69
|
+
}
|
|
70
|
+
mark(key, primitive) {
|
|
71
|
+
this.marks.add(this.entryKey(key, primitive));
|
|
72
|
+
}
|
|
73
|
+
isMarked(key, primitive) {
|
|
74
|
+
return this.marks.has(this.entryKey(key, primitive));
|
|
75
|
+
}
|
|
76
|
+
clear() {
|
|
77
|
+
this.marks.clear();
|
|
78
|
+
}
|
|
79
|
+
get size() {
|
|
80
|
+
return this.marks.size;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
var migrationRegistry = new MigrationRegistry;
|
|
84
|
+
async function migratePrimitive(source, destination, transform) {
|
|
85
|
+
if (source === destination) {
|
|
86
|
+
throw new MigrationError(`Cannot migrate a primitive to itself: "${source.key}" under ${source.primitive}.`, "same-primitive-instance", source.key, source.primitive);
|
|
87
|
+
}
|
|
88
|
+
if (migrationRegistry.isMarked(source.key, source.primitive)) {
|
|
89
|
+
throw new MigrationError(`Cannot migrate: source "${source.key}" under $${source.primitive} has already been migrated. Migrations are one-way and one-time.`, "already-migrated", source.key, source.primitive);
|
|
90
|
+
}
|
|
91
|
+
await source.loaded;
|
|
92
|
+
await destination.loaded;
|
|
93
|
+
const transformed = transform(source.value);
|
|
94
|
+
destination.value = transformed;
|
|
95
|
+
migrationRegistry.mark(source.key, source.primitive);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/shared/lib/primitive-registry.ts
|
|
99
|
+
class PrimitiveCollisionError extends Error {
|
|
100
|
+
key;
|
|
101
|
+
firstPrimitive;
|
|
102
|
+
firstCallSite;
|
|
103
|
+
secondPrimitive;
|
|
104
|
+
secondCallSite;
|
|
105
|
+
constructor(key, firstPrimitive, firstCallSite, secondPrimitive, secondCallSite) {
|
|
106
|
+
const firstLocation = firstCallSite ? ` (at ${firstCallSite})` : "";
|
|
107
|
+
const secondLocation = secondCallSite ? ` (at ${secondCallSite})` : "";
|
|
108
|
+
super(`Polly primitive key collision: "${key}" is already registered as ` + `$${firstPrimitive}${firstLocation} and cannot also be registered ` + `as $${secondPrimitive}${secondLocation}. Pick a different key or ` + `use the same primitive in both places.`);
|
|
109
|
+
this.name = "PrimitiveCollisionError";
|
|
110
|
+
this.key = key;
|
|
111
|
+
this.firstPrimitive = firstPrimitive;
|
|
112
|
+
this.firstCallSite = firstCallSite;
|
|
113
|
+
this.secondPrimitive = secondPrimitive;
|
|
114
|
+
this.secondCallSite = secondCallSite;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
class PrimitiveRegistry {
|
|
119
|
+
entries = new Map;
|
|
120
|
+
register(key, primitive, callSite) {
|
|
121
|
+
const existing = this.entries.get(key);
|
|
122
|
+
if (existing && existing.primitive !== primitive) {
|
|
123
|
+
throw new PrimitiveCollisionError(key, existing.primitive, existing.callSite, primitive, callSite);
|
|
124
|
+
}
|
|
125
|
+
if (!existing) {
|
|
126
|
+
this.entries.set(key, { primitive, callSite });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
has(key) {
|
|
130
|
+
return this.entries.has(key);
|
|
131
|
+
}
|
|
132
|
+
kindOf(key) {
|
|
133
|
+
return this.entries.get(key)?.primitive;
|
|
134
|
+
}
|
|
135
|
+
clear() {
|
|
136
|
+
this.entries.clear();
|
|
137
|
+
}
|
|
138
|
+
get size() {
|
|
139
|
+
return this.entries.size;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
var primitiveRegistry = new PrimitiveRegistry;
|
|
143
|
+
|
|
144
|
+
// src/shared/lib/schema-version.ts
|
|
145
|
+
var SCHEMA_VERSION_FIELD = "__schemaVersion";
|
|
146
|
+
|
|
147
|
+
class SchemaVersionError extends Error {
|
|
148
|
+
code;
|
|
149
|
+
docVersion;
|
|
150
|
+
targetVersion;
|
|
151
|
+
opVersion;
|
|
152
|
+
missingVersion;
|
|
153
|
+
constructor(message, code, details = {}) {
|
|
154
|
+
super(message);
|
|
155
|
+
this.name = "SchemaVersionError";
|
|
156
|
+
this.code = code;
|
|
157
|
+
if (details.docVersion !== undefined)
|
|
158
|
+
this.docVersion = details.docVersion;
|
|
159
|
+
if (details.targetVersion !== undefined)
|
|
160
|
+
this.targetVersion = details.targetVersion;
|
|
161
|
+
if (details.opVersion !== undefined)
|
|
162
|
+
this.opVersion = details.opVersion;
|
|
163
|
+
if (details.missingVersion !== undefined)
|
|
164
|
+
this.missingVersion = details.missingVersion;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function getDocVersion(doc) {
|
|
168
|
+
if (typeof doc !== "object" || doc === null)
|
|
169
|
+
return 0;
|
|
170
|
+
const record = doc;
|
|
171
|
+
const value = record[SCHEMA_VERSION_FIELD];
|
|
172
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 0;
|
|
173
|
+
}
|
|
174
|
+
function setDocVersion(doc, version) {
|
|
175
|
+
doc[SCHEMA_VERSION_FIELD] = version;
|
|
176
|
+
}
|
|
177
|
+
function runMigrations(doc, targetVersion, migrations) {
|
|
178
|
+
const current = getDocVersion(doc);
|
|
179
|
+
if (current > targetVersion) {
|
|
180
|
+
throw new SchemaVersionError(`Document is at schema version ${current} but the application targets ${targetVersion}. Upgrade the application to continue.`, "doc-ahead-of-app", { docVersion: current, targetVersion });
|
|
181
|
+
}
|
|
182
|
+
for (let v = current + 1;v <= targetVersion; v++) {
|
|
183
|
+
const migration = migrations[v];
|
|
184
|
+
if (!migration) {
|
|
185
|
+
throw new SchemaVersionError(`Missing migration for schema version ${v}. Migrations must be contiguous from ${current + 1} through ${targetVersion}.`, "missing-migration", { docVersion: current, targetVersion, missingVersion: v });
|
|
186
|
+
}
|
|
187
|
+
migration(doc);
|
|
188
|
+
setDocVersion(doc, v);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function checkOpVersion(opVersion, docVersion) {
|
|
192
|
+
if (opVersion < docVersion) {
|
|
193
|
+
return { compatible: false, reason: "op-older-than-doc", opVersion, docVersion };
|
|
194
|
+
}
|
|
195
|
+
if (opVersion > docVersion) {
|
|
196
|
+
return { compatible: false, reason: "op-newer-than-doc", opVersion, docVersion };
|
|
197
|
+
}
|
|
198
|
+
return { compatible: true };
|
|
199
|
+
}
|
|
200
|
+
function assertOpVersion(opVersion, docVersion) {
|
|
201
|
+
const result = checkOpVersion(opVersion, docVersion);
|
|
202
|
+
if (result.compatible)
|
|
203
|
+
return;
|
|
204
|
+
const message = result.reason === "op-older-than-doc" ? `Incoming op was produced at schema version ${opVersion} but the document is at version ${docVersion}. The producing peer is behind.` : `Incoming op was produced at schema version ${opVersion} but the document is at version ${docVersion}. The current peer is behind and should upgrade.`;
|
|
205
|
+
throw new SchemaVersionError(message, result.reason, { opVersion, docVersion });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/shared/lib/crdt-specialised.ts
|
|
209
|
+
function createSpecialisedPrimitive(config) {
|
|
210
|
+
if (migrationRegistry.isMarked(config.key, config.primitive)) {
|
|
211
|
+
throw new MigrationError(`Cannot construct $${config.primitive}("${config.key}"): this key has been marked as migrated. Migrations are one-way; use the destination primitive instead.`, "already-migrated", config.key, config.primitive);
|
|
212
|
+
}
|
|
213
|
+
primitiveRegistry.register(config.key, config.primitive, config.callSite);
|
|
214
|
+
const inner = signal(config.initialValue);
|
|
215
|
+
let updating = false;
|
|
216
|
+
let currentHandle;
|
|
217
|
+
const loaded = (async () => {
|
|
218
|
+
const handle = await config.getHandle();
|
|
219
|
+
await handle.whenReady();
|
|
220
|
+
currentHandle = handle;
|
|
221
|
+
if (config.schemaVersion !== undefined) {
|
|
222
|
+
const targetVersion = config.schemaVersion;
|
|
223
|
+
const migrations = config.migrations ?? {};
|
|
224
|
+
handle.change((doc) => {
|
|
225
|
+
runMigrations(doc, targetVersion, migrations);
|
|
226
|
+
setDocVersion(doc, targetVersion);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
updating = true;
|
|
230
|
+
try {
|
|
231
|
+
inner.value = config.extractValue(handle.doc());
|
|
232
|
+
} finally {
|
|
233
|
+
updating = false;
|
|
234
|
+
}
|
|
235
|
+
handle.on("change", (payload) => {
|
|
236
|
+
if (updating)
|
|
237
|
+
return;
|
|
238
|
+
updating = true;
|
|
239
|
+
try {
|
|
240
|
+
inner.value = config.extractValue(payload.doc);
|
|
241
|
+
} finally {
|
|
242
|
+
updating = false;
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
effect(() => {
|
|
246
|
+
const value = inner.value;
|
|
247
|
+
if (updating)
|
|
248
|
+
return;
|
|
249
|
+
if (!currentHandle)
|
|
250
|
+
return;
|
|
251
|
+
updating = true;
|
|
252
|
+
try {
|
|
253
|
+
currentHandle.change((doc) => {
|
|
254
|
+
config.applyWrite(doc, value);
|
|
255
|
+
});
|
|
256
|
+
} finally {
|
|
257
|
+
updating = false;
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
})();
|
|
261
|
+
return {
|
|
262
|
+
key: config.key,
|
|
263
|
+
primitive: config.primitive,
|
|
264
|
+
get value() {
|
|
265
|
+
return inner.value;
|
|
266
|
+
},
|
|
267
|
+
set value(next) {
|
|
268
|
+
inner.value = next;
|
|
269
|
+
},
|
|
270
|
+
loaded,
|
|
271
|
+
get handle() {
|
|
272
|
+
return currentHandle;
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function $crdtText(key, initialValue, options) {
|
|
277
|
+
return createSpecialisedPrimitive({
|
|
278
|
+
key,
|
|
279
|
+
primitive: options.primitive ?? "peerState",
|
|
280
|
+
initialValue,
|
|
281
|
+
getHandle: options.getHandle,
|
|
282
|
+
extractValue: (doc) => doc.text ?? "",
|
|
283
|
+
applyWrite: (doc, value) => {
|
|
284
|
+
if (doc.text === undefined) {
|
|
285
|
+
doc.text = value;
|
|
286
|
+
} else {
|
|
287
|
+
updateText(doc, ["text"], value);
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
schemaVersion: options.schemaVersion,
|
|
291
|
+
migrations: options.migrations,
|
|
292
|
+
access: options.access,
|
|
293
|
+
callSite: options.callSite
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
function $crdtCounter(key, initialValue, options) {
|
|
297
|
+
return createSpecialisedPrimitive({
|
|
298
|
+
key,
|
|
299
|
+
primitive: options.primitive ?? "peerState",
|
|
300
|
+
initialValue,
|
|
301
|
+
getHandle: options.getHandle,
|
|
302
|
+
extractValue: (doc) => {
|
|
303
|
+
const c = doc.count;
|
|
304
|
+
if (c === undefined)
|
|
305
|
+
return 0;
|
|
306
|
+
return c.value;
|
|
307
|
+
},
|
|
308
|
+
applyWrite: (doc, value) => {
|
|
309
|
+
const existing = doc.count;
|
|
310
|
+
if (existing === undefined) {
|
|
311
|
+
doc.count = new Counter(value);
|
|
312
|
+
} else {
|
|
313
|
+
const delta = value - existing.value;
|
|
314
|
+
if (delta !== 0) {
|
|
315
|
+
existing.increment(delta);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
schemaVersion: options.schemaVersion,
|
|
320
|
+
migrations: options.migrations,
|
|
321
|
+
access: options.access,
|
|
322
|
+
callSite: options.callSite
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
function $crdtList(key, initialValue, options) {
|
|
326
|
+
return createSpecialisedPrimitive({
|
|
327
|
+
key,
|
|
328
|
+
primitive: options.primitive ?? "peerState",
|
|
329
|
+
initialValue,
|
|
330
|
+
getHandle: options.getHandle,
|
|
331
|
+
extractValue: (doc) => doc.items ? [...doc.items] : [],
|
|
332
|
+
applyWrite: (doc, value) => {
|
|
333
|
+
doc.items = [...value];
|
|
334
|
+
},
|
|
335
|
+
schemaVersion: options.schemaVersion,
|
|
336
|
+
migrations: options.migrations,
|
|
337
|
+
access: options.access,
|
|
338
|
+
callSite: options.callSite
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
// src/shared/lib/crdt-state.ts
|
|
342
|
+
import { effect as effect2, signal as signal2 } from "@preact/signals";
|
|
343
|
+
function $crdtState(options) {
|
|
344
|
+
if (migrationRegistry.isMarked(options.key, options.primitive)) {
|
|
345
|
+
throw new MigrationError(`Cannot construct $${options.primitive}("${options.key}"): this key has been marked as migrated. Migrations are one-way; use the destination primitive instead.`, "already-migrated", options.key, options.primitive);
|
|
346
|
+
}
|
|
347
|
+
primitiveRegistry.register(options.key, options.primitive, options.callSite);
|
|
348
|
+
const inner = signal2(options.initialValue);
|
|
349
|
+
let updating = false;
|
|
350
|
+
let currentHandle;
|
|
351
|
+
const loaded = (async () => {
|
|
352
|
+
const handle = await options.getHandle();
|
|
353
|
+
await handle.whenReady();
|
|
354
|
+
currentHandle = handle;
|
|
355
|
+
if (options.schemaVersion !== undefined) {
|
|
356
|
+
const targetVersion = options.schemaVersion;
|
|
357
|
+
const migrations = options.migrations ?? {};
|
|
358
|
+
handle.change((doc) => {
|
|
359
|
+
runMigrations(doc, targetVersion, migrations);
|
|
360
|
+
setDocVersion(doc, targetVersion);
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
updating = true;
|
|
364
|
+
try {
|
|
365
|
+
inner.value = cloneDoc(handle.doc());
|
|
366
|
+
} finally {
|
|
367
|
+
updating = false;
|
|
368
|
+
}
|
|
369
|
+
handle.on("change", (payload) => {
|
|
370
|
+
if (updating)
|
|
371
|
+
return;
|
|
372
|
+
updating = true;
|
|
373
|
+
try {
|
|
374
|
+
inner.value = cloneDoc(payload.doc);
|
|
375
|
+
} finally {
|
|
376
|
+
updating = false;
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
effect2(() => {
|
|
380
|
+
const value = inner.value;
|
|
381
|
+
if (updating)
|
|
382
|
+
return;
|
|
383
|
+
if (!currentHandle)
|
|
384
|
+
return;
|
|
385
|
+
updating = true;
|
|
386
|
+
try {
|
|
387
|
+
currentHandle.change((doc) => {
|
|
388
|
+
applyTopLevel(doc, value);
|
|
389
|
+
});
|
|
390
|
+
} finally {
|
|
391
|
+
updating = false;
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
})();
|
|
395
|
+
return {
|
|
396
|
+
key: options.key,
|
|
397
|
+
primitive: options.primitive,
|
|
398
|
+
get value() {
|
|
399
|
+
return inner.value;
|
|
400
|
+
},
|
|
401
|
+
set value(next) {
|
|
402
|
+
inner.value = next;
|
|
403
|
+
},
|
|
404
|
+
loaded,
|
|
405
|
+
get handle() {
|
|
406
|
+
return currentHandle;
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
function cloneDoc(doc) {
|
|
411
|
+
return JSON.parse(JSON.stringify(doc));
|
|
412
|
+
}
|
|
413
|
+
function applyTopLevel(doc, value) {
|
|
414
|
+
for (const key of Object.keys(value)) {
|
|
415
|
+
if (key === SCHEMA_VERSION_FIELD)
|
|
416
|
+
continue;
|
|
417
|
+
doc[key] = value[key];
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// src/shared/lib/peer-relay-adapter.ts
|
|
421
|
+
import { Repo } from "@automerge/automerge-repo";
|
|
422
|
+
import { WebSocketClientAdapter } from "@automerge/automerge-repo-network-websocket";
|
|
423
|
+
import { signal as signal3 } from "@preact/signals";
|
|
424
|
+
|
|
425
|
+
// src/shared/lib/mesh-network-adapter.ts
|
|
426
|
+
import {
|
|
427
|
+
NetworkAdapter
|
|
428
|
+
} from "@automerge/automerge-repo";
|
|
429
|
+
|
|
430
|
+
// src/shared/lib/encryption.ts
|
|
431
|
+
import nacl from "tweetnacl";
|
|
432
|
+
var KEY_BYTES = 32;
|
|
433
|
+
var NONCE_BYTES = 24;
|
|
434
|
+
var TAG_BYTES = 16;
|
|
435
|
+
|
|
436
|
+
class EncryptionError extends Error {
|
|
437
|
+
code;
|
|
438
|
+
constructor(message, code) {
|
|
439
|
+
super(message);
|
|
440
|
+
this.name = "EncryptionError";
|
|
441
|
+
this.code = code;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function generateDocumentKey() {
|
|
445
|
+
return nacl.randomBytes(KEY_BYTES);
|
|
446
|
+
}
|
|
447
|
+
function encrypt(payload, key) {
|
|
448
|
+
if (key.length !== KEY_BYTES) {
|
|
449
|
+
throw new EncryptionError(`secretbox key must be ${KEY_BYTES} bytes, got ${key.length}.`, "invalid-key-length");
|
|
450
|
+
}
|
|
451
|
+
const nonce = nacl.randomBytes(NONCE_BYTES);
|
|
452
|
+
const ciphertext = nacl.secretbox(payload, nonce, key);
|
|
453
|
+
const out = new Uint8Array(NONCE_BYTES + ciphertext.length);
|
|
454
|
+
out.set(nonce, 0);
|
|
455
|
+
out.set(ciphertext, NONCE_BYTES);
|
|
456
|
+
return out;
|
|
457
|
+
}
|
|
458
|
+
function decrypt(sealed, key) {
|
|
459
|
+
if (key.length !== KEY_BYTES) {
|
|
460
|
+
throw new EncryptionError(`secretbox key must be ${KEY_BYTES} bytes, got ${key.length}.`, "invalid-key-length");
|
|
461
|
+
}
|
|
462
|
+
if (sealed.length < NONCE_BYTES + TAG_BYTES) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const nonce = sealed.subarray(0, NONCE_BYTES);
|
|
466
|
+
const ciphertext = sealed.subarray(NONCE_BYTES);
|
|
467
|
+
const opened = nacl.secretbox.open(ciphertext, nonce, key);
|
|
468
|
+
return opened ?? undefined;
|
|
469
|
+
}
|
|
470
|
+
function decryptOrThrow(sealed, key) {
|
|
471
|
+
const opened = decrypt(sealed, key);
|
|
472
|
+
if (!opened) {
|
|
473
|
+
throw new EncryptionError(`Failed to decrypt sealed blob: wrong key, malformed input, or tampered ciphertext.`, "decrypt-failed");
|
|
474
|
+
}
|
|
475
|
+
return opened;
|
|
476
|
+
}
|
|
477
|
+
function sealEnvelope(payload, documentId, key) {
|
|
478
|
+
return {
|
|
479
|
+
documentId,
|
|
480
|
+
sealed: encrypt(payload, key)
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function openEnvelope(envelope, key) {
|
|
484
|
+
return decryptOrThrow(envelope.sealed, key);
|
|
485
|
+
}
|
|
486
|
+
function encodeEncryptedEnvelope(envelope) {
|
|
487
|
+
const idBytes = new TextEncoder().encode(envelope.documentId);
|
|
488
|
+
const out = new Uint8Array(4 + idBytes.length + envelope.sealed.length);
|
|
489
|
+
const view = new DataView(out.buffer);
|
|
490
|
+
view.setUint32(0, idBytes.length, false);
|
|
491
|
+
out.set(idBytes, 4);
|
|
492
|
+
out.set(envelope.sealed, 4 + idBytes.length);
|
|
493
|
+
return out;
|
|
494
|
+
}
|
|
495
|
+
function decodeEncryptedEnvelope(bytes) {
|
|
496
|
+
if (bytes.length < 4) {
|
|
497
|
+
throw new EncryptionError(`Encrypted envelope too short: ${bytes.length} bytes.`, "envelope-malformed");
|
|
498
|
+
}
|
|
499
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
500
|
+
const idLen = view.getUint32(0, false);
|
|
501
|
+
if (bytes.length < 4 + idLen) {
|
|
502
|
+
throw new EncryptionError(`Encrypted envelope truncated: declared id length ${idLen}, total ${bytes.length}.`, "envelope-malformed");
|
|
503
|
+
}
|
|
504
|
+
const documentId = new TextDecoder().decode(bytes.subarray(4, 4 + idLen));
|
|
505
|
+
const sealed = bytes.slice(4 + idLen);
|
|
506
|
+
return { documentId, sealed };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/shared/lib/signing.ts
|
|
510
|
+
import nacl2 from "tweetnacl";
|
|
511
|
+
var PUBLIC_KEY_BYTES = 32;
|
|
512
|
+
var SECRET_KEY_BYTES = 64;
|
|
513
|
+
var SIGNATURE_BYTES = 64;
|
|
514
|
+
|
|
515
|
+
class SigningError extends Error {
|
|
516
|
+
code;
|
|
517
|
+
constructor(message, code) {
|
|
518
|
+
super(message);
|
|
519
|
+
this.name = "SigningError";
|
|
520
|
+
this.code = code;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function generateSigningKeyPair() {
|
|
524
|
+
const pair = nacl2.sign.keyPair();
|
|
525
|
+
return {
|
|
526
|
+
publicKey: pair.publicKey,
|
|
527
|
+
secretKey: pair.secretKey
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
function signingKeyPairFromSecret(secretKey) {
|
|
531
|
+
if (secretKey.length !== SECRET_KEY_BYTES) {
|
|
532
|
+
throw new SigningError(`Ed25519 secret key must be ${SECRET_KEY_BYTES} bytes, got ${secretKey.length}.`, "invalid-secret-key");
|
|
533
|
+
}
|
|
534
|
+
const pair = nacl2.sign.keyPair.fromSecretKey(secretKey);
|
|
535
|
+
return {
|
|
536
|
+
publicKey: pair.publicKey,
|
|
537
|
+
secretKey: pair.secretKey
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
function sign(payload, secretKey) {
|
|
541
|
+
if (secretKey.length !== SECRET_KEY_BYTES) {
|
|
542
|
+
throw new SigningError(`Ed25519 secret key must be ${SECRET_KEY_BYTES} bytes, got ${secretKey.length}.`, "invalid-secret-key");
|
|
543
|
+
}
|
|
544
|
+
return nacl2.sign.detached(payload, secretKey);
|
|
545
|
+
}
|
|
546
|
+
function verify(payload, signature, publicKey) {
|
|
547
|
+
if (publicKey.length !== PUBLIC_KEY_BYTES) {
|
|
548
|
+
throw new SigningError(`Ed25519 public key must be ${PUBLIC_KEY_BYTES} bytes, got ${publicKey.length}.`, "invalid-public-key");
|
|
549
|
+
}
|
|
550
|
+
if (signature.length !== SIGNATURE_BYTES) {
|
|
551
|
+
throw new SigningError(`Ed25519 signature must be ${SIGNATURE_BYTES} bytes, got ${signature.length}.`, "invalid-signature-length");
|
|
552
|
+
}
|
|
553
|
+
return nacl2.sign.detached.verify(payload, signature, publicKey);
|
|
554
|
+
}
|
|
555
|
+
function signEnvelope(payload, senderId, secretKey) {
|
|
556
|
+
const signature = sign(payload, secretKey);
|
|
557
|
+
return { senderId, payload, signature };
|
|
558
|
+
}
|
|
559
|
+
function openEnvelope2(envelope, publicKey) {
|
|
560
|
+
const ok = verify(envelope.payload, envelope.signature, publicKey);
|
|
561
|
+
if (!ok) {
|
|
562
|
+
throw new SigningError(`Signature verification failed for envelope from ${envelope.senderId}.`, "envelope-malformed");
|
|
563
|
+
}
|
|
564
|
+
return envelope.payload;
|
|
565
|
+
}
|
|
566
|
+
function encodeSignedEnvelope(envelope) {
|
|
567
|
+
const senderBytes = new TextEncoder().encode(envelope.senderId);
|
|
568
|
+
const total = 4 + senderBytes.length + SIGNATURE_BYTES + envelope.payload.length;
|
|
569
|
+
const out = new Uint8Array(total);
|
|
570
|
+
const view = new DataView(out.buffer);
|
|
571
|
+
view.setUint32(0, senderBytes.length, false);
|
|
572
|
+
out.set(senderBytes, 4);
|
|
573
|
+
out.set(envelope.signature, 4 + senderBytes.length);
|
|
574
|
+
out.set(envelope.payload, 4 + senderBytes.length + SIGNATURE_BYTES);
|
|
575
|
+
return out;
|
|
576
|
+
}
|
|
577
|
+
function decodeSignedEnvelope(bytes) {
|
|
578
|
+
if (bytes.length < 4 + SIGNATURE_BYTES) {
|
|
579
|
+
throw new SigningError(`Envelope too short: ${bytes.length} bytes, need at least ${4 + SIGNATURE_BYTES}.`, "envelope-malformed");
|
|
580
|
+
}
|
|
581
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
582
|
+
const senderLen = view.getUint32(0, false);
|
|
583
|
+
if (bytes.length < 4 + senderLen + SIGNATURE_BYTES) {
|
|
584
|
+
throw new SigningError(`Envelope truncated: declared sender length ${senderLen}, total ${bytes.length}.`, "envelope-malformed");
|
|
585
|
+
}
|
|
586
|
+
const senderId = new TextDecoder().decode(bytes.subarray(4, 4 + senderLen));
|
|
587
|
+
const signature = bytes.slice(4 + senderLen, 4 + senderLen + SIGNATURE_BYTES);
|
|
588
|
+
const payload = bytes.slice(4 + senderLen + SIGNATURE_BYTES);
|
|
589
|
+
return { senderId, payload, signature };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// src/shared/lib/mesh-network-adapter.ts
|
|
593
|
+
var DEFAULT_MESH_KEY_ID = "polly-mesh-default";
|
|
594
|
+
|
|
595
|
+
class MeshNetworkAdapter extends NetworkAdapter {
|
|
596
|
+
base;
|
|
597
|
+
keyring;
|
|
598
|
+
encryptionEnabled;
|
|
599
|
+
constructor(options) {
|
|
600
|
+
super();
|
|
601
|
+
this.base = options.base;
|
|
602
|
+
this.keyring = options.keyring;
|
|
603
|
+
this.encryptionEnabled = options.encryptionEnabled ?? true;
|
|
604
|
+
this.base.on("close", () => this.emit("close"));
|
|
605
|
+
this.base.on("peer-candidate", (payload) => this.emit("peer-candidate", payload));
|
|
606
|
+
this.base.on("peer-disconnected", (payload) => this.emit("peer-disconnected", payload));
|
|
607
|
+
this.base.on("message", (rawMessage) => {
|
|
608
|
+
const unwrapped = this.tryUnwrap(rawMessage);
|
|
609
|
+
if (unwrapped) {
|
|
610
|
+
this.emit("message", unwrapped);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
isReady() {
|
|
615
|
+
return this.base.isReady();
|
|
616
|
+
}
|
|
617
|
+
whenReady() {
|
|
618
|
+
return this.base.whenReady();
|
|
619
|
+
}
|
|
620
|
+
connect(peerId, peerMetadata) {
|
|
621
|
+
this.peerId = peerId;
|
|
622
|
+
if (peerMetadata !== undefined) {
|
|
623
|
+
this.peerMetadata = peerMetadata;
|
|
624
|
+
}
|
|
625
|
+
this.base.connect(peerId, peerMetadata);
|
|
626
|
+
}
|
|
627
|
+
disconnect() {
|
|
628
|
+
this.base.disconnect();
|
|
629
|
+
}
|
|
630
|
+
send(message) {
|
|
631
|
+
const wrapped = this.wrap(message);
|
|
632
|
+
this.base.send(wrapped);
|
|
633
|
+
}
|
|
634
|
+
wrap(message) {
|
|
635
|
+
const serialised = serialiseMessage(message);
|
|
636
|
+
let payloadToSign;
|
|
637
|
+
if (this.encryptionEnabled) {
|
|
638
|
+
const docKey = this.keyring.documentKeys.get(DEFAULT_MESH_KEY_ID);
|
|
639
|
+
if (!docKey) {
|
|
640
|
+
throw new Error(`MeshNetworkAdapter: missing document encryption key under id "${DEFAULT_MESH_KEY_ID}". Provision the key in the keyring before sending.`);
|
|
641
|
+
}
|
|
642
|
+
const encrypted = sealEnvelope(serialised, DEFAULT_MESH_KEY_ID, docKey);
|
|
643
|
+
payloadToSign = encodeEncryptedEnvelope(encrypted);
|
|
644
|
+
} else {
|
|
645
|
+
payloadToSign = serialised;
|
|
646
|
+
}
|
|
647
|
+
const signed = signEnvelope(payloadToSign, message.senderId, this.keyring.identity.secretKey);
|
|
648
|
+
const signedBytes = encodeSignedEnvelope(signed);
|
|
649
|
+
return {
|
|
650
|
+
type: message.type,
|
|
651
|
+
senderId: message.senderId,
|
|
652
|
+
targetId: message.targetId,
|
|
653
|
+
data: signedBytes
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
tryUnwrap(message) {
|
|
657
|
+
if (!message.data)
|
|
658
|
+
return;
|
|
659
|
+
let signed;
|
|
660
|
+
try {
|
|
661
|
+
signed = decodeSignedEnvelope(message.data);
|
|
662
|
+
} catch {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (this.keyring.revokedPeers.has(signed.senderId)) {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const senderKey = this.keyring.knownPeers.get(signed.senderId);
|
|
669
|
+
if (!senderKey) {
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
let verifiedPayload;
|
|
673
|
+
try {
|
|
674
|
+
verifiedPayload = openEnvelope2(signed, senderKey);
|
|
675
|
+
} catch {
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
if (!this.encryptionEnabled) {
|
|
679
|
+
return deserialiseMessage(verifiedPayload);
|
|
680
|
+
}
|
|
681
|
+
let encrypted;
|
|
682
|
+
try {
|
|
683
|
+
encrypted = decodeEncryptedEnvelope(verifiedPayload);
|
|
684
|
+
} catch {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const docKey = this.keyring.documentKeys.get(encrypted.documentId);
|
|
688
|
+
if (!docKey) {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
let plaintext;
|
|
692
|
+
try {
|
|
693
|
+
plaintext = openEnvelope(encrypted, docKey);
|
|
694
|
+
} catch {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
return deserialiseMessage(plaintext);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
function serialiseMessage(message) {
|
|
701
|
+
const headerObj = {
|
|
702
|
+
type: message.type,
|
|
703
|
+
senderId: message.senderId,
|
|
704
|
+
targetId: message.targetId
|
|
705
|
+
};
|
|
706
|
+
if ("documentId" in message && message.documentId !== undefined) {
|
|
707
|
+
headerObj["documentId"] = message.documentId;
|
|
708
|
+
}
|
|
709
|
+
if ("count" in message && message.count !== undefined) {
|
|
710
|
+
headerObj["count"] = message.count;
|
|
711
|
+
}
|
|
712
|
+
if ("sessionId" in message && message.sessionId !== undefined) {
|
|
713
|
+
headerObj["sessionId"] = message.sessionId;
|
|
714
|
+
}
|
|
715
|
+
const headerBytes = new TextEncoder().encode(JSON.stringify(headerObj));
|
|
716
|
+
const dataBytes = "data" in message && message.data instanceof Uint8Array ? message.data : new Uint8Array(0);
|
|
717
|
+
const out = new Uint8Array(4 + headerBytes.length + dataBytes.length);
|
|
718
|
+
const view = new DataView(out.buffer);
|
|
719
|
+
view.setUint32(0, headerBytes.length, false);
|
|
720
|
+
out.set(headerBytes, 4);
|
|
721
|
+
out.set(dataBytes, 4 + headerBytes.length);
|
|
722
|
+
return out;
|
|
723
|
+
}
|
|
724
|
+
function deserialiseMessage(bytes) {
|
|
725
|
+
if (bytes.length < 4) {
|
|
726
|
+
throw new Error("MeshNetworkAdapter: message too short to deserialise.");
|
|
727
|
+
}
|
|
728
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
729
|
+
const headerLen = view.getUint32(0, false);
|
|
730
|
+
if (bytes.length < 4 + headerLen) {
|
|
731
|
+
throw new Error("MeshNetworkAdapter: message header truncated.");
|
|
732
|
+
}
|
|
733
|
+
const header = JSON.parse(new TextDecoder().decode(bytes.subarray(4, 4 + headerLen)));
|
|
734
|
+
const data = bytes.slice(4 + headerLen);
|
|
735
|
+
return { ...header, data };
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// src/shared/lib/peer-relay-adapter.ts
|
|
739
|
+
function createPeerStateClient(options) {
|
|
740
|
+
if (options.sign && !options.keyring) {
|
|
741
|
+
throw new Error("Polly createPeerStateClient: { sign: true } requires a keyring. Pass { keyring: { identity, knownPeers, documentKeys: new Map(), revokedPeers: new Set() } } to enable signing.");
|
|
742
|
+
}
|
|
743
|
+
const adapter = new WebSocketClientAdapter(options.url, options.retryInterval);
|
|
744
|
+
const connectionState = signal3("connecting");
|
|
745
|
+
adapter.on("peer-candidate", () => {
|
|
746
|
+
connectionState.value = "connected";
|
|
747
|
+
});
|
|
748
|
+
adapter.on("peer-disconnected", () => {
|
|
749
|
+
connectionState.value = "disconnected";
|
|
750
|
+
});
|
|
751
|
+
adapter.on("close", () => {
|
|
752
|
+
connectionState.value = "disconnected";
|
|
753
|
+
});
|
|
754
|
+
const networkAdapter = options.sign && options.keyring ? new MeshNetworkAdapter({
|
|
755
|
+
base: adapter,
|
|
756
|
+
keyring: options.keyring,
|
|
757
|
+
encryptionEnabled: false
|
|
758
|
+
}) : adapter;
|
|
759
|
+
const repo = new Repo({
|
|
760
|
+
network: [networkAdapter],
|
|
761
|
+
...options.storage !== undefined && { storage: options.storage }
|
|
762
|
+
});
|
|
763
|
+
return {
|
|
764
|
+
repo,
|
|
765
|
+
connectionState,
|
|
766
|
+
adapter,
|
|
767
|
+
signEnabled: options.sign === true,
|
|
768
|
+
close: async () => {
|
|
769
|
+
await repo.shutdown();
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
// src/shared/lib/peer-repo-server.ts
|
|
774
|
+
import { Repo as Repo2 } from "@automerge/automerge-repo";
|
|
775
|
+
import { WebSocketServerAdapter } from "@automerge/automerge-repo-network-websocket";
|
|
776
|
+
import { NodeFSStorageAdapter } from "@automerge/automerge-repo-storage-nodefs";
|
|
777
|
+
import * as ws from "ws";
|
|
778
|
+
async function createPeerRepoServer(options) {
|
|
779
|
+
const wss = await (options.webSocketServer ? Promise.resolve(options.webSocketServer) : new Promise((resolve, reject) => {
|
|
780
|
+
const created = new ws.WebSocketServer({
|
|
781
|
+
port: options.port,
|
|
782
|
+
...options.host !== undefined && { host: options.host }
|
|
783
|
+
}, () => resolve(created));
|
|
784
|
+
created.once("error", reject);
|
|
785
|
+
}));
|
|
786
|
+
const adapter = new WebSocketServerAdapter(wss);
|
|
787
|
+
const storage = new NodeFSStorageAdapter(options.storagePath);
|
|
788
|
+
const repo = new Repo2({
|
|
789
|
+
network: [adapter],
|
|
790
|
+
storage
|
|
791
|
+
});
|
|
792
|
+
await repo.storageId();
|
|
793
|
+
return {
|
|
794
|
+
repo,
|
|
795
|
+
webSocketServer: wss,
|
|
796
|
+
adapter,
|
|
797
|
+
storage,
|
|
798
|
+
close: async () => {
|
|
799
|
+
for (const client of wss.clients) {
|
|
800
|
+
try {
|
|
801
|
+
client.terminate();
|
|
802
|
+
} catch {}
|
|
803
|
+
}
|
|
804
|
+
repo.shutdown();
|
|
805
|
+
try {
|
|
806
|
+
wss.close();
|
|
807
|
+
} catch {}
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
// src/shared/lib/peer-state.ts
|
|
812
|
+
var keyMapsByRepo = new WeakMap;
|
|
813
|
+
var signingEnabledRepos = new WeakSet;
|
|
814
|
+
var defaultRepo;
|
|
815
|
+
function configurePeerState(repo, options) {
|
|
816
|
+
defaultRepo = repo;
|
|
817
|
+
keyMapsByRepo.set(repo, new Map);
|
|
818
|
+
if (options?.signEnabled) {
|
|
819
|
+
signingEnabledRepos.add(repo);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
function resetPeerState() {
|
|
823
|
+
defaultRepo = undefined;
|
|
824
|
+
}
|
|
825
|
+
function resolveRepo(option) {
|
|
826
|
+
const repo = option ?? defaultRepo;
|
|
827
|
+
if (!repo) {
|
|
828
|
+
throw new Error("Polly $peerState: no Repo configured. Call configurePeerState(repo) at startup or pass { repo } in the primitive options.");
|
|
829
|
+
}
|
|
830
|
+
return repo;
|
|
831
|
+
}
|
|
832
|
+
function getKeyMap(repo) {
|
|
833
|
+
let map = keyMapsByRepo.get(repo);
|
|
834
|
+
if (!map) {
|
|
835
|
+
map = new Map;
|
|
836
|
+
keyMapsByRepo.set(repo, map);
|
|
837
|
+
}
|
|
838
|
+
return map;
|
|
839
|
+
}
|
|
840
|
+
function validateSignOption(options, repo) {
|
|
841
|
+
if (!options.sign)
|
|
842
|
+
return;
|
|
843
|
+
if (!signingEnabledRepos.has(repo)) {
|
|
844
|
+
throw new Error("Polly $peerState: { sign: true } was passed to the primitive but the configured Repo does not have signing enabled. " + "Pass { sign: true, keyring: ... } to createPeerStateClient to enable signing at the transport level, " + "then call configurePeerState(client.repo, { signEnabled: true }).");
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
function buildHandleFactory(repo, key, initialDoc) {
|
|
848
|
+
return async () => {
|
|
849
|
+
const map = getKeyMap(repo);
|
|
850
|
+
const existingId = map.get(key);
|
|
851
|
+
if (existingId !== undefined) {
|
|
852
|
+
return repo.find(existingId);
|
|
853
|
+
}
|
|
854
|
+
const handle = repo.create(initialDoc);
|
|
855
|
+
map.set(key, handle.documentId);
|
|
856
|
+
return handle;
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
function $peerState(key, initialValue, options = {}) {
|
|
860
|
+
const repo = resolveRepo(options.repo);
|
|
861
|
+
validateSignOption(options, repo);
|
|
862
|
+
return $crdtState({
|
|
863
|
+
key,
|
|
864
|
+
primitive: "peerState",
|
|
865
|
+
initialValue,
|
|
866
|
+
getHandle: buildHandleFactory(repo, key, initialValue),
|
|
867
|
+
schemaVersion: options.schemaVersion,
|
|
868
|
+
migrations: options.migrations,
|
|
869
|
+
access: options.access
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
function $peerText(key, initialValue, options = {}) {
|
|
873
|
+
const repo = resolveRepo(options.repo);
|
|
874
|
+
validateSignOption(options, repo);
|
|
875
|
+
return $crdtText(key, initialValue, {
|
|
876
|
+
primitive: "peerState",
|
|
877
|
+
getHandle: buildHandleFactory(repo, key, { text: initialValue }),
|
|
878
|
+
schemaVersion: options.schemaVersion,
|
|
879
|
+
migrations: options.migrations,
|
|
880
|
+
access: options.access
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
function $peerCounter(key, initialValue, options = {}) {
|
|
884
|
+
const repo = resolveRepo(options.repo);
|
|
885
|
+
validateSignOption(options, repo);
|
|
886
|
+
return $crdtCounter(key, initialValue, {
|
|
887
|
+
primitive: "peerState",
|
|
888
|
+
getHandle: buildHandleFactory(repo, key, {}),
|
|
889
|
+
schemaVersion: options.schemaVersion,
|
|
890
|
+
migrations: options.migrations,
|
|
891
|
+
access: options.access
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
function $peerList(key, initialValue, options = {}) {
|
|
895
|
+
const repo = resolveRepo(options.repo);
|
|
896
|
+
validateSignOption(options, repo);
|
|
897
|
+
return $crdtList(key, initialValue, {
|
|
898
|
+
primitive: "peerState",
|
|
899
|
+
getHandle: buildHandleFactory(repo, key, { items: initialValue }),
|
|
900
|
+
schemaVersion: options.schemaVersion,
|
|
901
|
+
migrations: options.migrations,
|
|
902
|
+
access: options.access
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
export {
|
|
906
|
+
resetPeerState,
|
|
907
|
+
migratePrimitive,
|
|
908
|
+
getDocVersion,
|
|
909
|
+
createPeerStateClient,
|
|
910
|
+
createPeerRepoServer,
|
|
911
|
+
configurePeerState,
|
|
912
|
+
checkOpVersion,
|
|
913
|
+
assertOpVersion,
|
|
914
|
+
SchemaVersionError,
|
|
915
|
+
SCHEMA_VERSION_FIELD,
|
|
916
|
+
PrimitiveCollisionError,
|
|
917
|
+
MigrationError,
|
|
918
|
+
$peerText,
|
|
919
|
+
$peerState,
|
|
920
|
+
$peerList,
|
|
921
|
+
$peerCounter,
|
|
922
|
+
$crdtText,
|
|
923
|
+
$crdtState,
|
|
924
|
+
$crdtList,
|
|
925
|
+
$crdtCounter
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
//# debugId=81EED7520484C6A364756E2164756E21
|