memeloop 0.1.3 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist/browser.cjs +1 -1
  2. package/dist/browser.cjs.map +1 -1
  3. package/dist/browser.d.cts +15 -15
  4. package/dist/browser.d.ts +15 -15
  5. package/dist/browser.js +1 -1
  6. package/dist/{chunk-5JEV7632.js → chunk-5VYZV33O.js} +2 -1
  7. package/dist/{chunk-5JEV7632.js.map → chunk-5VYZV33O.js.map} +1 -1
  8. package/dist/chunk-E5R24U4I.js +1292 -0
  9. package/dist/chunk-E5R24U4I.js.map +1 -0
  10. package/dist/{chunk-MRKY4HGF.js → chunk-EPHRIRAL.js} +20 -11
  11. package/dist/chunk-EPHRIRAL.js.map +1 -0
  12. package/dist/{chunk-P6L3444O.js → chunk-KV6TC5UJ.js} +2 -2
  13. package/dist/chunk-MXRXYGWW.js +1 -0
  14. package/dist/{chunk-CCEFAOSH.js → chunk-WHXFUMDE.js} +2 -2
  15. package/dist/{chunk-CCEFAOSH.js.map → chunk-WHXFUMDE.js.map} +1 -1
  16. package/dist/conversation.d.cts +3 -3
  17. package/dist/conversation.d.ts +3 -3
  18. package/dist/device-network-portable.cjs +814 -144
  19. package/dist/device-network-portable.cjs.map +1 -1
  20. package/dist/device-network-portable.d.cts +24 -14
  21. package/dist/device-network-portable.d.ts +24 -14
  22. package/dist/device-network-portable.js +18 -2
  23. package/dist/device-network.cjs +833 -154
  24. package/dist/device-network.cjs.map +1 -1
  25. package/dist/device-network.d.cts +5 -5
  26. package/dist/device-network.d.ts +5 -5
  27. package/dist/device-network.js +19 -3
  28. package/dist/{fetchProvider-WWnWnQFP.d.ts → fetchProvider-BhqRqi5-.d.ts} +1 -1
  29. package/dist/{fetchProvider-toTHy5Nu.d.cts → fetchProvider-HJ_FkORN.d.cts} +1 -1
  30. package/dist/index.cjs +841 -157
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +16 -16
  33. package/dist/index.d.ts +16 -16
  34. package/dist/index.js +26 -8
  35. package/dist/index.js.map +1 -1
  36. package/dist/{interface-IrKBFtL1.d.ts → interface-3C3eG8Hn.d.cts} +13 -1
  37. package/dist/{interface-IrKBFtL1.d.cts → interface-3C3eG8Hn.d.ts} +13 -1
  38. package/dist/llm-providers.d.cts +2 -2
  39. package/dist/llm-providers.d.ts +2 -2
  40. package/dist/loop-api.cjs +1 -0
  41. package/dist/loop-api.cjs.map +1 -1
  42. package/dist/loop-api.d.cts +7 -7
  43. package/dist/loop-api.d.ts +7 -7
  44. package/dist/loop-api.js +2 -2
  45. package/dist/mobile.d.cts +5 -5
  46. package/dist/mobile.d.ts +5 -5
  47. package/dist/{orchestration-portable-BHB_YrNN.d.ts → orchestration-portable-B4udCSVd.d.ts} +4 -4
  48. package/dist/{orchestration-portable-mvYBvGBb.d.cts → orchestration-portable-BWaCsCGK.d.cts} +4 -4
  49. package/dist/orchestration-portable.cjs +1 -1
  50. package/dist/orchestration-portable.cjs.map +1 -1
  51. package/dist/orchestration-portable.d.cts +5 -5
  52. package/dist/orchestration-portable.d.ts +5 -5
  53. package/dist/orchestration-portable.js +1 -1
  54. package/dist/pairingInvite-DENOiqa0.d.cts +271 -0
  55. package/dist/pairingInvite-DZy77i0p.d.ts +271 -0
  56. package/dist/{promptConcat-0WNFTg4l.d.ts → promptConcat-CUEIA_3T.d.ts} +3 -3
  57. package/dist/{promptConcat-D6wr5V-V.d.cts → promptConcat-VLe5SEN4.d.cts} +3 -3
  58. package/dist/{providerRegistry-CPsktuiX.d.cts → providerRegistry-BDGGs5gz.d.cts} +2 -2
  59. package/dist/{providerRegistry-Bi6pAvSj.d.ts → providerRegistry-CaFK_aUd.d.ts} +2 -2
  60. package/dist/{registry-D0kbxksn.d.cts → registry-DNSEVYug.d.cts} +3 -3
  61. package/dist/{registry-DDbvhlhs.d.ts → registry-yGnYHWhq.d.ts} +3 -3
  62. package/dist/{remoteClient-CBggnRy3.d.cts → remoteClient-DFIyCZEv.d.cts} +2 -2
  63. package/dist/{remoteClient-B_spK019.d.ts → remoteClient-DwBCoHpc.d.ts} +2 -2
  64. package/dist/{runtime-UONPTAYK.js → runtime-6EELHWVB.js} +2 -2
  65. package/dist/{scriptDeploymentPipeline-CXJcC82H.d.ts → scriptDeploymentPipeline-B1q-UhrO.d.ts} +1 -1
  66. package/dist/{scriptDeploymentPipeline-yG1ZhVVI.d.cts → scriptDeploymentPipeline-XJzQyYys.d.cts} +1 -1
  67. package/dist/{syncCloudDevices-Ds_Kcgrv.d.cts → syncCloudDevices-BFw_c1g5.d.cts} +6 -5
  68. package/dist/{syncCloudDevices-BEqwgrhD.d.ts → syncCloudDevices-C8fCUhnX.d.ts} +6 -5
  69. package/dist/{types-CdFVSntj.d.ts → types-C8gnhG9M.d.cts} +6 -4
  70. package/dist/{types-7pYti-k9.d.cts → types-CcFFIk8W.d.ts} +6 -4
  71. package/package.json +1 -1
  72. package/dist/chunk-426CJUZP.js +0 -630
  73. package/dist/chunk-426CJUZP.js.map +0 -1
  74. package/dist/chunk-MRKY4HGF.js.map +0 -1
  75. package/dist/chunk-VZKQX5PP.js +0 -1
  76. package/dist/pairingInvite-BXPWY3aM.d.cts +0 -149
  77. package/dist/pairingInvite-Cuz0wuq9.d.ts +0 -149
  78. /package/dist/{chunk-P6L3444O.js.map → chunk-KV6TC5UJ.js.map} +0 -0
  79. /package/dist/{chunk-VZKQX5PP.js.map → chunk-MXRXYGWW.js.map} +0 -0
  80. /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,228 @@ __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
+ const stoppedController = this.controller;
126
+ stoppedController.abort(new Error("device cloud coordinator stopped"));
127
+ await this.inFlight?.catch(() => void 0);
128
+ if (this.controller === stoppedController) {
129
+ this.controller = new AbortController();
130
+ }
131
+ }
132
+ async setConfiguration(configuration) {
133
+ this.configuration = configuration;
134
+ this.generation += 1;
135
+ this.consecutiveFailures = 0;
136
+ this.clearTimer();
137
+ this.controller.abort(new Error("device cloud configuration changed"));
138
+ this.controller = new AbortController();
139
+ this.inFlight = void 0;
140
+ this.resetSnapshot();
141
+ if (this.started && this.options.adapter.isConfigured(configuration)) await this.runNow();
142
+ }
143
+ runNow() {
144
+ if (this.inFlight) return this.inFlight;
145
+ if (!this.options.adapter.isConfigured(this.configuration)) {
146
+ this.resetSnapshot();
147
+ return Promise.resolve();
148
+ }
149
+ const generation = this.generation;
150
+ const configuration = this.configuration;
151
+ const signal = this.controller.signal;
152
+ const run = this.maintain(configuration, generation, signal);
153
+ const tracked = run.finally(() => {
154
+ if (this.inFlight === tracked) this.inFlight = void 0;
155
+ });
156
+ this.inFlight = tracked;
157
+ return tracked;
158
+ }
159
+ async maintain(configuration, generation, signal) {
160
+ if (!this.isCurrent(generation, signal)) return;
161
+ await this.updateSnapshot(generation, {
162
+ status: "connecting",
163
+ lastError: void 0,
164
+ nextRetryAt: void 0
165
+ });
166
+ try {
167
+ if (this.snapshotValue.components.authorizer !== "ready") {
168
+ await this.runStep("authorizer", generation, signal, () => this.options.adapter.ensureAuthorizer(configuration, signal));
169
+ }
170
+ if (this.snapshotValue.components.registration !== "ready") {
171
+ await this.runStep("registration", generation, signal, () => this.options.adapter.registerDevice(configuration, signal));
172
+ }
173
+ let relayError;
174
+ try {
175
+ await this.runStep("relay", generation, signal, () => this.options.adapter.ensureRelay(configuration, signal));
176
+ } catch (error) {
177
+ relayError = error;
178
+ this.options.logWarning?.("Cloud relay maintenance failed", error);
179
+ }
180
+ await this.runStep("heartbeat", generation, signal, () => this.options.adapter.heartbeat(configuration, signal));
181
+ let directoryError;
182
+ try {
183
+ await this.runStep("directory", generation, signal, () => this.options.adapter.syncDirectory(configuration, signal));
184
+ } catch (error) {
185
+ directoryError = error;
186
+ this.options.logWarning?.("Cloud directory synchronization failed", error);
187
+ }
188
+ if (!this.isCurrent(generation, signal)) return;
189
+ this.consecutiveFailures = 0;
190
+ const relayRequired = this.options.adapter.relayRequiredForOnline(configuration);
191
+ await this.updateSnapshot(generation, {
192
+ status: directoryError || relayRequired && relayError ? "degraded" : "online",
193
+ lastError: directoryError ?? relayError,
194
+ nextRetryAt: this.now() + this.heartbeatIntervalMs
195
+ });
196
+ this.schedule(this.heartbeatIntervalMs, generation);
197
+ } catch (error) {
198
+ if (!this.isCurrent(generation, signal)) return;
199
+ this.consecutiveFailures += 1;
200
+ if (this.snapshotValue.components.heartbeat === "failed") {
201
+ this.snapshotValue.components.registration = "not-run";
202
+ }
203
+ const status = this.options.adapter.classifyError?.(error) ?? "offline";
204
+ const delay = this.backoffDelay();
205
+ await this.updateSnapshot(generation, {
206
+ status,
207
+ lastError: error,
208
+ nextRetryAt: this.now() + delay
209
+ });
210
+ this.schedule(delay, generation);
211
+ throw error;
212
+ }
213
+ }
214
+ async runStep(component, generation, signal, operation) {
215
+ if (!this.isCurrent(generation, signal)) return;
216
+ await this.setComponent(component, "pending", generation);
217
+ try {
218
+ const result = await operation();
219
+ if (!this.isCurrent(generation, signal)) return;
220
+ await result?.commit?.();
221
+ if (!this.isCurrent(generation, signal)) return;
222
+ await this.setComponent(component, "ready", generation);
223
+ } catch (error) {
224
+ if (this.isCurrent(generation, signal)) await this.setComponent(component, "failed", generation);
225
+ throw error;
226
+ }
227
+ }
228
+ backoffDelay() {
229
+ const exponential = Math.min(
230
+ this.maxBackoffMs,
231
+ this.initialBackoffMs * 2 ** Math.max(0, this.consecutiveFailures - 1)
232
+ );
233
+ const jitter = exponential * this.jitterRatio * (this.random() * 2 - 1);
234
+ return Math.max(0, Math.round(exponential + jitter));
235
+ }
236
+ schedule(delayMs, generation) {
237
+ if (!this.started || generation !== this.generation) return;
238
+ this.clearTimer();
239
+ this.timer = setTimeout(() => {
240
+ if (generation !== this.generation) return;
241
+ void this.runNow().catch((error) => {
242
+ this.options.logWarning?.("Cloud maintenance failed", error);
243
+ });
244
+ }, delayMs);
245
+ }
246
+ clearTimer() {
247
+ if (this.timer) clearTimeout(this.timer);
248
+ this.timer = void 0;
249
+ }
250
+ isCurrent(generation, signal) {
251
+ return generation === this.generation && !signal.aborted;
252
+ }
253
+ async setComponent(component, status, generation) {
254
+ if (generation !== this.generation) return;
255
+ this.snapshotValue = {
256
+ ...this.snapshotValue,
257
+ components: { ...this.snapshotValue.components, [component]: status }
258
+ };
259
+ await this.emit();
260
+ }
261
+ async updateSnapshot(generation, update) {
262
+ if (generation !== this.generation) return;
263
+ this.snapshotValue = { ...this.snapshotValue, ...update };
264
+ await this.emit();
265
+ }
266
+ resetSnapshot() {
267
+ this.snapshotValue = {
268
+ status: this.options.adapter.isConfigured(this.configuration) ? "offline" : "not-configured",
269
+ generation: this.generation,
270
+ components: emptyComponents()
271
+ };
272
+ void this.emit();
273
+ }
274
+ async emit() {
275
+ await this.options.onStatus?.(this.snapshot);
276
+ }
277
+ };
278
+
49
279
  // src/orchestration/errors.ts
50
280
  var OrchestrationError = class extends Error {
51
281
  code;
@@ -74,9 +304,222 @@ var OrchestrationError = class extends Error {
74
304
  }
75
305
  };
76
306
 
307
+ // src/device-network/jsonFrame.ts
308
+ var JsonFrameError = class extends Error {
309
+ code;
310
+ constructor(code, message, options) {
311
+ super(message, options);
312
+ this.name = "JsonFrameError";
313
+ this.code = code;
314
+ }
315
+ };
316
+ var HEADER_BYTES = 4;
317
+ var MAX_UINT32 = 4294967295;
318
+ function assertPositiveInteger(value, name) {
319
+ if (!Number.isSafeInteger(value) || value <= 0) {
320
+ throw new TypeError(`${name} must be a positive safe integer`);
321
+ }
322
+ }
323
+ function validateMaxPayloadBytes(maxPayloadBytes) {
324
+ assertPositiveInteger(maxPayloadBytes, "maxPayloadBytes");
325
+ if (maxPayloadBytes > MAX_UINT32) {
326
+ throw new TypeError("maxPayloadBytes must fit in an unsigned 32-bit frame length");
327
+ }
328
+ }
329
+ function encodeJsonFrame(value, maxPayloadBytes = MAX_UINT32) {
330
+ validateMaxPayloadBytes(maxPayloadBytes);
331
+ let serialized;
332
+ try {
333
+ serialized = JSON.stringify(value);
334
+ } catch (error) {
335
+ throw new JsonFrameError("INVALID_JSON", "JSON frame value cannot be serialized", {
336
+ cause: error
337
+ });
338
+ }
339
+ if (serialized === void 0) {
340
+ throw new JsonFrameError("INVALID_JSON", "JSON frame value cannot be serialized");
341
+ }
342
+ const payload = new TextEncoder().encode(serialized);
343
+ if (payload.byteLength > maxPayloadBytes) {
344
+ throw new JsonFrameError(
345
+ "FRAME_TOO_LARGE",
346
+ `JSON frame payload exceeds ${maxPayloadBytes} bytes`
347
+ );
348
+ }
349
+ const frame = new Uint8Array(HEADER_BYTES + payload.byteLength);
350
+ new DataView(frame.buffer, frame.byteOffset, HEADER_BYTES).setUint32(0, payload.byteLength, false);
351
+ frame.set(payload, HEADER_BYTES);
352
+ return frame;
353
+ }
354
+ async function* encodeJsonFrames(values, maxPayloadBytes = MAX_UINT32) {
355
+ for await (const value of values) {
356
+ yield encodeJsonFrame(value, maxPayloadBytes);
357
+ }
358
+ }
359
+ function concatBytes(left, right) {
360
+ if (left.byteLength === 0) return right.slice();
361
+ if (right.byteLength === 0) return left;
362
+ const combined = new Uint8Array(left.byteLength + right.byteLength);
363
+ combined.set(left);
364
+ combined.set(right, left.byteLength);
365
+ return combined;
366
+ }
367
+ function abortedError(signal) {
368
+ return new JsonFrameError("ABORTED", "JSON frame reading was aborted", {
369
+ cause: signal?.reason
370
+ });
371
+ }
372
+ async function readNextChunk(iterator, idleDeadline, totalDeadline, signal) {
373
+ if (signal?.aborted) throw abortedError(signal);
374
+ const now = Date.now();
375
+ const deadline = totalDeadline === void 0 ? idleDeadline : Math.min(idleDeadline, totalDeadline);
376
+ if (deadline <= now) {
377
+ throw new JsonFrameError(
378
+ totalDeadline !== void 0 && totalDeadline <= idleDeadline ? "TOTAL_TIMEOUT" : "IDLE_TIMEOUT",
379
+ totalDeadline !== void 0 && totalDeadline <= idleDeadline ? "JSON frame total timeout expired" : "JSON frame idle timeout expired"
380
+ );
381
+ }
382
+ return new Promise((resolve, reject) => {
383
+ let settled = false;
384
+ const finish = (callback) => {
385
+ if (settled) return;
386
+ settled = true;
387
+ clearTimeout(timer);
388
+ signal?.removeEventListener("abort", onAbort);
389
+ callback();
390
+ };
391
+ const onAbort = () => {
392
+ finish(() => {
393
+ reject(abortedError(signal));
394
+ });
395
+ };
396
+ const timer = setTimeout(() => {
397
+ const currentTime = Date.now();
398
+ const totalExpired = totalDeadline !== void 0 && currentTime >= totalDeadline;
399
+ finish(() => {
400
+ reject(
401
+ new JsonFrameError(
402
+ totalExpired ? "TOTAL_TIMEOUT" : "IDLE_TIMEOUT",
403
+ totalExpired ? "JSON frame total timeout expired" : "JSON frame idle timeout expired"
404
+ )
405
+ );
406
+ });
407
+ }, Math.max(1, deadline - now));
408
+ signal?.addEventListener("abort", onAbort, { once: true });
409
+ void iterator.next().then(
410
+ (result) => {
411
+ finish(() => {
412
+ resolve(result);
413
+ });
414
+ },
415
+ (error) => {
416
+ finish(() => {
417
+ reject(
418
+ error instanceof Error ? error : new Error("JSON frame source failed", {
419
+ cause: error
420
+ })
421
+ );
422
+ });
423
+ }
424
+ );
425
+ });
426
+ }
427
+ function createJsonFrameReader(source, options) {
428
+ validateMaxPayloadBytes(options.maxPayloadBytes);
429
+ assertPositiveInteger(options.idleTimeoutMs, "idleTimeoutMs");
430
+ assertPositiveInteger(options.totalTimeoutMs, "totalTimeoutMs");
431
+ return (async function* readFrames() {
432
+ const iterator = source[Symbol.asyncIterator]();
433
+ let buffered = new Uint8Array(0);
434
+ let expectedPayloadBytes;
435
+ let idleDeadline = Date.now() + options.idleTimeoutMs;
436
+ let totalDeadline;
437
+ let abortCalled = false;
438
+ const abortOnce = async (error) => {
439
+ if (abortCalled) return;
440
+ abortCalled = true;
441
+ await Promise.resolve(options.abort?.(error)).catch(() => void 0);
442
+ };
443
+ try {
444
+ for (; ; ) {
445
+ if (expectedPayloadBytes === void 0 && buffered.byteLength >= HEADER_BYTES) {
446
+ expectedPayloadBytes = new DataView(
447
+ buffered.buffer,
448
+ buffered.byteOffset,
449
+ HEADER_BYTES
450
+ ).getUint32(0, false);
451
+ buffered = buffered.slice(HEADER_BYTES);
452
+ totalDeadline ??= Date.now() + options.totalTimeoutMs;
453
+ if (expectedPayloadBytes > options.maxPayloadBytes) {
454
+ throw new JsonFrameError(
455
+ "FRAME_TOO_LARGE",
456
+ `JSON frame payload exceeds ${options.maxPayloadBytes} bytes`
457
+ );
458
+ }
459
+ }
460
+ if (expectedPayloadBytes !== void 0 && buffered.byteLength >= expectedPayloadBytes) {
461
+ const payload = buffered.slice(0, expectedPayloadBytes);
462
+ buffered = buffered.slice(expectedPayloadBytes);
463
+ expectedPayloadBytes = void 0;
464
+ totalDeadline = buffered.byteLength > 0 ? Date.now() + options.totalTimeoutMs : void 0;
465
+ let text;
466
+ try {
467
+ text = new TextDecoder("utf-8", { fatal: true }).decode(payload);
468
+ } catch (error) {
469
+ throw new JsonFrameError("INVALID_UTF8", "JSON frame payload is not valid UTF-8", {
470
+ cause: error
471
+ });
472
+ }
473
+ let value;
474
+ try {
475
+ value = JSON.parse(text);
476
+ } catch (error) {
477
+ throw new JsonFrameError("INVALID_JSON", "JSON frame payload is not valid JSON", {
478
+ cause: error
479
+ });
480
+ }
481
+ yield value;
482
+ idleDeadline = Date.now() + options.idleTimeoutMs;
483
+ continue;
484
+ }
485
+ const result = await readNextChunk(
486
+ iterator,
487
+ idleDeadline,
488
+ totalDeadline,
489
+ options.signal
490
+ );
491
+ if (result.done) {
492
+ if (buffered.byteLength > 0 || expectedPayloadBytes !== void 0) {
493
+ throw new JsonFrameError("TRUNCATED_FRAME", "JSON frame stream ended mid-frame");
494
+ }
495
+ return;
496
+ }
497
+ if (!(result.value instanceof Uint8Array)) {
498
+ throw new TypeError("JSON frame source must yield Uint8Array chunks");
499
+ }
500
+ if (result.value.byteLength === 0) continue;
501
+ const now = Date.now();
502
+ idleDeadline = now + options.idleTimeoutMs;
503
+ totalDeadline ??= now + options.totalTimeoutMs;
504
+ buffered = concatBytes(buffered, result.value);
505
+ }
506
+ } catch (error) {
507
+ const frameError = error instanceof JsonFrameError ? error : new JsonFrameError("ABORTED", "JSON frame source failed", { cause: error });
508
+ await abortOnce(frameError);
509
+ throw frameError;
510
+ } finally {
511
+ void iterator.return?.().catch(() => void 0);
512
+ }
513
+ })();
514
+ }
515
+
77
516
  // src/device-network/deviceOrchestrationTransport.ts
78
- var DEVICE_ORCHESTRATION_PROTOCOL = "/memeloop/orchestration/1.0.0";
517
+ var DEVICE_ORCHESTRATION_PROTOCOL = "/memeloop/orchestration/2.0.0";
79
518
  var DEFAULT_MAX_FRAME_BYTES = 1024 * 1024;
519
+ var REQUEST_IDLE_TIMEOUT_MS = 1e4;
520
+ var REQUEST_TOTAL_TIMEOUT_MS = 3e4;
521
+ var WATCH_IDLE_TIMEOUT_MS = 9e4;
522
+ var WATCH_BOOKMARK_INTERVAL_MS = 3e4;
80
523
  function orchestrationTransportError(code, message) {
81
524
  return new OrchestrationError({
82
525
  code,
@@ -89,65 +532,6 @@ function assertFrameLimit(limit) {
89
532
  throw new TypeError("maxFrameBytes must be a safe integer of at least 256");
90
533
  }
91
534
  }
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
535
  function throwIfAborted(signal) {
152
536
  if (signal?.aborted) {
153
537
  throw orchestrationTransportError("CANCELLED", "device orchestration request was cancelled");
@@ -166,11 +550,41 @@ async function openAuthenticatedStream(options) {
166
550
  }
167
551
  async function writeRequest(stream, request, grant, maxFrameBytes) {
168
552
  const envelope = {
169
- type: "memeloop-device-orchestration-request-v1",
553
+ type: "memeloop-device-orchestration-request-v2",
170
554
  request,
171
555
  ...grant ? { grant } : {}
172
556
  };
173
- await stream.sink(encodeLines([envelope], maxFrameBytes));
557
+ await stream.sink(encodeJsonFrames([envelope], maxFrameBytes));
558
+ }
559
+ function abortStreamOnce(stream) {
560
+ let aborted = false;
561
+ return async (error) => {
562
+ if (aborted) return;
563
+ aborted = true;
564
+ await stream.abort(error);
565
+ };
566
+ }
567
+ function frameReader(stream, maxFrameBytes, idleTimeoutMs, signal, abort) {
568
+ return createJsonFrameReader(stream.source, {
569
+ maxPayloadBytes: maxFrameBytes,
570
+ idleTimeoutMs,
571
+ totalTimeoutMs: REQUEST_TOTAL_TIMEOUT_MS,
572
+ signal,
573
+ abort
574
+ });
575
+ }
576
+ function mapFrameError(error) {
577
+ if (!(error instanceof JsonFrameError)) throw error;
578
+ if (error.code === "ABORTED") {
579
+ throw orchestrationTransportError("CANCELLED", error.message);
580
+ }
581
+ if (error.code === "FRAME_TOO_LARGE") {
582
+ throw orchestrationTransportError("EXHAUSTED", error.message);
583
+ }
584
+ if (error.code === "IDLE_TIMEOUT" || error.code === "TOTAL_TIMEOUT") {
585
+ throw orchestrationTransportError("UNAVAILABLE", error.message);
586
+ }
587
+ throw orchestrationTransportError("INVALID", error.message);
174
588
  }
175
589
  function createDeviceOrchestrationTransport(options) {
176
590
  const maxFrameBytes = options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES;
@@ -179,8 +593,9 @@ function createDeviceOrchestrationTransport(options) {
179
593
  async request(request, transportOptions = {}) {
180
594
  throwIfAborted(transportOptions.signal);
181
595
  const { stream, grant } = await openAuthenticatedStream(options);
596
+ const abort = abortStreamOnce(stream);
182
597
  const closeOnAbort = () => {
183
- void stream.close();
598
+ void abort(orchestrationTransportError("CANCELLED", "device orchestration request was cancelled"));
184
599
  };
185
600
  transportOptions.signal?.addEventListener("abort", closeOnAbort, {
186
601
  once: true
@@ -188,7 +603,13 @@ function createDeviceOrchestrationTransport(options) {
188
603
  try {
189
604
  await writeRequest(stream, request, grant, maxFrameBytes);
190
605
  let response;
191
- for await (const value of decodeLines(stream.source, maxFrameBytes)) {
606
+ for await (const value of frameReader(
607
+ stream,
608
+ maxFrameBytes,
609
+ REQUEST_IDLE_TIMEOUT_MS,
610
+ transportOptions.signal,
611
+ abort
612
+ )) {
192
613
  if (response) {
193
614
  throw orchestrationTransportError(
194
615
  "INVALID",
@@ -205,6 +626,9 @@ function createDeviceOrchestrationTransport(options) {
205
626
  );
206
627
  }
207
628
  return response;
629
+ } catch (error) {
630
+ if (error instanceof JsonFrameError) await abort(error);
631
+ mapFrameError(error);
208
632
  } finally {
209
633
  transportOptions.signal?.removeEventListener("abort", closeOnAbort);
210
634
  await stream.close().catch(() => void 0);
@@ -213,19 +637,29 @@ function createDeviceOrchestrationTransport(options) {
213
637
  async *watch(request, transportOptions = {}) {
214
638
  throwIfAborted(transportOptions.signal);
215
639
  const { stream, grant } = await openAuthenticatedStream(options);
640
+ const abort = abortStreamOnce(stream);
216
641
  const closeOnAbort = () => {
217
- void stream.close();
642
+ void abort(orchestrationTransportError("CANCELLED", "device orchestration watch was cancelled"));
218
643
  };
219
644
  transportOptions.signal?.addEventListener("abort", closeOnAbort, {
220
645
  once: true
221
646
  });
222
647
  try {
223
648
  await writeRequest(stream, request, grant, maxFrameBytes);
224
- for await (const value of decodeLines(stream.source, maxFrameBytes)) {
649
+ for await (const value of frameReader(
650
+ stream,
651
+ maxFrameBytes,
652
+ WATCH_IDLE_TIMEOUT_MS,
653
+ transportOptions.signal,
654
+ abort
655
+ )) {
225
656
  throwIfAborted(transportOptions.signal);
226
657
  yield value;
227
658
  }
228
659
  throwIfAborted(transportOptions.signal);
660
+ } catch (error) {
661
+ if (error instanceof JsonFrameError) await abort(error);
662
+ mapFrameError(error);
229
663
  } finally {
230
664
  transportOptions.signal?.removeEventListener("abort", closeOnAbort);
231
665
  await stream.close().catch(() => void 0);
@@ -234,15 +668,22 @@ function createDeviceOrchestrationTransport(options) {
234
668
  };
235
669
  }
236
670
  async function readSingleRequest(stream, maxFrameBytes) {
671
+ const abort = abortStreamOnce(stream);
237
672
  let envelope;
238
- for await (const value of decodeLines(stream.source, maxFrameBytes)) {
673
+ for await (const value of frameReader(
674
+ stream,
675
+ maxFrameBytes,
676
+ REQUEST_IDLE_TIMEOUT_MS,
677
+ void 0,
678
+ abort
679
+ )) {
239
680
  if (envelope) {
240
681
  throw orchestrationTransportError(
241
682
  "INVALID",
242
683
  "device orchestration stream accepts exactly one request"
243
684
  );
244
685
  }
245
- if (value === null || typeof value !== "object" || Array.isArray(value) || value.type !== "memeloop-device-orchestration-request-v1" || value.request === null || typeof value.request !== "object") {
686
+ if (value === null || typeof value !== "object" || Array.isArray(value) || value.type !== "memeloop-device-orchestration-request-v2" || value.request === null || typeof value.request !== "object") {
246
687
  throw orchestrationTransportError(
247
688
  "INVALID",
248
689
  "device orchestration request envelope is invalid"
@@ -255,6 +696,94 @@ async function readSingleRequest(stream, maxFrameBytes) {
255
696
  }
256
697
  return envelope;
257
698
  }
699
+ async function* withWatchBookmarks(source, request, signal) {
700
+ const iterator = source[Symbol.asyncIterator]();
701
+ let activity;
702
+ let activityWaiter;
703
+ let bookmarkTimer;
704
+ let readInFlight = false;
705
+ let lastResourceVersion = watchResourceVersion(request.payload.options) ?? "0";
706
+ const clearBookmarkTimer = () => {
707
+ if (bookmarkTimer !== void 0) clearTimeout(bookmarkTimer);
708
+ bookmarkTimer = void 0;
709
+ };
710
+ const publishActivity = (value) => {
711
+ if (activityWaiter) {
712
+ const resolve = activityWaiter;
713
+ activityWaiter = void 0;
714
+ clearBookmarkTimer();
715
+ resolve(value);
716
+ return;
717
+ }
718
+ activity = value;
719
+ };
720
+ const readNext = () => {
721
+ if (readInFlight) return;
722
+ readInFlight = true;
723
+ void iterator.next().then(
724
+ (value) => {
725
+ readInFlight = false;
726
+ publishActivity({ kind: "value", value });
727
+ },
728
+ (error) => {
729
+ readInFlight = false;
730
+ publishActivity({ kind: "error", error });
731
+ }
732
+ );
733
+ };
734
+ const waitForActivity = () => {
735
+ if (activity) {
736
+ const value = activity;
737
+ activity = void 0;
738
+ return Promise.resolve(value);
739
+ }
740
+ if (signal.aborted) return Promise.resolve({ kind: "aborted" });
741
+ return new Promise((resolve) => {
742
+ activityWaiter = resolve;
743
+ bookmarkTimer = setTimeout(() => {
744
+ bookmarkTimer = void 0;
745
+ activityWaiter = void 0;
746
+ resolve({ kind: "bookmark" });
747
+ }, WATCH_BOOKMARK_INTERVAL_MS);
748
+ });
749
+ };
750
+ const onAbort = () => {
751
+ publishActivity({ kind: "aborted" });
752
+ };
753
+ signal.addEventListener("abort", onAbort, { once: true });
754
+ readNext();
755
+ try {
756
+ for (; ; ) {
757
+ const result = await waitForActivity();
758
+ if (result.kind === "aborted") return;
759
+ if (result.kind === "bookmark") {
760
+ yield {
761
+ protocol: request.protocol,
762
+ requestId: request.requestId,
763
+ ok: true,
764
+ result: { type: "BOOKMARK", resourceVersion: lastResourceVersion }
765
+ };
766
+ continue;
767
+ }
768
+ if (result.kind === "error") throw result.error;
769
+ if (result.value.done) return;
770
+ const response = result.value.value;
771
+ lastResourceVersion = response.ok ? watchResourceVersion(response.result) ?? lastResourceVersion : lastResourceVersion;
772
+ yield response;
773
+ readNext();
774
+ }
775
+ } finally {
776
+ clearBookmarkTimer();
777
+ activityWaiter = void 0;
778
+ signal.removeEventListener("abort", onAbort);
779
+ void iterator.return?.();
780
+ }
781
+ }
782
+ function watchResourceVersion(value) {
783
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
784
+ const resourceVersion = value.resourceVersion;
785
+ return typeof resourceVersion === "string" && resourceVersion.length > 0 ? resourceVersion : void 0;
786
+ }
258
787
  function createDeviceOrchestrationStreamHandler(options) {
259
788
  const maxFrameBytes = options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES;
260
789
  assertFrameLimit(maxFrameBytes);
@@ -269,10 +798,13 @@ function createDeviceOrchestrationStreamHandler(options) {
269
798
  const handler = await options.resolveHandler(remotePeerId);
270
799
  if (request.operation === "watch") {
271
800
  await stream.sink(
272
- encodeLines(handler.watch(request, { signal: abort.signal }), maxFrameBytes)
801
+ encodeJsonFrames(
802
+ withWatchBookmarks(handler.watch(request, { signal: abort.signal }), request, abort.signal),
803
+ maxFrameBytes
804
+ )
273
805
  );
274
806
  } else {
275
- await stream.sink(encodeLines([await handler.request(request)], maxFrameBytes));
807
+ await stream.sink(encodeJsonFrames([await handler.request(request)], maxFrameBytes));
276
808
  }
277
809
  } finally {
278
810
  abort.abort();
@@ -281,9 +813,23 @@ function createDeviceOrchestrationStreamHandler(options) {
281
813
  };
282
814
  }
283
815
 
816
+ // src/device-network/deviceSyncStateStore.ts
817
+ var MemoryDeviceSyncStateStore = class {
818
+ versionVector;
819
+ constructor(initial = {}) {
820
+ this.versionVector = { ...initial };
821
+ }
822
+ async loadVersionVector() {
823
+ return { ...this.versionVector };
824
+ }
825
+ async saveVersionVector(versionVector) {
826
+ this.versionVector = { ...versionVector };
827
+ }
828
+ };
829
+
284
830
  // 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";
831
+ var LIBP2P_SYNC_REQUEST_TYPE = "memeloop-sync-request-v2";
832
+ var LIBP2P_SYNC_RESPONSE_TYPE = "memeloop-sync-response-v2";
287
833
  var syncMethods = /* @__PURE__ */ new Set([
288
834
  "exchangeVersionVector",
289
835
  "pullMissingMetadata",
@@ -343,8 +889,11 @@ var Libp2pDeviceSyncTransport = class {
343
889
  exchangeVersionVector(peerId, localVersion) {
344
890
  return this.request(peerId, "exchangeVersionVector", { localVersion });
345
891
  }
346
- pullMissingMetadata(peerId, sinceVersion) {
347
- return this.request(peerId, "pullMissingMetadata", { sinceVersion });
892
+ pullMissingMetadata(peerId, sinceVersion, cursor) {
893
+ return this.request(peerId, "pullMissingMetadata", {
894
+ sinceVersion,
895
+ cursor
896
+ });
348
897
  }
349
898
  pullMissingMessages(peerId, conversationId, knownMessageIds) {
350
899
  return this.request(peerId, "pullMissingMessages", { conversationId, knownMessageIds });
@@ -355,7 +904,7 @@ var Libp2pDeviceSyncTransport = class {
355
904
  }
356
905
  async request(peerId, method, parameters) {
357
906
  const grant = await this.grantProvider?.(peerId);
358
- const stream = await this.deviceNetwork.openStream(peerId, "/memeloop/sync/1.0.0", grant);
907
+ const stream = await this.deviceNetwork.openStream(peerId, "/memeloop/sync/2.0.0", grant);
359
908
  const request = {
360
909
  type: LIBP2P_SYNC_REQUEST_TYPE,
361
910
  id: crypto.randomUUID(),
@@ -363,31 +912,34 @@ var Libp2pDeviceSyncTransport = class {
363
912
  params: parameters,
364
913
  grant
365
914
  };
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;
915
+ try {
916
+ await stream.sink(encodeJsonFrames([request], 16 * 1024 * 1024));
917
+ let response;
918
+ for await (const value of createJsonFrameReader(stream.source, {
919
+ maxPayloadBytes: 16 * 1024 * 1024,
920
+ idleTimeoutMs: 15e3,
921
+ totalTimeoutMs: 12e4,
922
+ abort: async (error) => stream.abort(error)
923
+ })) {
924
+ if (response !== void 0) throw new Error("sync_response_multiple");
925
+ response = value;
926
+ }
927
+ if (!isLibp2pSyncResponse(response)) throw new Error("invalid_sync_response");
928
+ if (response.id !== request.id) throw new Error("sync_response_id_mismatch");
929
+ if (!response.ok) throw new Error(response.error);
930
+ return response.result;
931
+ } catch (error) {
932
+ if (error instanceof JsonFrameError) await stream.abort(error);
933
+ throw error;
934
+ } finally {
935
+ await stream.close().catch(() => void 0);
936
+ }
373
937
  }
374
938
  };
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
939
 
388
940
  // 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";
941
+ var LIBP2P_RPC_REQUEST_TYPE = "memeloop-rpc-request-v2";
942
+ var LIBP2P_RPC_RESPONSE_TYPE = "memeloop-rpc-response-v2";
391
943
  function isLibp2pRpcRequest(value) {
392
944
  if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
393
945
  const record = value;
@@ -402,7 +954,7 @@ function isLibp2pRpcResponse(value) {
402
954
  }
403
955
 
404
956
  // src/device-network/localTrustDeviceAuthorizer.ts
405
- var PAIRING_PROTOCOL = "/memeloop/pairing/1.0.0";
957
+ var PAIRING_PROTOCOL = "/memeloop/pairing/2.0.0";
406
958
  var LocalTrustDeviceAuthorizer = class {
407
959
  constructor(options = {}) {
408
960
  this.options = options;
@@ -428,7 +980,7 @@ var LocalTrustDeviceAuthorizer = class {
428
980
  };
429
981
 
430
982
  // src/device-network/pairingInvite.ts
431
- var DEVICE_PAIRING_INVITE_PROTOCOL = "memeloop-device-pairing-v1";
983
+ var DEVICE_PAIRING_INVITE_PROTOCOL = "memeloop-device-pairing-v2";
432
984
  var DEVICE_PAIRING_INVITE_TTL_MS = 5 * 6e4;
433
985
  var MAX_INVITE_LENGTH = 16 * 1024;
434
986
  var MAX_ADDRESSES = 8;
@@ -439,37 +991,82 @@ function requireText(value, name, maxLength) {
439
991
  }
440
992
  return trimmed;
441
993
  }
442
- function validateAddresses(addresses) {
994
+ function finalPeerId(address) {
995
+ const parts = address.split("/");
996
+ for (let index = parts.length - 2; index >= 0; index -= 1) {
997
+ if (parts[index] === "p2p") return parts[index + 1];
998
+ }
999
+ return void 0;
1000
+ }
1001
+ function validateAddresses(addresses, peerId) {
443
1002
  const unique = [...new Set(addresses.map((address) => address.trim()))].filter(Boolean);
444
1003
  if (unique.length === 0 || unique.length > MAX_ADDRESSES || unique.some(
445
- (address) => address.length > 2048 || !address.startsWith("/") || !address.includes("/ws") && !address.includes("/wss")
1004
+ (address) => address.length > 2048 || !address.startsWith("/") || !address.includes("/ws") && !address.includes("/wss") || finalPeerId(address) !== peerId
446
1005
  )) {
447
- throw new Error("device pairing invite requires WebSocket multiaddrs");
1006
+ throw new Error("device pairing invite requires PeerId-bound WebSocket multiaddrs");
448
1007
  }
449
1008
  return unique;
450
1009
  }
451
- function createDevicePairingInvite(device, options = {}) {
1010
+ function validateLifetime(createdAt, expiresAt, now) {
1011
+ if (!Number.isSafeInteger(createdAt) || !Number.isSafeInteger(expiresAt) || expiresAt <= createdAt || expiresAt - createdAt > DEVICE_PAIRING_INVITE_TTL_MS) {
1012
+ throw new Error("invalid device pairing invite lifetime");
1013
+ }
1014
+ if (expiresAt <= now) throw new Error("device pairing invite has expired");
1015
+ }
1016
+ function unsignedPayload(invite) {
1017
+ const peerId = requireText(invite.peerId, "peerId", 256);
1018
+ return {
1019
+ protocol: DEVICE_PAIRING_INVITE_PROTOCOL,
1020
+ peerId,
1021
+ publicKeyMultibase: requireText(invite.publicKeyMultibase, "publicKeyMultibase", 4096),
1022
+ deviceName: requireText(invite.deviceName, "deviceName", 256),
1023
+ multiaddrs: [...validateAddresses(invite.multiaddrs, peerId)].sort(),
1024
+ createdAt: invite.createdAt,
1025
+ expiresAt: invite.expiresAt
1026
+ };
1027
+ }
1028
+ function canonicalDevicePairingInviteBytes(invite) {
1029
+ const payload = unsignedPayload(invite);
1030
+ const canonical = [
1031
+ DEVICE_PAIRING_INVITE_PROTOCOL,
1032
+ `peerId=${payload.peerId}`,
1033
+ `publicKey=${payload.publicKeyMultibase}`,
1034
+ `deviceName=${payload.deviceName}`,
1035
+ `multiaddrs=${payload.multiaddrs.join(",")}`,
1036
+ `createdAt=${payload.createdAt}`,
1037
+ `expiresAt=${payload.expiresAt}`
1038
+ ].join("\n");
1039
+ return new TextEncoder().encode(canonical);
1040
+ }
1041
+ async function createDevicePairingInvite(device, options) {
452
1042
  const createdAt = options.now ?? Date.now();
453
1043
  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) {
1044
+ if (!Number.isSafeInteger(createdAt) || !Number.isSafeInteger(ttlMs) || ttlMs <= 0) {
455
1045
  throw new Error("invalid device pairing invite lifetime");
456
1046
  }
457
- return {
1047
+ const payload = unsignedPayload({
458
1048
  protocol: DEVICE_PAIRING_INVITE_PROTOCOL,
459
- peerId: requireText(device.peerId, "peerId", 256),
460
- deviceName: requireText(device.displayName, "deviceName", 256),
461
- multiaddrs: validateAddresses(device.multiaddrs ?? []),
1049
+ peerId: device.peerId,
1050
+ publicKeyMultibase: device.publicKeyMultibase,
1051
+ deviceName: device.displayName,
1052
+ multiaddrs: device.multiaddrs ?? [],
462
1053
  createdAt,
463
1054
  expiresAt: createdAt + ttlMs
464
- };
1055
+ });
1056
+ validateLifetime(payload.createdAt, payload.expiresAt, createdAt - 1);
1057
+ const signature = requireText(
1058
+ await options.sign(canonicalDevicePairingInviteBytes(payload)),
1059
+ "signature",
1060
+ 4096
1061
+ );
1062
+ return { ...payload, signature };
465
1063
  }
466
1064
  function encodeDevicePairingInvite(invite) {
467
- const validated = parseDevicePairingInvite(JSON.stringify(invite), {
468
- now: invite.createdAt
469
- });
470
- return JSON.stringify(validated);
1065
+ const payload = unsignedPayload(invite);
1066
+ const signature = requireText(invite.signature, "signature", 4096);
1067
+ return JSON.stringify({ ...payload, signature });
471
1068
  }
472
- function parseDevicePairingInvite(serialized, options = {}) {
1069
+ async function parseDevicePairingInvite(serialized, options) {
473
1070
  if (!serialized || serialized.length > MAX_INVITE_LENGTH) {
474
1071
  throw new Error("invalid device pairing invite length");
475
1072
  }
@@ -483,20 +1080,30 @@ function parseDevicePairingInvite(serialized, options = {}) {
483
1080
  throw new Error("invalid device pairing invite");
484
1081
  }
485
1082
  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) {
1083
+ 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
1084
  throw new Error("invalid device pairing invite");
488
1085
  }
489
- if (record.expiresAt <= (options.now ?? Date.now())) {
490
- throw new Error("device pairing invite has expired");
491
- }
492
- return {
1086
+ validateLifetime(record.createdAt, record.expiresAt, options.now ?? Date.now());
1087
+ const payload = unsignedPayload({
493
1088
  protocol: DEVICE_PAIRING_INVITE_PROTOCOL,
494
- peerId: requireText(record.peerId, "peerId", 256),
495
- deviceName: requireText(record.deviceName, "deviceName", 256),
496
- multiaddrs: validateAddresses(record.multiaddrs),
1089
+ peerId: record.peerId,
1090
+ publicKeyMultibase: record.publicKeyMultibase,
1091
+ deviceName: record.deviceName,
1092
+ multiaddrs: record.multiaddrs,
497
1093
  createdAt: record.createdAt,
498
1094
  expiresAt: record.expiresAt
1095
+ });
1096
+ const invite = {
1097
+ ...payload,
1098
+ signature: requireText(record.signature, "signature", 4096)
499
1099
  };
1100
+ if (!await options.verifyIdentity({
1101
+ invite,
1102
+ payload: canonicalDevicePairingInviteBytes(payload)
1103
+ })) {
1104
+ throw new Error("device pairing invite identity verification failed");
1105
+ }
1106
+ return invite;
500
1107
  }
501
1108
 
502
1109
  // src/sync/attachmentRpcCodec.ts
@@ -525,43 +1132,66 @@ var ChatSyncEngine = class {
525
1132
  nodeId;
526
1133
  storage;
527
1134
  getPeers;
1135
+ stateStore;
528
1136
  versionVector = {};
1137
+ loadStatePromise;
1138
+ stateMutation = Promise.resolve();
529
1139
  constructor(options) {
530
1140
  this.nodeId = options.nodeId;
531
1141
  this.storage = options.storage;
532
1142
  this.getPeers = options.peers;
533
- this.versionVector[this.nodeId] = 0;
1143
+ this.stateStore = options.stateStore ?? new MemoryDeviceSyncStateStore();
534
1144
  }
535
1145
  /**
536
1146
  * 在本节点新产生一条消息时调用,增加本节点的 lamportClock。
537
1147
  */
538
1148
  bumpLocalVersion() {
539
- this.versionVector[this.nodeId] = (this.versionVector[this.nodeId] ?? 0) + 1;
1149
+ let nextClock = 0;
1150
+ const mutation = this.stateMutation.then(async () => {
1151
+ await this.ensureStateLoaded();
1152
+ nextClock = (this.versionVector[this.nodeId] ?? 0) + 1;
1153
+ this.versionVector[this.nodeId] = nextClock;
1154
+ await this.stateStore.saveVersionVector(this.versionVector);
1155
+ });
1156
+ this.stateMutation = mutation.catch(() => void 0);
1157
+ return mutation.then(() => nextClock);
540
1158
  }
541
1159
  /**
542
1160
  * 执行一次与所有 peer 的增量同步(metadata 级别),并尽力拉取缺失消息。
543
1161
  */
544
1162
  async syncOnce() {
1163
+ await this.ensureStateLoaded();
1164
+ await this.stateMutation;
1165
+ await this.reconcileVersionVectorFromStorage();
545
1166
  const peers = this.getPeers();
546
1167
  if (peers.length === 0) return;
547
- const localVersion = { ...this.versionVector };
548
1168
  const pulledConversations = /* @__PURE__ */ new Set();
549
1169
  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
- }
1170
+ const { remoteVersion } = await peer.exchangeVersionVector({ ...this.versionVector });
1171
+ let cursor;
1172
+ do {
1173
+ const page = await peer.pullMissingMetadata(this.versionVector, cursor);
1174
+ for (const meta of page.items) {
1175
+ await this.storage.upsertConversationMetadata(meta);
1176
+ pulledConversations.add(meta.conversationId);
1177
+ }
1178
+ cursor = page.nextCursor;
1179
+ } while (cursor !== void 0);
556
1180
  for (const [nodeId, clock] of Object.entries(remoteVersion)) {
557
1181
  const current = this.versionVector[nodeId] ?? 0;
558
- if (clock > current) {
559
- this.versionVector[nodeId] = clock;
560
- }
1182
+ if (clock > current) this.versionVector[nodeId] = clock;
561
1183
  }
562
- if (missingForRemote.length > 0) {
563
- this.bumpLocalVersion();
1184
+ await this.stateStore.saveVersionVector(this.versionVector);
1185
+ }
1186
+ const pageSize = 100;
1187
+ let offset = 0;
1188
+ for (; ; ) {
1189
+ const conversations = await this.storage.listConversations({ limit: pageSize, offset });
1190
+ for (const conversation of conversations) {
1191
+ pulledConversations.add(conversation.conversationId);
564
1192
  }
1193
+ if (conversations.length < pageSize) break;
1194
+ offset += conversations.length;
565
1195
  }
566
1196
  for (const conversationId of pulledConversations) {
567
1197
  await this.pullMessagesForConversationFromPeers(conversationId, peers);
@@ -572,11 +1202,6 @@ var ChatSyncEngine = class {
572
1202
  */
573
1203
  async antiEntropyOnce() {
574
1204
  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
1205
  }
581
1206
  async pullMessagesForConversationFromPeers(conversationId, peers) {
582
1207
  const localMsgs = await this.storage.getMessages(conversationId, { mode: "full-content" });
@@ -629,12 +1254,39 @@ var ChatSyncEngine = class {
629
1254
  }
630
1255
  }
631
1256
  }
632
- getVersionVector() {
1257
+ async getVersionVector() {
1258
+ await this.ensureStateLoaded();
1259
+ await this.stateMutation;
633
1260
  return { ...this.versionVector };
634
1261
  }
635
1262
  getStorage() {
636
1263
  return this.storage;
637
1264
  }
1265
+ ensureStateLoaded() {
1266
+ this.loadStatePromise ??= (async () => {
1267
+ const stored = await this.stateStore.loadVersionVector();
1268
+ this.versionVector = { ...stored, [this.nodeId]: stored[this.nodeId] ?? 0 };
1269
+ })();
1270
+ return this.loadStatePromise;
1271
+ }
1272
+ async reconcileVersionVectorFromStorage() {
1273
+ const pageSize = 100;
1274
+ let offset = 0;
1275
+ let changed = false;
1276
+ for (; ; ) {
1277
+ const conversations = await this.storage.listConversations({ limit: pageSize, offset });
1278
+ for (const conversation of conversations) {
1279
+ const current = this.versionVector[conversation.originNodeId] ?? 0;
1280
+ if (conversation.originClock > current) {
1281
+ this.versionVector[conversation.originNodeId] = conversation.originClock;
1282
+ changed = true;
1283
+ }
1284
+ }
1285
+ if (conversations.length < pageSize) break;
1286
+ offset += conversations.length;
1287
+ }
1288
+ if (changed) await this.stateStore.saveVersionVector(this.versionVector);
1289
+ }
638
1290
  };
639
1291
 
640
1292
  // src/sync/peerNodeAdapter.ts
@@ -648,8 +1300,8 @@ var PeerNodeSyncAdapter = class {
648
1300
  exchangeVersionVector(localVersion) {
649
1301
  return this.transport.exchangeVersionVector(this.nodeId, localVersion);
650
1302
  }
651
- pullMissingMetadata(sinceVersion) {
652
- return this.transport.pullMissingMetadata(this.nodeId, sinceVersion);
1303
+ pullMissingMetadata(sinceVersion, cursor) {
1304
+ return this.transport.pullMissingMetadata(this.nodeId, sinceVersion, cursor);
653
1305
  }
654
1306
  pullMissingMessages(conversationId, knownMessageIds) {
655
1307
  const function_ = this.transport.pullMissingMessages?.bind(this.transport);
@@ -671,7 +1323,17 @@ var PeerNodeSyncAdapter = class {
671
1323
  function isConversationMeta(value) {
672
1324
  if (value === null || typeof value !== "object") return false;
673
1325
  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";
1326
+ 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";
1327
+ }
1328
+ function computeMissingVersionRanges(currentVersion, availableVersion) {
1329
+ const ranges = [];
1330
+ for (const [originNodeId, availableClock] of Object.entries(availableVersion)) {
1331
+ const currentClock = currentVersion[originNodeId] ?? 0;
1332
+ if (availableClock > currentClock) {
1333
+ ranges.push({ originNodeId, fromExclusive: currentClock, toInclusive: availableClock });
1334
+ }
1335
+ }
1336
+ return ranges.sort((left, right) => left.originNodeId.localeCompare(right.originNodeId));
675
1337
  }
676
1338
  // Annotate the CommonJS export names for ESM import in node:
677
1339
  0 && (module.exports = {
@@ -679,19 +1341,27 @@ function isConversationMeta(value) {
679
1341
  DEVICE_ORCHESTRATION_PROTOCOL,
680
1342
  DEVICE_PAIRING_INVITE_PROTOCOL,
681
1343
  DEVICE_PAIRING_INVITE_TTL_MS,
1344
+ DeviceCloudConnectionCoordinator,
1345
+ JsonFrameError,
682
1346
  LIBP2P_RPC_REQUEST_TYPE,
683
1347
  LIBP2P_RPC_RESPONSE_TYPE,
684
1348
  LIBP2P_SYNC_REQUEST_TYPE,
685
1349
  LIBP2P_SYNC_RESPONSE_TYPE,
686
1350
  Libp2pDeviceSyncTransport,
687
1351
  LocalTrustDeviceAuthorizer,
1352
+ MemoryDeviceSyncStateStore,
688
1353
  PeerNodeSyncAdapter,
689
1354
  attachmentBlobFromWire,
1355
+ canonicalDevicePairingInviteBytes,
1356
+ computeMissingVersionRanges,
690
1357
  createDeviceOrchestrationStreamHandler,
691
1358
  createDeviceOrchestrationTransport,
692
1359
  createDevicePairingInvite,
1360
+ createJsonFrameReader,
693
1361
  decodeAttachmentBlobRpc,
694
1362
  encodeDevicePairingInvite,
1363
+ encodeJsonFrame,
1364
+ encodeJsonFrames,
695
1365
  isConversationMeta,
696
1366
  isLibp2pRpcRequest,
697
1367
  isLibp2pRpcResponse,