memeloop 0.1.3 → 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.
Files changed (72) hide show
  1. package/dist/browser.d.cts +14 -14
  2. package/dist/browser.d.ts +14 -14
  3. package/dist/{chunk-5JEV7632.js → chunk-5VYZV33O.js} +2 -1
  4. package/dist/{chunk-5JEV7632.js.map → chunk-5VYZV33O.js.map} +1 -1
  5. package/dist/chunk-ECNKBH5A.js +1235 -0
  6. package/dist/chunk-ECNKBH5A.js.map +1 -0
  7. package/dist/{chunk-MRKY4HGF.js → chunk-EPHRIRAL.js} +20 -11
  8. package/dist/chunk-EPHRIRAL.js.map +1 -0
  9. package/dist/{chunk-P6L3444O.js → chunk-KV6TC5UJ.js} +2 -2
  10. package/dist/chunk-MXRXYGWW.js +1 -0
  11. package/dist/conversation.d.cts +3 -3
  12. package/dist/conversation.d.ts +3 -3
  13. package/dist/device-network-portable.cjs +757 -144
  14. package/dist/device-network-portable.cjs.map +1 -1
  15. package/dist/device-network-portable.d.cts +24 -14
  16. package/dist/device-network-portable.d.ts +24 -14
  17. package/dist/device-network-portable.js +18 -2
  18. package/dist/device-network.cjs +776 -154
  19. package/dist/device-network.cjs.map +1 -1
  20. package/dist/device-network.d.cts +5 -5
  21. package/dist/device-network.d.ts +5 -5
  22. package/dist/device-network.js +19 -3
  23. package/dist/{fetchProvider-WWnWnQFP.d.ts → fetchProvider-BhqRqi5-.d.ts} +1 -1
  24. package/dist/{fetchProvider-toTHy5Nu.d.cts → fetchProvider-HJ_FkORN.d.cts} +1 -1
  25. package/dist/index.cjs +782 -155
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +16 -16
  28. package/dist/index.d.ts +16 -16
  29. package/dist/index.js +24 -6
  30. package/dist/index.js.map +1 -1
  31. package/dist/{interface-IrKBFtL1.d.ts → interface-3C3eG8Hn.d.cts} +13 -1
  32. package/dist/{interface-IrKBFtL1.d.cts → interface-3C3eG8Hn.d.ts} +13 -1
  33. package/dist/llm-providers.d.cts +2 -2
  34. package/dist/llm-providers.d.ts +2 -2
  35. package/dist/loop-api.cjs +1 -0
  36. package/dist/loop-api.cjs.map +1 -1
  37. package/dist/loop-api.d.cts +7 -7
  38. package/dist/loop-api.d.ts +7 -7
  39. package/dist/loop-api.js +2 -2
  40. package/dist/mobile.d.cts +5 -5
  41. package/dist/mobile.d.ts +5 -5
  42. package/dist/{orchestration-portable-BHB_YrNN.d.ts → orchestration-portable-BbkwLbC9.d.ts} +4 -4
  43. package/dist/{orchestration-portable-mvYBvGBb.d.cts → orchestration-portable-DNKzAtNq.d.cts} +4 -4
  44. package/dist/orchestration-portable.d.cts +5 -5
  45. package/dist/orchestration-portable.d.ts +5 -5
  46. package/dist/pairingInvite-DDx_-IAp.d.cts +271 -0
  47. package/dist/pairingInvite-Dkz8CLsb.d.ts +271 -0
  48. package/dist/{promptConcat-0WNFTg4l.d.ts → promptConcat-CUEIA_3T.d.ts} +3 -3
  49. package/dist/{promptConcat-D6wr5V-V.d.cts → promptConcat-VLe5SEN4.d.cts} +3 -3
  50. package/dist/{providerRegistry-CPsktuiX.d.cts → providerRegistry-BDGGs5gz.d.cts} +2 -2
  51. package/dist/{providerRegistry-Bi6pAvSj.d.ts → providerRegistry-CaFK_aUd.d.ts} +2 -2
  52. package/dist/{registry-D0kbxksn.d.cts → registry-DNSEVYug.d.cts} +3 -3
  53. package/dist/{registry-DDbvhlhs.d.ts → registry-yGnYHWhq.d.ts} +3 -3
  54. package/dist/{remoteClient-CBggnRy3.d.cts → remoteClient-BKmQEQY9.d.cts} +1 -1
  55. package/dist/{remoteClient-B_spK019.d.ts → remoteClient-D5hco6cS.d.ts} +1 -1
  56. package/dist/{runtime-UONPTAYK.js → runtime-6EELHWVB.js} +2 -2
  57. package/dist/{scriptDeploymentPipeline-CXJcC82H.d.ts → scriptDeploymentPipeline-B1q-UhrO.d.ts} +1 -1
  58. package/dist/{scriptDeploymentPipeline-yG1ZhVVI.d.cts → scriptDeploymentPipeline-XJzQyYys.d.cts} +1 -1
  59. package/dist/{syncCloudDevices-Ds_Kcgrv.d.cts → syncCloudDevices-BFw_c1g5.d.cts} +6 -5
  60. package/dist/{syncCloudDevices-BEqwgrhD.d.ts → syncCloudDevices-C8fCUhnX.d.ts} +6 -5
  61. package/dist/{types-CdFVSntj.d.ts → types-C8gnhG9M.d.cts} +6 -4
  62. package/dist/{types-7pYti-k9.d.cts → types-CcFFIk8W.d.ts} +6 -4
  63. package/package.json +1 -1
  64. package/dist/chunk-426CJUZP.js +0 -630
  65. package/dist/chunk-426CJUZP.js.map +0 -1
  66. package/dist/chunk-MRKY4HGF.js.map +0 -1
  67. package/dist/chunk-VZKQX5PP.js +0 -1
  68. package/dist/pairingInvite-BXPWY3aM.d.cts +0 -149
  69. package/dist/pairingInvite-Cuz0wuq9.d.ts +0 -149
  70. /package/dist/{chunk-P6L3444O.js.map → chunk-KV6TC5UJ.js.map} +0 -0
  71. /package/dist/{chunk-VZKQX5PP.js.map → chunk-MXRXYGWW.js.map} +0 -0
  72. /package/dist/{runtime-UONPTAYK.js.map → runtime-6EELHWVB.js.map} +0 -0
@@ -24,19 +24,27 @@ __export(device_network_portable_exports, {
24
24
  DEVICE_ORCHESTRATION_PROTOCOL: () => DEVICE_ORCHESTRATION_PROTOCOL,
25
25
  DEVICE_PAIRING_INVITE_PROTOCOL: () => DEVICE_PAIRING_INVITE_PROTOCOL,
26
26
  DEVICE_PAIRING_INVITE_TTL_MS: () => DEVICE_PAIRING_INVITE_TTL_MS,
27
+ DeviceCloudConnectionCoordinator: () => DeviceCloudConnectionCoordinator,
28
+ JsonFrameError: () => JsonFrameError,
27
29
  LIBP2P_RPC_REQUEST_TYPE: () => LIBP2P_RPC_REQUEST_TYPE,
28
30
  LIBP2P_RPC_RESPONSE_TYPE: () => LIBP2P_RPC_RESPONSE_TYPE,
29
31
  LIBP2P_SYNC_REQUEST_TYPE: () => LIBP2P_SYNC_REQUEST_TYPE,
30
32
  LIBP2P_SYNC_RESPONSE_TYPE: () => LIBP2P_SYNC_RESPONSE_TYPE,
31
33
  Libp2pDeviceSyncTransport: () => Libp2pDeviceSyncTransport,
32
34
  LocalTrustDeviceAuthorizer: () => LocalTrustDeviceAuthorizer,
35
+ MemoryDeviceSyncStateStore: () => MemoryDeviceSyncStateStore,
33
36
  PeerNodeSyncAdapter: () => PeerNodeSyncAdapter,
34
37
  attachmentBlobFromWire: () => attachmentBlobFromWire,
38
+ canonicalDevicePairingInviteBytes: () => canonicalDevicePairingInviteBytes,
39
+ computeMissingVersionRanges: () => computeMissingVersionRanges,
35
40
  createDeviceOrchestrationStreamHandler: () => createDeviceOrchestrationStreamHandler,
36
41
  createDeviceOrchestrationTransport: () => createDeviceOrchestrationTransport,
37
42
  createDevicePairingInvite: () => createDevicePairingInvite,
43
+ createJsonFrameReader: () => createJsonFrameReader,
38
44
  decodeAttachmentBlobRpc: () => decodeAttachmentBlobRpc,
39
45
  encodeDevicePairingInvite: () => encodeDevicePairingInvite,
46
+ encodeJsonFrame: () => encodeJsonFrame,
47
+ encodeJsonFrames: () => encodeJsonFrames,
40
48
  isConversationMeta: () => isConversationMeta,
41
49
  isLibp2pRpcRequest: () => isLibp2pRpcRequest,
42
50
  isLibp2pRpcResponse: () => isLibp2pRpcResponse,
@@ -46,6 +54,224 @@ __export(device_network_portable_exports, {
46
54
  });
47
55
  module.exports = __toCommonJS(device_network_portable_exports);
48
56
 
57
+ // src/device-network/deviceCloudConnectionCoordinator.ts
58
+ var DEFAULT_HEARTBEAT_INTERVAL_MS = 6e4;
59
+ var DEFAULT_INITIAL_BACKOFF_MS = 1e3;
60
+ var DEFAULT_MAX_BACKOFF_MS = 6e4;
61
+ var DEFAULT_JITTER_RATIO = 0.2;
62
+ function emptyComponents() {
63
+ return {
64
+ authorizer: "not-run",
65
+ registration: "not-run",
66
+ relay: "not-run",
67
+ heartbeat: "not-run",
68
+ directory: "not-run"
69
+ };
70
+ }
71
+ function assertNonNegativeFinite(value, name) {
72
+ if (!Number.isFinite(value) || value < 0) {
73
+ throw new TypeError(`${name} must be a finite non-negative number`);
74
+ }
75
+ }
76
+ var DeviceCloudConnectionCoordinator = class {
77
+ constructor(options) {
78
+ this.options = options;
79
+ this.configuration = options.configuration;
80
+ this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
81
+ this.initialBackoffMs = options.initialBackoffMs ?? DEFAULT_INITIAL_BACKOFF_MS;
82
+ this.maxBackoffMs = options.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS;
83
+ this.jitterRatio = options.jitterRatio ?? DEFAULT_JITTER_RATIO;
84
+ this.now = options.now ?? Date.now;
85
+ this.random = options.random ?? Math.random;
86
+ assertNonNegativeFinite(this.heartbeatIntervalMs, "heartbeatIntervalMs");
87
+ assertNonNegativeFinite(this.initialBackoffMs, "initialBackoffMs");
88
+ assertNonNegativeFinite(this.maxBackoffMs, "maxBackoffMs");
89
+ assertNonNegativeFinite(this.jitterRatio, "jitterRatio");
90
+ if (this.jitterRatio > 1) throw new TypeError("jitterRatio must not exceed 1");
91
+ this.resetSnapshot();
92
+ }
93
+ configuration;
94
+ generation = 0;
95
+ controller = new AbortController();
96
+ timer;
97
+ inFlight;
98
+ started = false;
99
+ consecutiveFailures = 0;
100
+ snapshotValue = {
101
+ status: "not-configured",
102
+ generation: 0,
103
+ components: emptyComponents()
104
+ };
105
+ heartbeatIntervalMs;
106
+ initialBackoffMs;
107
+ maxBackoffMs;
108
+ jitterRatio;
109
+ now;
110
+ random;
111
+ get snapshot() {
112
+ return {
113
+ ...this.snapshotValue,
114
+ components: { ...this.snapshotValue.components }
115
+ };
116
+ }
117
+ async start() {
118
+ if (this.started) return this.runNow();
119
+ this.started = true;
120
+ await this.runNow();
121
+ }
122
+ async stop() {
123
+ this.started = false;
124
+ this.clearTimer();
125
+ this.controller.abort(new Error("device cloud coordinator stopped"));
126
+ await this.inFlight?.catch(() => void 0);
127
+ }
128
+ async setConfiguration(configuration) {
129
+ this.configuration = configuration;
130
+ this.generation += 1;
131
+ this.consecutiveFailures = 0;
132
+ this.clearTimer();
133
+ this.controller.abort(new Error("device cloud configuration changed"));
134
+ this.controller = new AbortController();
135
+ this.inFlight = void 0;
136
+ this.resetSnapshot();
137
+ if (this.started && this.options.adapter.isConfigured(configuration)) await this.runNow();
138
+ }
139
+ runNow() {
140
+ if (this.inFlight) return this.inFlight;
141
+ if (!this.options.adapter.isConfigured(this.configuration)) {
142
+ this.resetSnapshot();
143
+ return Promise.resolve();
144
+ }
145
+ const generation = this.generation;
146
+ const configuration = this.configuration;
147
+ const signal = this.controller.signal;
148
+ const run = this.maintain(configuration, generation, signal);
149
+ const tracked = run.finally(() => {
150
+ if (this.inFlight === tracked) this.inFlight = void 0;
151
+ });
152
+ this.inFlight = tracked;
153
+ return tracked;
154
+ }
155
+ async maintain(configuration, generation, signal) {
156
+ if (!this.isCurrent(generation, signal)) return;
157
+ await this.updateSnapshot(generation, {
158
+ status: "connecting",
159
+ lastError: void 0,
160
+ nextRetryAt: void 0
161
+ });
162
+ try {
163
+ if (this.snapshotValue.components.authorizer !== "ready") {
164
+ await this.runStep("authorizer", generation, signal, () => this.options.adapter.ensureAuthorizer(configuration, signal));
165
+ }
166
+ if (this.snapshotValue.components.registration !== "ready") {
167
+ await this.runStep("registration", generation, signal, () => this.options.adapter.registerDevice(configuration, signal));
168
+ }
169
+ let relayError;
170
+ try {
171
+ await this.runStep("relay", generation, signal, () => this.options.adapter.ensureRelay(configuration, signal));
172
+ } catch (error) {
173
+ relayError = error;
174
+ this.options.logWarning?.("Cloud relay maintenance failed", error);
175
+ }
176
+ await this.runStep("heartbeat", generation, signal, () => this.options.adapter.heartbeat(configuration, signal));
177
+ let directoryError;
178
+ try {
179
+ await this.runStep("directory", generation, signal, () => this.options.adapter.syncDirectory(configuration, signal));
180
+ } catch (error) {
181
+ directoryError = error;
182
+ this.options.logWarning?.("Cloud directory synchronization failed", error);
183
+ }
184
+ if (!this.isCurrent(generation, signal)) return;
185
+ this.consecutiveFailures = 0;
186
+ const relayRequired = this.options.adapter.relayRequiredForOnline(configuration);
187
+ await this.updateSnapshot(generation, {
188
+ status: directoryError || relayRequired && relayError ? "degraded" : "online",
189
+ lastError: directoryError ?? relayError,
190
+ nextRetryAt: this.now() + this.heartbeatIntervalMs
191
+ });
192
+ this.schedule(this.heartbeatIntervalMs, generation);
193
+ } catch (error) {
194
+ if (!this.isCurrent(generation, signal)) return;
195
+ this.consecutiveFailures += 1;
196
+ if (this.snapshotValue.components.heartbeat === "failed") {
197
+ this.snapshotValue.components.registration = "not-run";
198
+ }
199
+ const status = this.options.adapter.classifyError?.(error) ?? "offline";
200
+ const delay = this.backoffDelay();
201
+ await this.updateSnapshot(generation, {
202
+ status,
203
+ lastError: error,
204
+ nextRetryAt: this.now() + delay
205
+ });
206
+ this.schedule(delay, generation);
207
+ throw error;
208
+ }
209
+ }
210
+ async runStep(component, generation, signal, operation) {
211
+ if (!this.isCurrent(generation, signal)) return;
212
+ await this.setComponent(component, "pending", generation);
213
+ try {
214
+ const result = await operation();
215
+ if (!this.isCurrent(generation, signal)) return;
216
+ await result?.commit?.();
217
+ if (!this.isCurrent(generation, signal)) return;
218
+ await this.setComponent(component, "ready", generation);
219
+ } catch (error) {
220
+ if (this.isCurrent(generation, signal)) await this.setComponent(component, "failed", generation);
221
+ throw error;
222
+ }
223
+ }
224
+ backoffDelay() {
225
+ const exponential = Math.min(
226
+ this.maxBackoffMs,
227
+ this.initialBackoffMs * 2 ** Math.max(0, this.consecutiveFailures - 1)
228
+ );
229
+ const jitter = exponential * this.jitterRatio * (this.random() * 2 - 1);
230
+ return Math.max(0, Math.round(exponential + jitter));
231
+ }
232
+ schedule(delayMs, generation) {
233
+ if (!this.started || generation !== this.generation) return;
234
+ this.clearTimer();
235
+ this.timer = setTimeout(() => {
236
+ if (generation !== this.generation) return;
237
+ void this.runNow().catch((error) => {
238
+ this.options.logWarning?.("Cloud maintenance failed", error);
239
+ });
240
+ }, delayMs);
241
+ }
242
+ clearTimer() {
243
+ if (this.timer) clearTimeout(this.timer);
244
+ this.timer = void 0;
245
+ }
246
+ isCurrent(generation, signal) {
247
+ return generation === this.generation && !signal.aborted;
248
+ }
249
+ async setComponent(component, status, generation) {
250
+ if (generation !== this.generation) return;
251
+ this.snapshotValue = {
252
+ ...this.snapshotValue,
253
+ components: { ...this.snapshotValue.components, [component]: status }
254
+ };
255
+ await this.emit();
256
+ }
257
+ async updateSnapshot(generation, update) {
258
+ if (generation !== this.generation) return;
259
+ this.snapshotValue = { ...this.snapshotValue, ...update };
260
+ await this.emit();
261
+ }
262
+ resetSnapshot() {
263
+ this.snapshotValue = {
264
+ status: this.options.adapter.isConfigured(this.configuration) ? "offline" : "not-configured",
265
+ generation: this.generation,
266
+ components: emptyComponents()
267
+ };
268
+ void this.emit();
269
+ }
270
+ async emit() {
271
+ await this.options.onStatus?.(this.snapshot);
272
+ }
273
+ };
274
+
49
275
  // src/orchestration/errors.ts
50
276
  var OrchestrationError = class extends Error {
51
277
  code;
@@ -74,9 +300,222 @@ var OrchestrationError = class extends Error {
74
300
  }
75
301
  };
76
302
 
303
+ // src/device-network/jsonFrame.ts
304
+ var JsonFrameError = class extends Error {
305
+ code;
306
+ constructor(code, message, options) {
307
+ super(message, options);
308
+ this.name = "JsonFrameError";
309
+ this.code = code;
310
+ }
311
+ };
312
+ var HEADER_BYTES = 4;
313
+ var MAX_UINT32 = 4294967295;
314
+ function assertPositiveInteger(value, name) {
315
+ if (!Number.isSafeInteger(value) || value <= 0) {
316
+ throw new TypeError(`${name} must be a positive safe integer`);
317
+ }
318
+ }
319
+ function validateMaxPayloadBytes(maxPayloadBytes) {
320
+ assertPositiveInteger(maxPayloadBytes, "maxPayloadBytes");
321
+ if (maxPayloadBytes > MAX_UINT32) {
322
+ throw new TypeError("maxPayloadBytes must fit in an unsigned 32-bit frame length");
323
+ }
324
+ }
325
+ function encodeJsonFrame(value, maxPayloadBytes = MAX_UINT32) {
326
+ validateMaxPayloadBytes(maxPayloadBytes);
327
+ let serialized;
328
+ try {
329
+ serialized = JSON.stringify(value);
330
+ } catch (error) {
331
+ throw new JsonFrameError("INVALID_JSON", "JSON frame value cannot be serialized", {
332
+ cause: error
333
+ });
334
+ }
335
+ if (serialized === void 0) {
336
+ throw new JsonFrameError("INVALID_JSON", "JSON frame value cannot be serialized");
337
+ }
338
+ const payload = new TextEncoder().encode(serialized);
339
+ if (payload.byteLength > maxPayloadBytes) {
340
+ throw new JsonFrameError(
341
+ "FRAME_TOO_LARGE",
342
+ `JSON frame payload exceeds ${maxPayloadBytes} bytes`
343
+ );
344
+ }
345
+ const frame = new Uint8Array(HEADER_BYTES + payload.byteLength);
346
+ new DataView(frame.buffer, frame.byteOffset, HEADER_BYTES).setUint32(0, payload.byteLength, false);
347
+ frame.set(payload, HEADER_BYTES);
348
+ return frame;
349
+ }
350
+ async function* encodeJsonFrames(values, maxPayloadBytes = MAX_UINT32) {
351
+ for await (const value of values) {
352
+ yield encodeJsonFrame(value, maxPayloadBytes);
353
+ }
354
+ }
355
+ function concatBytes(left, right) {
356
+ if (left.byteLength === 0) return right.slice();
357
+ if (right.byteLength === 0) return left;
358
+ const combined = new Uint8Array(left.byteLength + right.byteLength);
359
+ combined.set(left);
360
+ combined.set(right, left.byteLength);
361
+ return combined;
362
+ }
363
+ function abortedError(signal) {
364
+ return new JsonFrameError("ABORTED", "JSON frame reading was aborted", {
365
+ cause: signal?.reason
366
+ });
367
+ }
368
+ async function readNextChunk(iterator, idleDeadline, totalDeadline, signal) {
369
+ if (signal?.aborted) throw abortedError(signal);
370
+ const now = Date.now();
371
+ const deadline = totalDeadline === void 0 ? idleDeadline : Math.min(idleDeadline, totalDeadline);
372
+ if (deadline <= now) {
373
+ throw new JsonFrameError(
374
+ totalDeadline !== void 0 && totalDeadline <= idleDeadline ? "TOTAL_TIMEOUT" : "IDLE_TIMEOUT",
375
+ totalDeadline !== void 0 && totalDeadline <= idleDeadline ? "JSON frame total timeout expired" : "JSON frame idle timeout expired"
376
+ );
377
+ }
378
+ return new Promise((resolve, reject) => {
379
+ let settled = false;
380
+ const finish = (callback) => {
381
+ if (settled) return;
382
+ settled = true;
383
+ clearTimeout(timer);
384
+ signal?.removeEventListener("abort", onAbort);
385
+ callback();
386
+ };
387
+ const onAbort = () => {
388
+ finish(() => {
389
+ reject(abortedError(signal));
390
+ });
391
+ };
392
+ const timer = setTimeout(() => {
393
+ const currentTime = Date.now();
394
+ const totalExpired = totalDeadline !== void 0 && currentTime >= totalDeadline;
395
+ finish(() => {
396
+ reject(
397
+ new JsonFrameError(
398
+ totalExpired ? "TOTAL_TIMEOUT" : "IDLE_TIMEOUT",
399
+ totalExpired ? "JSON frame total timeout expired" : "JSON frame idle timeout expired"
400
+ )
401
+ );
402
+ });
403
+ }, Math.max(1, deadline - now));
404
+ signal?.addEventListener("abort", onAbort, { once: true });
405
+ void iterator.next().then(
406
+ (result) => {
407
+ finish(() => {
408
+ resolve(result);
409
+ });
410
+ },
411
+ (error) => {
412
+ finish(() => {
413
+ reject(
414
+ error instanceof Error ? error : new Error("JSON frame source failed", {
415
+ cause: error
416
+ })
417
+ );
418
+ });
419
+ }
420
+ );
421
+ });
422
+ }
423
+ function createJsonFrameReader(source, options) {
424
+ validateMaxPayloadBytes(options.maxPayloadBytes);
425
+ assertPositiveInteger(options.idleTimeoutMs, "idleTimeoutMs");
426
+ assertPositiveInteger(options.totalTimeoutMs, "totalTimeoutMs");
427
+ return (async function* readFrames() {
428
+ const iterator = source[Symbol.asyncIterator]();
429
+ let buffered = new Uint8Array(0);
430
+ let expectedPayloadBytes;
431
+ let idleDeadline = Date.now() + options.idleTimeoutMs;
432
+ let totalDeadline;
433
+ let abortCalled = false;
434
+ const abortOnce = async (error) => {
435
+ if (abortCalled) return;
436
+ abortCalled = true;
437
+ await Promise.resolve(options.abort?.(error)).catch(() => void 0);
438
+ };
439
+ try {
440
+ for (; ; ) {
441
+ if (expectedPayloadBytes === void 0 && buffered.byteLength >= HEADER_BYTES) {
442
+ expectedPayloadBytes = new DataView(
443
+ buffered.buffer,
444
+ buffered.byteOffset,
445
+ HEADER_BYTES
446
+ ).getUint32(0, false);
447
+ buffered = buffered.slice(HEADER_BYTES);
448
+ totalDeadline ??= Date.now() + options.totalTimeoutMs;
449
+ if (expectedPayloadBytes > options.maxPayloadBytes) {
450
+ throw new JsonFrameError(
451
+ "FRAME_TOO_LARGE",
452
+ `JSON frame payload exceeds ${options.maxPayloadBytes} bytes`
453
+ );
454
+ }
455
+ }
456
+ if (expectedPayloadBytes !== void 0 && buffered.byteLength >= expectedPayloadBytes) {
457
+ const payload = buffered.slice(0, expectedPayloadBytes);
458
+ buffered = buffered.slice(expectedPayloadBytes);
459
+ expectedPayloadBytes = void 0;
460
+ totalDeadline = buffered.byteLength > 0 ? Date.now() + options.totalTimeoutMs : void 0;
461
+ let text;
462
+ try {
463
+ text = new TextDecoder("utf-8", { fatal: true }).decode(payload);
464
+ } catch (error) {
465
+ throw new JsonFrameError("INVALID_UTF8", "JSON frame payload is not valid UTF-8", {
466
+ cause: error
467
+ });
468
+ }
469
+ let value;
470
+ try {
471
+ value = JSON.parse(text);
472
+ } catch (error) {
473
+ throw new JsonFrameError("INVALID_JSON", "JSON frame payload is not valid JSON", {
474
+ cause: error
475
+ });
476
+ }
477
+ yield value;
478
+ idleDeadline = Date.now() + options.idleTimeoutMs;
479
+ continue;
480
+ }
481
+ const result = await readNextChunk(
482
+ iterator,
483
+ idleDeadline,
484
+ totalDeadline,
485
+ options.signal
486
+ );
487
+ if (result.done) {
488
+ if (buffered.byteLength > 0 || expectedPayloadBytes !== void 0) {
489
+ throw new JsonFrameError("TRUNCATED_FRAME", "JSON frame stream ended mid-frame");
490
+ }
491
+ return;
492
+ }
493
+ if (!(result.value instanceof Uint8Array)) {
494
+ throw new TypeError("JSON frame source must yield Uint8Array chunks");
495
+ }
496
+ if (result.value.byteLength === 0) continue;
497
+ const now = Date.now();
498
+ idleDeadline = now + options.idleTimeoutMs;
499
+ totalDeadline ??= now + options.totalTimeoutMs;
500
+ buffered = concatBytes(buffered, result.value);
501
+ }
502
+ } catch (error) {
503
+ const frameError = error instanceof JsonFrameError ? error : new JsonFrameError("ABORTED", "JSON frame source failed", { cause: error });
504
+ await abortOnce(frameError);
505
+ throw frameError;
506
+ } finally {
507
+ void iterator.return?.().catch(() => void 0);
508
+ }
509
+ })();
510
+ }
511
+
77
512
  // src/device-network/deviceOrchestrationTransport.ts
78
- var DEVICE_ORCHESTRATION_PROTOCOL = "/memeloop/orchestration/1.0.0";
513
+ var DEVICE_ORCHESTRATION_PROTOCOL = "/memeloop/orchestration/2.0.0";
79
514
  var DEFAULT_MAX_FRAME_BYTES = 1024 * 1024;
515
+ var REQUEST_IDLE_TIMEOUT_MS = 1e4;
516
+ var REQUEST_TOTAL_TIMEOUT_MS = 3e4;
517
+ var WATCH_IDLE_TIMEOUT_MS = 9e4;
518
+ var WATCH_BOOKMARK_INTERVAL_MS = 3e4;
80
519
  function orchestrationTransportError(code, message) {
81
520
  return new OrchestrationError({
82
521
  code,
@@ -89,65 +528,6 @@ function assertFrameLimit(limit) {
89
528
  throw new TypeError("maxFrameBytes must be a safe integer of at least 256");
90
529
  }
91
530
  }
92
- async function* encodeLines(values, maxFrameBytes) {
93
- const encoder = new TextEncoder();
94
- for await (const value of values) {
95
- const frame = encoder.encode(`${JSON.stringify(value)}
96
- `);
97
- if (frame.byteLength > maxFrameBytes) {
98
- throw orchestrationTransportError(
99
- "EXHAUSTED",
100
- `device orchestration frame exceeds ${maxFrameBytes} bytes`
101
- );
102
- }
103
- yield frame;
104
- }
105
- }
106
- async function* decodeLines(source, maxFrameBytes) {
107
- const decoder = new TextDecoder();
108
- let buffered = "";
109
- for await (const chunk of source) {
110
- buffered += decoder.decode(chunk, { stream: true });
111
- for (; ; ) {
112
- const newline = buffered.indexOf("\n");
113
- if (newline < 0) break;
114
- const line = buffered.slice(0, newline);
115
- buffered = buffered.slice(newline + 1);
116
- if (!line) continue;
117
- if (new TextEncoder().encode(line).byteLength > maxFrameBytes) {
118
- throw orchestrationTransportError(
119
- "EXHAUSTED",
120
- `device orchestration frame exceeds ${maxFrameBytes} bytes`
121
- );
122
- }
123
- try {
124
- yield JSON.parse(line);
125
- } catch {
126
- throw orchestrationTransportError(
127
- "INVALID",
128
- "device orchestration stream contains invalid JSON"
129
- );
130
- }
131
- }
132
- if (new TextEncoder().encode(buffered).byteLength > maxFrameBytes) {
133
- throw orchestrationTransportError(
134
- "EXHAUSTED",
135
- `device orchestration frame exceeds ${maxFrameBytes} bytes`
136
- );
137
- }
138
- }
139
- buffered += decoder.decode();
140
- if (buffered.trim()) {
141
- try {
142
- yield JSON.parse(buffered);
143
- } catch {
144
- throw orchestrationTransportError(
145
- "INVALID",
146
- "device orchestration stream contains invalid trailing JSON"
147
- );
148
- }
149
- }
150
- }
151
531
  function throwIfAborted(signal) {
152
532
  if (signal?.aborted) {
153
533
  throw orchestrationTransportError("CANCELLED", "device orchestration request was cancelled");
@@ -166,11 +546,41 @@ async function openAuthenticatedStream(options) {
166
546
  }
167
547
  async function writeRequest(stream, request, grant, maxFrameBytes) {
168
548
  const envelope = {
169
- type: "memeloop-device-orchestration-request-v1",
549
+ type: "memeloop-device-orchestration-request-v2",
170
550
  request,
171
551
  ...grant ? { grant } : {}
172
552
  };
173
- await stream.sink(encodeLines([envelope], maxFrameBytes));
553
+ await stream.sink(encodeJsonFrames([envelope], maxFrameBytes));
554
+ }
555
+ function abortStreamOnce(stream) {
556
+ let aborted = false;
557
+ return async (error) => {
558
+ if (aborted) return;
559
+ aborted = true;
560
+ await stream.abort(error);
561
+ };
562
+ }
563
+ function frameReader(stream, maxFrameBytes, idleTimeoutMs, signal, abort) {
564
+ return createJsonFrameReader(stream.source, {
565
+ maxPayloadBytes: maxFrameBytes,
566
+ idleTimeoutMs,
567
+ totalTimeoutMs: REQUEST_TOTAL_TIMEOUT_MS,
568
+ signal,
569
+ abort
570
+ });
571
+ }
572
+ function mapFrameError(error) {
573
+ if (!(error instanceof JsonFrameError)) throw error;
574
+ if (error.code === "ABORTED") {
575
+ throw orchestrationTransportError("CANCELLED", error.message);
576
+ }
577
+ if (error.code === "FRAME_TOO_LARGE") {
578
+ throw orchestrationTransportError("EXHAUSTED", error.message);
579
+ }
580
+ if (error.code === "IDLE_TIMEOUT" || error.code === "TOTAL_TIMEOUT") {
581
+ throw orchestrationTransportError("UNAVAILABLE", error.message);
582
+ }
583
+ throw orchestrationTransportError("INVALID", error.message);
174
584
  }
175
585
  function createDeviceOrchestrationTransport(options) {
176
586
  const maxFrameBytes = options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES;
@@ -179,8 +589,9 @@ function createDeviceOrchestrationTransport(options) {
179
589
  async request(request, transportOptions = {}) {
180
590
  throwIfAborted(transportOptions.signal);
181
591
  const { stream, grant } = await openAuthenticatedStream(options);
592
+ const abort = abortStreamOnce(stream);
182
593
  const closeOnAbort = () => {
183
- void stream.close();
594
+ void abort(orchestrationTransportError("CANCELLED", "device orchestration request was cancelled"));
184
595
  };
185
596
  transportOptions.signal?.addEventListener("abort", closeOnAbort, {
186
597
  once: true
@@ -188,7 +599,13 @@ function createDeviceOrchestrationTransport(options) {
188
599
  try {
189
600
  await writeRequest(stream, request, grant, maxFrameBytes);
190
601
  let response;
191
- for await (const value of decodeLines(stream.source, maxFrameBytes)) {
602
+ for await (const value of frameReader(
603
+ stream,
604
+ maxFrameBytes,
605
+ REQUEST_IDLE_TIMEOUT_MS,
606
+ transportOptions.signal,
607
+ abort
608
+ )) {
192
609
  if (response) {
193
610
  throw orchestrationTransportError(
194
611
  "INVALID",
@@ -205,6 +622,9 @@ function createDeviceOrchestrationTransport(options) {
205
622
  );
206
623
  }
207
624
  return response;
625
+ } catch (error) {
626
+ if (error instanceof JsonFrameError) await abort(error);
627
+ mapFrameError(error);
208
628
  } finally {
209
629
  transportOptions.signal?.removeEventListener("abort", closeOnAbort);
210
630
  await stream.close().catch(() => void 0);
@@ -213,19 +633,29 @@ function createDeviceOrchestrationTransport(options) {
213
633
  async *watch(request, transportOptions = {}) {
214
634
  throwIfAborted(transportOptions.signal);
215
635
  const { stream, grant } = await openAuthenticatedStream(options);
636
+ const abort = abortStreamOnce(stream);
216
637
  const closeOnAbort = () => {
217
- void stream.close();
638
+ void abort(orchestrationTransportError("CANCELLED", "device orchestration watch was cancelled"));
218
639
  };
219
640
  transportOptions.signal?.addEventListener("abort", closeOnAbort, {
220
641
  once: true
221
642
  });
222
643
  try {
223
644
  await writeRequest(stream, request, grant, maxFrameBytes);
224
- for await (const value of decodeLines(stream.source, maxFrameBytes)) {
645
+ for await (const value of frameReader(
646
+ stream,
647
+ maxFrameBytes,
648
+ WATCH_IDLE_TIMEOUT_MS,
649
+ transportOptions.signal,
650
+ abort
651
+ )) {
225
652
  throwIfAborted(transportOptions.signal);
226
653
  yield value;
227
654
  }
228
655
  throwIfAborted(transportOptions.signal);
656
+ } catch (error) {
657
+ if (error instanceof JsonFrameError) await abort(error);
658
+ mapFrameError(error);
229
659
  } finally {
230
660
  transportOptions.signal?.removeEventListener("abort", closeOnAbort);
231
661
  await stream.close().catch(() => void 0);
@@ -234,15 +664,22 @@ function createDeviceOrchestrationTransport(options) {
234
664
  };
235
665
  }
236
666
  async function readSingleRequest(stream, maxFrameBytes) {
667
+ const abort = abortStreamOnce(stream);
237
668
  let envelope;
238
- for await (const value of decodeLines(stream.source, maxFrameBytes)) {
669
+ for await (const value of frameReader(
670
+ stream,
671
+ maxFrameBytes,
672
+ REQUEST_IDLE_TIMEOUT_MS,
673
+ void 0,
674
+ abort
675
+ )) {
239
676
  if (envelope) {
240
677
  throw orchestrationTransportError(
241
678
  "INVALID",
242
679
  "device orchestration stream accepts exactly one request"
243
680
  );
244
681
  }
245
- if (value === null || typeof value !== "object" || Array.isArray(value) || value.type !== "memeloop-device-orchestration-request-v1" || value.request === null || typeof value.request !== "object") {
682
+ if (value === null || typeof value !== "object" || Array.isArray(value) || value.type !== "memeloop-device-orchestration-request-v2" || value.request === null || typeof value.request !== "object") {
246
683
  throw orchestrationTransportError(
247
684
  "INVALID",
248
685
  "device orchestration request envelope is invalid"
@@ -255,6 +692,41 @@ async function readSingleRequest(stream, maxFrameBytes) {
255
692
  }
256
693
  return envelope;
257
694
  }
695
+ async function* withWatchBookmarks(source, request, signal) {
696
+ const iterator = source[Symbol.asyncIterator]();
697
+ let pending = iterator.next();
698
+ try {
699
+ for (; ; ) {
700
+ const result = await Promise.race([
701
+ pending.then((value) => ({ kind: "value", value })),
702
+ new Promise((resolve) => {
703
+ const timer = setTimeout(() => {
704
+ resolve({ kind: "bookmark" });
705
+ }, WATCH_BOOKMARK_INTERVAL_MS);
706
+ signal.addEventListener("abort", () => {
707
+ clearTimeout(timer);
708
+ resolve({ kind: "bookmark" });
709
+ }, { once: true });
710
+ })
711
+ ]);
712
+ if (signal.aborted) return;
713
+ if (result.kind === "bookmark") {
714
+ yield {
715
+ protocol: request.protocol,
716
+ requestId: request.requestId,
717
+ ok: true,
718
+ result: { type: "BOOKMARK", resourceVersion: String(Date.now()) }
719
+ };
720
+ continue;
721
+ }
722
+ if (result.value.done) return;
723
+ yield result.value.value;
724
+ pending = iterator.next();
725
+ }
726
+ } finally {
727
+ void iterator.return?.();
728
+ }
729
+ }
258
730
  function createDeviceOrchestrationStreamHandler(options) {
259
731
  const maxFrameBytes = options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES;
260
732
  assertFrameLimit(maxFrameBytes);
@@ -269,10 +741,13 @@ function createDeviceOrchestrationStreamHandler(options) {
269
741
  const handler = await options.resolveHandler(remotePeerId);
270
742
  if (request.operation === "watch") {
271
743
  await stream.sink(
272
- encodeLines(handler.watch(request, { signal: abort.signal }), maxFrameBytes)
744
+ encodeJsonFrames(
745
+ withWatchBookmarks(handler.watch(request, { signal: abort.signal }), request, abort.signal),
746
+ maxFrameBytes
747
+ )
273
748
  );
274
749
  } else {
275
- await stream.sink(encodeLines([await handler.request(request)], maxFrameBytes));
750
+ await stream.sink(encodeJsonFrames([await handler.request(request)], maxFrameBytes));
276
751
  }
277
752
  } finally {
278
753
  abort.abort();
@@ -281,9 +756,23 @@ function createDeviceOrchestrationStreamHandler(options) {
281
756
  };
282
757
  }
283
758
 
759
+ // src/device-network/deviceSyncStateStore.ts
760
+ var MemoryDeviceSyncStateStore = class {
761
+ versionVector;
762
+ constructor(initial = {}) {
763
+ this.versionVector = { ...initial };
764
+ }
765
+ async loadVersionVector() {
766
+ return { ...this.versionVector };
767
+ }
768
+ async saveVersionVector(versionVector) {
769
+ this.versionVector = { ...versionVector };
770
+ }
771
+ };
772
+
284
773
  // src/device-network/libp2pSyncProtocol.ts
285
- var LIBP2P_SYNC_REQUEST_TYPE = "memeloop-sync-request-v1";
286
- var LIBP2P_SYNC_RESPONSE_TYPE = "memeloop-sync-response-v1";
774
+ var LIBP2P_SYNC_REQUEST_TYPE = "memeloop-sync-request-v2";
775
+ var LIBP2P_SYNC_RESPONSE_TYPE = "memeloop-sync-response-v2";
287
776
  var syncMethods = /* @__PURE__ */ new Set([
288
777
  "exchangeVersionVector",
289
778
  "pullMissingMetadata",
@@ -343,8 +832,11 @@ var Libp2pDeviceSyncTransport = class {
343
832
  exchangeVersionVector(peerId, localVersion) {
344
833
  return this.request(peerId, "exchangeVersionVector", { localVersion });
345
834
  }
346
- pullMissingMetadata(peerId, sinceVersion) {
347
- return this.request(peerId, "pullMissingMetadata", { sinceVersion });
835
+ pullMissingMetadata(peerId, sinceVersion, cursor) {
836
+ return this.request(peerId, "pullMissingMetadata", {
837
+ sinceVersion,
838
+ cursor
839
+ });
348
840
  }
349
841
  pullMissingMessages(peerId, conversationId, knownMessageIds) {
350
842
  return this.request(peerId, "pullMissingMessages", { conversationId, knownMessageIds });
@@ -355,7 +847,7 @@ var Libp2pDeviceSyncTransport = class {
355
847
  }
356
848
  async request(peerId, method, parameters) {
357
849
  const grant = await this.grantProvider?.(peerId);
358
- const stream = await this.deviceNetwork.openStream(peerId, "/memeloop/sync/1.0.0", grant);
850
+ const stream = await this.deviceNetwork.openStream(peerId, "/memeloop/sync/2.0.0", grant);
359
851
  const request = {
360
852
  type: LIBP2P_SYNC_REQUEST_TYPE,
361
853
  id: crypto.randomUUID(),
@@ -363,31 +855,34 @@ var Libp2pDeviceSyncTransport = class {
363
855
  params: parameters,
364
856
  grant
365
857
  };
366
- await writeJsonToStream(stream, request);
367
- const response = await readJsonFromStream(stream);
368
- await stream.close();
369
- if (!isLibp2pSyncResponse(response)) throw new Error("invalid_sync_response");
370
- if (response.id !== request.id) throw new Error("sync_response_id_mismatch");
371
- if (!response.ok) throw new Error(response.error);
372
- return response.result;
858
+ try {
859
+ await stream.sink(encodeJsonFrames([request], 16 * 1024 * 1024));
860
+ let response;
861
+ for await (const value of createJsonFrameReader(stream.source, {
862
+ maxPayloadBytes: 16 * 1024 * 1024,
863
+ idleTimeoutMs: 15e3,
864
+ totalTimeoutMs: 12e4,
865
+ abort: async (error) => stream.abort(error)
866
+ })) {
867
+ if (response !== void 0) throw new Error("sync_response_multiple");
868
+ response = value;
869
+ }
870
+ if (!isLibp2pSyncResponse(response)) throw new Error("invalid_sync_response");
871
+ if (response.id !== request.id) throw new Error("sync_response_id_mismatch");
872
+ if (!response.ok) throw new Error(response.error);
873
+ return response.result;
874
+ } catch (error) {
875
+ if (error instanceof JsonFrameError) await stream.abort(error);
876
+ throw error;
877
+ } finally {
878
+ await stream.close().catch(() => void 0);
879
+ }
373
880
  }
374
881
  };
375
- async function writeJsonToStream(stream, message) {
376
- const payload = new TextEncoder().encode(JSON.stringify(message));
377
- await stream.sink((async function* () {
378
- yield payload;
379
- })());
380
- }
381
- async function readJsonFromStream(stream) {
382
- const reader = stream.source[Symbol.asyncIterator]();
383
- const result = await reader.next();
384
- if (result.done || !result.value) throw new Error("sync_response_missing");
385
- return JSON.parse(new TextDecoder().decode(result.value));
386
- }
387
882
 
388
883
  // src/device-network/libp2pRpcProtocol.ts
389
- var LIBP2P_RPC_REQUEST_TYPE = "memeloop-rpc-request-v1";
390
- var LIBP2P_RPC_RESPONSE_TYPE = "memeloop-rpc-response-v1";
884
+ var LIBP2P_RPC_REQUEST_TYPE = "memeloop-rpc-request-v2";
885
+ var LIBP2P_RPC_RESPONSE_TYPE = "memeloop-rpc-response-v2";
391
886
  function isLibp2pRpcRequest(value) {
392
887
  if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
393
888
  const record = value;
@@ -402,7 +897,7 @@ function isLibp2pRpcResponse(value) {
402
897
  }
403
898
 
404
899
  // src/device-network/localTrustDeviceAuthorizer.ts
405
- var PAIRING_PROTOCOL = "/memeloop/pairing/1.0.0";
900
+ var PAIRING_PROTOCOL = "/memeloop/pairing/2.0.0";
406
901
  var LocalTrustDeviceAuthorizer = class {
407
902
  constructor(options = {}) {
408
903
  this.options = options;
@@ -428,7 +923,7 @@ var LocalTrustDeviceAuthorizer = class {
428
923
  };
429
924
 
430
925
  // src/device-network/pairingInvite.ts
431
- var DEVICE_PAIRING_INVITE_PROTOCOL = "memeloop-device-pairing-v1";
926
+ var DEVICE_PAIRING_INVITE_PROTOCOL = "memeloop-device-pairing-v2";
432
927
  var DEVICE_PAIRING_INVITE_TTL_MS = 5 * 6e4;
433
928
  var MAX_INVITE_LENGTH = 16 * 1024;
434
929
  var MAX_ADDRESSES = 8;
@@ -439,37 +934,82 @@ function requireText(value, name, maxLength) {
439
934
  }
440
935
  return trimmed;
441
936
  }
442
- function validateAddresses(addresses) {
937
+ function finalPeerId(address) {
938
+ const parts = address.split("/");
939
+ for (let index = parts.length - 2; index >= 0; index -= 1) {
940
+ if (parts[index] === "p2p") return parts[index + 1];
941
+ }
942
+ return void 0;
943
+ }
944
+ function validateAddresses(addresses, peerId) {
443
945
  const unique = [...new Set(addresses.map((address) => address.trim()))].filter(Boolean);
444
946
  if (unique.length === 0 || unique.length > MAX_ADDRESSES || unique.some(
445
- (address) => address.length > 2048 || !address.startsWith("/") || !address.includes("/ws") && !address.includes("/wss")
947
+ (address) => address.length > 2048 || !address.startsWith("/") || !address.includes("/ws") && !address.includes("/wss") || finalPeerId(address) !== peerId
446
948
  )) {
447
- throw new Error("device pairing invite requires WebSocket multiaddrs");
949
+ throw new Error("device pairing invite requires PeerId-bound WebSocket multiaddrs");
448
950
  }
449
951
  return unique;
450
952
  }
451
- function createDevicePairingInvite(device, options = {}) {
953
+ function validateLifetime(createdAt, expiresAt, now) {
954
+ if (!Number.isSafeInteger(createdAt) || !Number.isSafeInteger(expiresAt) || expiresAt <= createdAt || expiresAt - createdAt > DEVICE_PAIRING_INVITE_TTL_MS) {
955
+ throw new Error("invalid device pairing invite lifetime");
956
+ }
957
+ if (expiresAt <= now) throw new Error("device pairing invite has expired");
958
+ }
959
+ function unsignedPayload(invite) {
960
+ const peerId = requireText(invite.peerId, "peerId", 256);
961
+ return {
962
+ protocol: DEVICE_PAIRING_INVITE_PROTOCOL,
963
+ peerId,
964
+ publicKeyMultibase: requireText(invite.publicKeyMultibase, "publicKeyMultibase", 4096),
965
+ deviceName: requireText(invite.deviceName, "deviceName", 256),
966
+ multiaddrs: [...validateAddresses(invite.multiaddrs, peerId)].sort(),
967
+ createdAt: invite.createdAt,
968
+ expiresAt: invite.expiresAt
969
+ };
970
+ }
971
+ function canonicalDevicePairingInviteBytes(invite) {
972
+ const payload = unsignedPayload(invite);
973
+ const canonical = [
974
+ DEVICE_PAIRING_INVITE_PROTOCOL,
975
+ `peerId=${payload.peerId}`,
976
+ `publicKey=${payload.publicKeyMultibase}`,
977
+ `deviceName=${payload.deviceName}`,
978
+ `multiaddrs=${payload.multiaddrs.join(",")}`,
979
+ `createdAt=${payload.createdAt}`,
980
+ `expiresAt=${payload.expiresAt}`
981
+ ].join("\n");
982
+ return new TextEncoder().encode(canonical);
983
+ }
984
+ async function createDevicePairingInvite(device, options) {
452
985
  const createdAt = options.now ?? Date.now();
453
986
  const ttlMs = options.ttlMs ?? DEVICE_PAIRING_INVITE_TTL_MS;
454
- if (!Number.isSafeInteger(createdAt) || !Number.isSafeInteger(ttlMs) || ttlMs <= 0 || ttlMs > DEVICE_PAIRING_INVITE_TTL_MS) {
987
+ if (!Number.isSafeInteger(createdAt) || !Number.isSafeInteger(ttlMs) || ttlMs <= 0) {
455
988
  throw new Error("invalid device pairing invite lifetime");
456
989
  }
457
- return {
990
+ const payload = unsignedPayload({
458
991
  protocol: DEVICE_PAIRING_INVITE_PROTOCOL,
459
- peerId: requireText(device.peerId, "peerId", 256),
460
- deviceName: requireText(device.displayName, "deviceName", 256),
461
- multiaddrs: validateAddresses(device.multiaddrs ?? []),
992
+ peerId: device.peerId,
993
+ publicKeyMultibase: device.publicKeyMultibase,
994
+ deviceName: device.displayName,
995
+ multiaddrs: device.multiaddrs ?? [],
462
996
  createdAt,
463
997
  expiresAt: createdAt + ttlMs
464
- };
998
+ });
999
+ validateLifetime(payload.createdAt, payload.expiresAt, createdAt - 1);
1000
+ const signature = requireText(
1001
+ await options.sign(canonicalDevicePairingInviteBytes(payload)),
1002
+ "signature",
1003
+ 4096
1004
+ );
1005
+ return { ...payload, signature };
465
1006
  }
466
1007
  function encodeDevicePairingInvite(invite) {
467
- const validated = parseDevicePairingInvite(JSON.stringify(invite), {
468
- now: invite.createdAt
469
- });
470
- return JSON.stringify(validated);
1008
+ const payload = unsignedPayload(invite);
1009
+ const signature = requireText(invite.signature, "signature", 4096);
1010
+ return JSON.stringify({ ...payload, signature });
471
1011
  }
472
- function parseDevicePairingInvite(serialized, options = {}) {
1012
+ async function parseDevicePairingInvite(serialized, options) {
473
1013
  if (!serialized || serialized.length > MAX_INVITE_LENGTH) {
474
1014
  throw new Error("invalid device pairing invite length");
475
1015
  }
@@ -483,20 +1023,30 @@ function parseDevicePairingInvite(serialized, options = {}) {
483
1023
  throw new Error("invalid device pairing invite");
484
1024
  }
485
1025
  const record = parsed;
486
- if (record.protocol !== DEVICE_PAIRING_INVITE_PROTOCOL || typeof record.peerId !== "string" || typeof record.deviceName !== "string" || !Array.isArray(record.multiaddrs) || record.multiaddrs.some((address) => typeof address !== "string") || typeof record.createdAt !== "number" || typeof record.expiresAt !== "number" || !Number.isSafeInteger(record.createdAt) || !Number.isSafeInteger(record.expiresAt) || record.expiresAt <= record.createdAt || record.expiresAt - record.createdAt > DEVICE_PAIRING_INVITE_TTL_MS) {
1026
+ if (record.protocol !== DEVICE_PAIRING_INVITE_PROTOCOL || typeof record.peerId !== "string" || typeof record.publicKeyMultibase !== "string" || typeof record.deviceName !== "string" || !Array.isArray(record.multiaddrs) || record.multiaddrs.some((address) => typeof address !== "string") || typeof record.createdAt !== "number" || typeof record.expiresAt !== "number" || typeof record.signature !== "string") {
487
1027
  throw new Error("invalid device pairing invite");
488
1028
  }
489
- if (record.expiresAt <= (options.now ?? Date.now())) {
490
- throw new Error("device pairing invite has expired");
491
- }
492
- return {
1029
+ validateLifetime(record.createdAt, record.expiresAt, options.now ?? Date.now());
1030
+ const payload = unsignedPayload({
493
1031
  protocol: DEVICE_PAIRING_INVITE_PROTOCOL,
494
- peerId: requireText(record.peerId, "peerId", 256),
495
- deviceName: requireText(record.deviceName, "deviceName", 256),
496
- multiaddrs: validateAddresses(record.multiaddrs),
1032
+ peerId: record.peerId,
1033
+ publicKeyMultibase: record.publicKeyMultibase,
1034
+ deviceName: record.deviceName,
1035
+ multiaddrs: record.multiaddrs,
497
1036
  createdAt: record.createdAt,
498
1037
  expiresAt: record.expiresAt
1038
+ });
1039
+ const invite = {
1040
+ ...payload,
1041
+ signature: requireText(record.signature, "signature", 4096)
499
1042
  };
1043
+ if (!await options.verifyIdentity({
1044
+ invite,
1045
+ payload: canonicalDevicePairingInviteBytes(payload)
1046
+ })) {
1047
+ throw new Error("device pairing invite identity verification failed");
1048
+ }
1049
+ return invite;
500
1050
  }
501
1051
 
502
1052
  // src/sync/attachmentRpcCodec.ts
@@ -525,43 +1075,66 @@ var ChatSyncEngine = class {
525
1075
  nodeId;
526
1076
  storage;
527
1077
  getPeers;
1078
+ stateStore;
528
1079
  versionVector = {};
1080
+ loadStatePromise;
1081
+ stateMutation = Promise.resolve();
529
1082
  constructor(options) {
530
1083
  this.nodeId = options.nodeId;
531
1084
  this.storage = options.storage;
532
1085
  this.getPeers = options.peers;
533
- this.versionVector[this.nodeId] = 0;
1086
+ this.stateStore = options.stateStore ?? new MemoryDeviceSyncStateStore();
534
1087
  }
535
1088
  /**
536
1089
  * 在本节点新产生一条消息时调用,增加本节点的 lamportClock。
537
1090
  */
538
1091
  bumpLocalVersion() {
539
- this.versionVector[this.nodeId] = (this.versionVector[this.nodeId] ?? 0) + 1;
1092
+ let nextClock = 0;
1093
+ const mutation = this.stateMutation.then(async () => {
1094
+ await this.ensureStateLoaded();
1095
+ nextClock = (this.versionVector[this.nodeId] ?? 0) + 1;
1096
+ this.versionVector[this.nodeId] = nextClock;
1097
+ await this.stateStore.saveVersionVector(this.versionVector);
1098
+ });
1099
+ this.stateMutation = mutation.catch(() => void 0);
1100
+ return mutation.then(() => nextClock);
540
1101
  }
541
1102
  /**
542
1103
  * 执行一次与所有 peer 的增量同步(metadata 级别),并尽力拉取缺失消息。
543
1104
  */
544
1105
  async syncOnce() {
1106
+ await this.ensureStateLoaded();
1107
+ await this.stateMutation;
1108
+ await this.reconcileVersionVectorFromStorage();
545
1109
  const peers = this.getPeers();
546
1110
  if (peers.length === 0) return;
547
- const localVersion = { ...this.versionVector };
548
1111
  const pulledConversations = /* @__PURE__ */ new Set();
549
1112
  for (const peer of peers) {
550
- const { remoteVersion, missingForRemote } = await peer.exchangeVersionVector(localVersion);
551
- const missingForLocal = await peer.pullMissingMetadata(this.versionVector);
552
- for (const meta of missingForLocal) {
553
- await this.storage.upsertConversationMetadata(meta);
554
- pulledConversations.add(meta.conversationId);
555
- }
1113
+ const { remoteVersion } = await peer.exchangeVersionVector({ ...this.versionVector });
1114
+ let cursor;
1115
+ do {
1116
+ const page = await peer.pullMissingMetadata(this.versionVector, cursor);
1117
+ for (const meta of page.items) {
1118
+ await this.storage.upsertConversationMetadata(meta);
1119
+ pulledConversations.add(meta.conversationId);
1120
+ }
1121
+ cursor = page.nextCursor;
1122
+ } while (cursor !== void 0);
556
1123
  for (const [nodeId, clock] of Object.entries(remoteVersion)) {
557
1124
  const current = this.versionVector[nodeId] ?? 0;
558
- if (clock > current) {
559
- this.versionVector[nodeId] = clock;
560
- }
1125
+ if (clock > current) this.versionVector[nodeId] = clock;
561
1126
  }
562
- if (missingForRemote.length > 0) {
563
- this.bumpLocalVersion();
1127
+ await this.stateStore.saveVersionVector(this.versionVector);
1128
+ }
1129
+ const pageSize = 100;
1130
+ let offset = 0;
1131
+ for (; ; ) {
1132
+ const conversations = await this.storage.listConversations({ limit: pageSize, offset });
1133
+ for (const conversation of conversations) {
1134
+ pulledConversations.add(conversation.conversationId);
564
1135
  }
1136
+ if (conversations.length < pageSize) break;
1137
+ offset += conversations.length;
565
1138
  }
566
1139
  for (const conversationId of pulledConversations) {
567
1140
  await this.pullMessagesForConversationFromPeers(conversationId, peers);
@@ -572,11 +1145,6 @@ var ChatSyncEngine = class {
572
1145
  */
573
1146
  async antiEntropyOnce() {
574
1147
  await this.syncOnce();
575
- const list = await this.storage.listConversations({ limit: 500 });
576
- const peers = this.getPeers();
577
- for (const meta of list) {
578
- await this.pullMessagesForConversationFromPeers(meta.conversationId, peers);
579
- }
580
1148
  }
581
1149
  async pullMessagesForConversationFromPeers(conversationId, peers) {
582
1150
  const localMsgs = await this.storage.getMessages(conversationId, { mode: "full-content" });
@@ -629,12 +1197,39 @@ var ChatSyncEngine = class {
629
1197
  }
630
1198
  }
631
1199
  }
632
- getVersionVector() {
1200
+ async getVersionVector() {
1201
+ await this.ensureStateLoaded();
1202
+ await this.stateMutation;
633
1203
  return { ...this.versionVector };
634
1204
  }
635
1205
  getStorage() {
636
1206
  return this.storage;
637
1207
  }
1208
+ ensureStateLoaded() {
1209
+ this.loadStatePromise ??= (async () => {
1210
+ const stored = await this.stateStore.loadVersionVector();
1211
+ this.versionVector = { ...stored, [this.nodeId]: stored[this.nodeId] ?? 0 };
1212
+ })();
1213
+ return this.loadStatePromise;
1214
+ }
1215
+ async reconcileVersionVectorFromStorage() {
1216
+ const pageSize = 100;
1217
+ let offset = 0;
1218
+ let changed = false;
1219
+ for (; ; ) {
1220
+ const conversations = await this.storage.listConversations({ limit: pageSize, offset });
1221
+ for (const conversation of conversations) {
1222
+ const current = this.versionVector[conversation.originNodeId] ?? 0;
1223
+ if (conversation.originClock > current) {
1224
+ this.versionVector[conversation.originNodeId] = conversation.originClock;
1225
+ changed = true;
1226
+ }
1227
+ }
1228
+ if (conversations.length < pageSize) break;
1229
+ offset += conversations.length;
1230
+ }
1231
+ if (changed) await this.stateStore.saveVersionVector(this.versionVector);
1232
+ }
638
1233
  };
639
1234
 
640
1235
  // src/sync/peerNodeAdapter.ts
@@ -648,8 +1243,8 @@ var PeerNodeSyncAdapter = class {
648
1243
  exchangeVersionVector(localVersion) {
649
1244
  return this.transport.exchangeVersionVector(this.nodeId, localVersion);
650
1245
  }
651
- pullMissingMetadata(sinceVersion) {
652
- return this.transport.pullMissingMetadata(this.nodeId, sinceVersion);
1246
+ pullMissingMetadata(sinceVersion, cursor) {
1247
+ return this.transport.pullMissingMetadata(this.nodeId, sinceVersion, cursor);
653
1248
  }
654
1249
  pullMissingMessages(conversationId, knownMessageIds) {
655
1250
  const function_ = this.transport.pullMissingMessages?.bind(this.transport);
@@ -671,7 +1266,17 @@ var PeerNodeSyncAdapter = class {
671
1266
  function isConversationMeta(value) {
672
1267
  if (value === null || typeof value !== "object") return false;
673
1268
  const o = value;
674
- return typeof o.conversationId === "string" && typeof o.title === "string" && typeof o.originNodeId === "string" && typeof o.definitionId === "string" && typeof o.messageCount === "number" && typeof o.lastMessageTimestamp === "number" && typeof o.isUserInitiated === "boolean";
1269
+ return typeof o.conversationId === "string" && typeof o.title === "string" && typeof o.originNodeId === "string" && typeof o.originClock === "number" && typeof o.definitionId === "string" && typeof o.messageCount === "number" && typeof o.lastMessageTimestamp === "number" && typeof o.isUserInitiated === "boolean";
1270
+ }
1271
+ function computeMissingVersionRanges(currentVersion, availableVersion) {
1272
+ const ranges = [];
1273
+ for (const [originNodeId, availableClock] of Object.entries(availableVersion)) {
1274
+ const currentClock = currentVersion[originNodeId] ?? 0;
1275
+ if (availableClock > currentClock) {
1276
+ ranges.push({ originNodeId, fromExclusive: currentClock, toInclusive: availableClock });
1277
+ }
1278
+ }
1279
+ return ranges.sort((left, right) => left.originNodeId.localeCompare(right.originNodeId));
675
1280
  }
676
1281
  // Annotate the CommonJS export names for ESM import in node:
677
1282
  0 && (module.exports = {
@@ -679,19 +1284,27 @@ function isConversationMeta(value) {
679
1284
  DEVICE_ORCHESTRATION_PROTOCOL,
680
1285
  DEVICE_PAIRING_INVITE_PROTOCOL,
681
1286
  DEVICE_PAIRING_INVITE_TTL_MS,
1287
+ DeviceCloudConnectionCoordinator,
1288
+ JsonFrameError,
682
1289
  LIBP2P_RPC_REQUEST_TYPE,
683
1290
  LIBP2P_RPC_RESPONSE_TYPE,
684
1291
  LIBP2P_SYNC_REQUEST_TYPE,
685
1292
  LIBP2P_SYNC_RESPONSE_TYPE,
686
1293
  Libp2pDeviceSyncTransport,
687
1294
  LocalTrustDeviceAuthorizer,
1295
+ MemoryDeviceSyncStateStore,
688
1296
  PeerNodeSyncAdapter,
689
1297
  attachmentBlobFromWire,
1298
+ canonicalDevicePairingInviteBytes,
1299
+ computeMissingVersionRanges,
690
1300
  createDeviceOrchestrationStreamHandler,
691
1301
  createDeviceOrchestrationTransport,
692
1302
  createDevicePairingInvite,
1303
+ createJsonFrameReader,
693
1304
  decodeAttachmentBlobRpc,
694
1305
  encodeDevicePairingInvite,
1306
+ encodeJsonFrame,
1307
+ encodeJsonFrames,
695
1308
  isConversationMeta,
696
1309
  isLibp2pRpcRequest,
697
1310
  isLibp2pRpcResponse,