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,6 +24,8 @@ __export(device_network_entry_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,
@@ -31,13 +33,19 @@ __export(device_network_entry_exports, {
31
33
  Libp2pDeviceSyncTransport: () => Libp2pDeviceSyncTransport,
32
34
  LocalTrustDeviceAuthorizer: () => LocalTrustDeviceAuthorizer,
33
35
  MemoryDeviceNetworkService: () => MemoryDeviceNetworkService,
36
+ MemoryDeviceSyncStateStore: () => MemoryDeviceSyncStateStore,
34
37
  PeerNodeSyncAdapter: () => PeerNodeSyncAdapter,
35
38
  attachmentBlobFromWire: () => attachmentBlobFromWire,
39
+ canonicalDevicePairingInviteBytes: () => canonicalDevicePairingInviteBytes,
40
+ computeMissingVersionRanges: () => computeMissingVersionRanges,
36
41
  createDeviceOrchestrationStreamHandler: () => createDeviceOrchestrationStreamHandler,
37
42
  createDeviceOrchestrationTransport: () => createDeviceOrchestrationTransport,
38
43
  createDevicePairingInvite: () => createDevicePairingInvite,
44
+ createJsonFrameReader: () => createJsonFrameReader,
39
45
  decodeAttachmentBlobRpc: () => decodeAttachmentBlobRpc,
40
46
  encodeDevicePairingInvite: () => encodeDevicePairingInvite,
47
+ encodeJsonFrame: () => encodeJsonFrame,
48
+ encodeJsonFrames: () => encodeJsonFrames,
41
49
  isConversationMeta: () => isConversationMeta,
42
50
  isLibp2pRpcRequest: () => isLibp2pRpcRequest,
43
51
  isLibp2pRpcResponse: () => isLibp2pRpcResponse,
@@ -48,6 +56,228 @@ __export(device_network_entry_exports, {
48
56
  });
49
57
  module.exports = __toCommonJS(device_network_entry_exports);
50
58
 
59
+ // src/device-network/deviceCloudConnectionCoordinator.ts
60
+ var DEFAULT_HEARTBEAT_INTERVAL_MS = 6e4;
61
+ var DEFAULT_INITIAL_BACKOFF_MS = 1e3;
62
+ var DEFAULT_MAX_BACKOFF_MS = 6e4;
63
+ var DEFAULT_JITTER_RATIO = 0.2;
64
+ function emptyComponents() {
65
+ return {
66
+ authorizer: "not-run",
67
+ registration: "not-run",
68
+ relay: "not-run",
69
+ heartbeat: "not-run",
70
+ directory: "not-run"
71
+ };
72
+ }
73
+ function assertNonNegativeFinite(value, name) {
74
+ if (!Number.isFinite(value) || value < 0) {
75
+ throw new TypeError(`${name} must be a finite non-negative number`);
76
+ }
77
+ }
78
+ var DeviceCloudConnectionCoordinator = class {
79
+ constructor(options) {
80
+ this.options = options;
81
+ this.configuration = options.configuration;
82
+ this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
83
+ this.initialBackoffMs = options.initialBackoffMs ?? DEFAULT_INITIAL_BACKOFF_MS;
84
+ this.maxBackoffMs = options.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS;
85
+ this.jitterRatio = options.jitterRatio ?? DEFAULT_JITTER_RATIO;
86
+ this.now = options.now ?? Date.now;
87
+ this.random = options.random ?? Math.random;
88
+ assertNonNegativeFinite(this.heartbeatIntervalMs, "heartbeatIntervalMs");
89
+ assertNonNegativeFinite(this.initialBackoffMs, "initialBackoffMs");
90
+ assertNonNegativeFinite(this.maxBackoffMs, "maxBackoffMs");
91
+ assertNonNegativeFinite(this.jitterRatio, "jitterRatio");
92
+ if (this.jitterRatio > 1) throw new TypeError("jitterRatio must not exceed 1");
93
+ this.resetSnapshot();
94
+ }
95
+ configuration;
96
+ generation = 0;
97
+ controller = new AbortController();
98
+ timer;
99
+ inFlight;
100
+ started = false;
101
+ consecutiveFailures = 0;
102
+ snapshotValue = {
103
+ status: "not-configured",
104
+ generation: 0,
105
+ components: emptyComponents()
106
+ };
107
+ heartbeatIntervalMs;
108
+ initialBackoffMs;
109
+ maxBackoffMs;
110
+ jitterRatio;
111
+ now;
112
+ random;
113
+ get snapshot() {
114
+ return {
115
+ ...this.snapshotValue,
116
+ components: { ...this.snapshotValue.components }
117
+ };
118
+ }
119
+ async start() {
120
+ if (this.started) return this.runNow();
121
+ this.started = true;
122
+ await this.runNow();
123
+ }
124
+ async stop() {
125
+ this.started = false;
126
+ this.clearTimer();
127
+ const stoppedController = this.controller;
128
+ stoppedController.abort(new Error("device cloud coordinator stopped"));
129
+ await this.inFlight?.catch(() => void 0);
130
+ if (this.controller === stoppedController) {
131
+ this.controller = new AbortController();
132
+ }
133
+ }
134
+ async setConfiguration(configuration) {
135
+ this.configuration = configuration;
136
+ this.generation += 1;
137
+ this.consecutiveFailures = 0;
138
+ this.clearTimer();
139
+ this.controller.abort(new Error("device cloud configuration changed"));
140
+ this.controller = new AbortController();
141
+ this.inFlight = void 0;
142
+ this.resetSnapshot();
143
+ if (this.started && this.options.adapter.isConfigured(configuration)) await this.runNow();
144
+ }
145
+ runNow() {
146
+ if (this.inFlight) return this.inFlight;
147
+ if (!this.options.adapter.isConfigured(this.configuration)) {
148
+ this.resetSnapshot();
149
+ return Promise.resolve();
150
+ }
151
+ const generation = this.generation;
152
+ const configuration = this.configuration;
153
+ const signal = this.controller.signal;
154
+ const run = this.maintain(configuration, generation, signal);
155
+ const tracked = run.finally(() => {
156
+ if (this.inFlight === tracked) this.inFlight = void 0;
157
+ });
158
+ this.inFlight = tracked;
159
+ return tracked;
160
+ }
161
+ async maintain(configuration, generation, signal) {
162
+ if (!this.isCurrent(generation, signal)) return;
163
+ await this.updateSnapshot(generation, {
164
+ status: "connecting",
165
+ lastError: void 0,
166
+ nextRetryAt: void 0
167
+ });
168
+ try {
169
+ if (this.snapshotValue.components.authorizer !== "ready") {
170
+ await this.runStep("authorizer", generation, signal, () => this.options.adapter.ensureAuthorizer(configuration, signal));
171
+ }
172
+ if (this.snapshotValue.components.registration !== "ready") {
173
+ await this.runStep("registration", generation, signal, () => this.options.adapter.registerDevice(configuration, signal));
174
+ }
175
+ let relayError;
176
+ try {
177
+ await this.runStep("relay", generation, signal, () => this.options.adapter.ensureRelay(configuration, signal));
178
+ } catch (error) {
179
+ relayError = error;
180
+ this.options.logWarning?.("Cloud relay maintenance failed", error);
181
+ }
182
+ await this.runStep("heartbeat", generation, signal, () => this.options.adapter.heartbeat(configuration, signal));
183
+ let directoryError;
184
+ try {
185
+ await this.runStep("directory", generation, signal, () => this.options.adapter.syncDirectory(configuration, signal));
186
+ } catch (error) {
187
+ directoryError = error;
188
+ this.options.logWarning?.("Cloud directory synchronization failed", error);
189
+ }
190
+ if (!this.isCurrent(generation, signal)) return;
191
+ this.consecutiveFailures = 0;
192
+ const relayRequired = this.options.adapter.relayRequiredForOnline(configuration);
193
+ await this.updateSnapshot(generation, {
194
+ status: directoryError || relayRequired && relayError ? "degraded" : "online",
195
+ lastError: directoryError ?? relayError,
196
+ nextRetryAt: this.now() + this.heartbeatIntervalMs
197
+ });
198
+ this.schedule(this.heartbeatIntervalMs, generation);
199
+ } catch (error) {
200
+ if (!this.isCurrent(generation, signal)) return;
201
+ this.consecutiveFailures += 1;
202
+ if (this.snapshotValue.components.heartbeat === "failed") {
203
+ this.snapshotValue.components.registration = "not-run";
204
+ }
205
+ const status = this.options.adapter.classifyError?.(error) ?? "offline";
206
+ const delay = this.backoffDelay();
207
+ await this.updateSnapshot(generation, {
208
+ status,
209
+ lastError: error,
210
+ nextRetryAt: this.now() + delay
211
+ });
212
+ this.schedule(delay, generation);
213
+ throw error;
214
+ }
215
+ }
216
+ async runStep(component, generation, signal, operation) {
217
+ if (!this.isCurrent(generation, signal)) return;
218
+ await this.setComponent(component, "pending", generation);
219
+ try {
220
+ const result = await operation();
221
+ if (!this.isCurrent(generation, signal)) return;
222
+ await result?.commit?.();
223
+ if (!this.isCurrent(generation, signal)) return;
224
+ await this.setComponent(component, "ready", generation);
225
+ } catch (error) {
226
+ if (this.isCurrent(generation, signal)) await this.setComponent(component, "failed", generation);
227
+ throw error;
228
+ }
229
+ }
230
+ backoffDelay() {
231
+ const exponential = Math.min(
232
+ this.maxBackoffMs,
233
+ this.initialBackoffMs * 2 ** Math.max(0, this.consecutiveFailures - 1)
234
+ );
235
+ const jitter = exponential * this.jitterRatio * (this.random() * 2 - 1);
236
+ return Math.max(0, Math.round(exponential + jitter));
237
+ }
238
+ schedule(delayMs, generation) {
239
+ if (!this.started || generation !== this.generation) return;
240
+ this.clearTimer();
241
+ this.timer = setTimeout(() => {
242
+ if (generation !== this.generation) return;
243
+ void this.runNow().catch((error) => {
244
+ this.options.logWarning?.("Cloud maintenance failed", error);
245
+ });
246
+ }, delayMs);
247
+ }
248
+ clearTimer() {
249
+ if (this.timer) clearTimeout(this.timer);
250
+ this.timer = void 0;
251
+ }
252
+ isCurrent(generation, signal) {
253
+ return generation === this.generation && !signal.aborted;
254
+ }
255
+ async setComponent(component, status, generation) {
256
+ if (generation !== this.generation) return;
257
+ this.snapshotValue = {
258
+ ...this.snapshotValue,
259
+ components: { ...this.snapshotValue.components, [component]: status }
260
+ };
261
+ await this.emit();
262
+ }
263
+ async updateSnapshot(generation, update) {
264
+ if (generation !== this.generation) return;
265
+ this.snapshotValue = { ...this.snapshotValue, ...update };
266
+ await this.emit();
267
+ }
268
+ resetSnapshot() {
269
+ this.snapshotValue = {
270
+ status: this.options.adapter.isConfigured(this.configuration) ? "offline" : "not-configured",
271
+ generation: this.generation,
272
+ components: emptyComponents()
273
+ };
274
+ void this.emit();
275
+ }
276
+ async emit() {
277
+ await this.options.onStatus?.(this.snapshot);
278
+ }
279
+ };
280
+
51
281
  // src/orchestration/errors.ts
52
282
  var OrchestrationError = class extends Error {
53
283
  code;
@@ -76,9 +306,222 @@ var OrchestrationError = class extends Error {
76
306
  }
77
307
  };
78
308
 
309
+ // src/device-network/jsonFrame.ts
310
+ var JsonFrameError = class extends Error {
311
+ code;
312
+ constructor(code, message, options) {
313
+ super(message, options);
314
+ this.name = "JsonFrameError";
315
+ this.code = code;
316
+ }
317
+ };
318
+ var HEADER_BYTES = 4;
319
+ var MAX_UINT32 = 4294967295;
320
+ function assertPositiveInteger(value, name) {
321
+ if (!Number.isSafeInteger(value) || value <= 0) {
322
+ throw new TypeError(`${name} must be a positive safe integer`);
323
+ }
324
+ }
325
+ function validateMaxPayloadBytes(maxPayloadBytes) {
326
+ assertPositiveInteger(maxPayloadBytes, "maxPayloadBytes");
327
+ if (maxPayloadBytes > MAX_UINT32) {
328
+ throw new TypeError("maxPayloadBytes must fit in an unsigned 32-bit frame length");
329
+ }
330
+ }
331
+ function encodeJsonFrame(value, maxPayloadBytes = MAX_UINT32) {
332
+ validateMaxPayloadBytes(maxPayloadBytes);
333
+ let serialized;
334
+ try {
335
+ serialized = JSON.stringify(value);
336
+ } catch (error) {
337
+ throw new JsonFrameError("INVALID_JSON", "JSON frame value cannot be serialized", {
338
+ cause: error
339
+ });
340
+ }
341
+ if (serialized === void 0) {
342
+ throw new JsonFrameError("INVALID_JSON", "JSON frame value cannot be serialized");
343
+ }
344
+ const payload = new TextEncoder().encode(serialized);
345
+ if (payload.byteLength > maxPayloadBytes) {
346
+ throw new JsonFrameError(
347
+ "FRAME_TOO_LARGE",
348
+ `JSON frame payload exceeds ${maxPayloadBytes} bytes`
349
+ );
350
+ }
351
+ const frame = new Uint8Array(HEADER_BYTES + payload.byteLength);
352
+ new DataView(frame.buffer, frame.byteOffset, HEADER_BYTES).setUint32(0, payload.byteLength, false);
353
+ frame.set(payload, HEADER_BYTES);
354
+ return frame;
355
+ }
356
+ async function* encodeJsonFrames(values, maxPayloadBytes = MAX_UINT32) {
357
+ for await (const value of values) {
358
+ yield encodeJsonFrame(value, maxPayloadBytes);
359
+ }
360
+ }
361
+ function concatBytes(left, right) {
362
+ if (left.byteLength === 0) return right.slice();
363
+ if (right.byteLength === 0) return left;
364
+ const combined = new Uint8Array(left.byteLength + right.byteLength);
365
+ combined.set(left);
366
+ combined.set(right, left.byteLength);
367
+ return combined;
368
+ }
369
+ function abortedError(signal) {
370
+ return new JsonFrameError("ABORTED", "JSON frame reading was aborted", {
371
+ cause: signal?.reason
372
+ });
373
+ }
374
+ async function readNextChunk(iterator, idleDeadline, totalDeadline, signal) {
375
+ if (signal?.aborted) throw abortedError(signal);
376
+ const now = Date.now();
377
+ const deadline = totalDeadline === void 0 ? idleDeadline : Math.min(idleDeadline, totalDeadline);
378
+ if (deadline <= now) {
379
+ throw new JsonFrameError(
380
+ totalDeadline !== void 0 && totalDeadline <= idleDeadline ? "TOTAL_TIMEOUT" : "IDLE_TIMEOUT",
381
+ totalDeadline !== void 0 && totalDeadline <= idleDeadline ? "JSON frame total timeout expired" : "JSON frame idle timeout expired"
382
+ );
383
+ }
384
+ return new Promise((resolve, reject) => {
385
+ let settled = false;
386
+ const finish = (callback) => {
387
+ if (settled) return;
388
+ settled = true;
389
+ clearTimeout(timer);
390
+ signal?.removeEventListener("abort", onAbort);
391
+ callback();
392
+ };
393
+ const onAbort = () => {
394
+ finish(() => {
395
+ reject(abortedError(signal));
396
+ });
397
+ };
398
+ const timer = setTimeout(() => {
399
+ const currentTime = Date.now();
400
+ const totalExpired = totalDeadline !== void 0 && currentTime >= totalDeadline;
401
+ finish(() => {
402
+ reject(
403
+ new JsonFrameError(
404
+ totalExpired ? "TOTAL_TIMEOUT" : "IDLE_TIMEOUT",
405
+ totalExpired ? "JSON frame total timeout expired" : "JSON frame idle timeout expired"
406
+ )
407
+ );
408
+ });
409
+ }, Math.max(1, deadline - now));
410
+ signal?.addEventListener("abort", onAbort, { once: true });
411
+ void iterator.next().then(
412
+ (result) => {
413
+ finish(() => {
414
+ resolve(result);
415
+ });
416
+ },
417
+ (error) => {
418
+ finish(() => {
419
+ reject(
420
+ error instanceof Error ? error : new Error("JSON frame source failed", {
421
+ cause: error
422
+ })
423
+ );
424
+ });
425
+ }
426
+ );
427
+ });
428
+ }
429
+ function createJsonFrameReader(source, options) {
430
+ validateMaxPayloadBytes(options.maxPayloadBytes);
431
+ assertPositiveInteger(options.idleTimeoutMs, "idleTimeoutMs");
432
+ assertPositiveInteger(options.totalTimeoutMs, "totalTimeoutMs");
433
+ return (async function* readFrames() {
434
+ const iterator = source[Symbol.asyncIterator]();
435
+ let buffered = new Uint8Array(0);
436
+ let expectedPayloadBytes;
437
+ let idleDeadline = Date.now() + options.idleTimeoutMs;
438
+ let totalDeadline;
439
+ let abortCalled = false;
440
+ const abortOnce = async (error) => {
441
+ if (abortCalled) return;
442
+ abortCalled = true;
443
+ await Promise.resolve(options.abort?.(error)).catch(() => void 0);
444
+ };
445
+ try {
446
+ for (; ; ) {
447
+ if (expectedPayloadBytes === void 0 && buffered.byteLength >= HEADER_BYTES) {
448
+ expectedPayloadBytes = new DataView(
449
+ buffered.buffer,
450
+ buffered.byteOffset,
451
+ HEADER_BYTES
452
+ ).getUint32(0, false);
453
+ buffered = buffered.slice(HEADER_BYTES);
454
+ totalDeadline ??= Date.now() + options.totalTimeoutMs;
455
+ if (expectedPayloadBytes > options.maxPayloadBytes) {
456
+ throw new JsonFrameError(
457
+ "FRAME_TOO_LARGE",
458
+ `JSON frame payload exceeds ${options.maxPayloadBytes} bytes`
459
+ );
460
+ }
461
+ }
462
+ if (expectedPayloadBytes !== void 0 && buffered.byteLength >= expectedPayloadBytes) {
463
+ const payload = buffered.slice(0, expectedPayloadBytes);
464
+ buffered = buffered.slice(expectedPayloadBytes);
465
+ expectedPayloadBytes = void 0;
466
+ totalDeadline = buffered.byteLength > 0 ? Date.now() + options.totalTimeoutMs : void 0;
467
+ let text;
468
+ try {
469
+ text = new TextDecoder("utf-8", { fatal: true }).decode(payload);
470
+ } catch (error) {
471
+ throw new JsonFrameError("INVALID_UTF8", "JSON frame payload is not valid UTF-8", {
472
+ cause: error
473
+ });
474
+ }
475
+ let value;
476
+ try {
477
+ value = JSON.parse(text);
478
+ } catch (error) {
479
+ throw new JsonFrameError("INVALID_JSON", "JSON frame payload is not valid JSON", {
480
+ cause: error
481
+ });
482
+ }
483
+ yield value;
484
+ idleDeadline = Date.now() + options.idleTimeoutMs;
485
+ continue;
486
+ }
487
+ const result = await readNextChunk(
488
+ iterator,
489
+ idleDeadline,
490
+ totalDeadline,
491
+ options.signal
492
+ );
493
+ if (result.done) {
494
+ if (buffered.byteLength > 0 || expectedPayloadBytes !== void 0) {
495
+ throw new JsonFrameError("TRUNCATED_FRAME", "JSON frame stream ended mid-frame");
496
+ }
497
+ return;
498
+ }
499
+ if (!(result.value instanceof Uint8Array)) {
500
+ throw new TypeError("JSON frame source must yield Uint8Array chunks");
501
+ }
502
+ if (result.value.byteLength === 0) continue;
503
+ const now = Date.now();
504
+ idleDeadline = now + options.idleTimeoutMs;
505
+ totalDeadline ??= now + options.totalTimeoutMs;
506
+ buffered = concatBytes(buffered, result.value);
507
+ }
508
+ } catch (error) {
509
+ const frameError = error instanceof JsonFrameError ? error : new JsonFrameError("ABORTED", "JSON frame source failed", { cause: error });
510
+ await abortOnce(frameError);
511
+ throw frameError;
512
+ } finally {
513
+ void iterator.return?.().catch(() => void 0);
514
+ }
515
+ })();
516
+ }
517
+
79
518
  // src/device-network/deviceOrchestrationTransport.ts
80
- var DEVICE_ORCHESTRATION_PROTOCOL = "/memeloop/orchestration/1.0.0";
519
+ var DEVICE_ORCHESTRATION_PROTOCOL = "/memeloop/orchestration/2.0.0";
81
520
  var DEFAULT_MAX_FRAME_BYTES = 1024 * 1024;
521
+ var REQUEST_IDLE_TIMEOUT_MS = 1e4;
522
+ var REQUEST_TOTAL_TIMEOUT_MS = 3e4;
523
+ var WATCH_IDLE_TIMEOUT_MS = 9e4;
524
+ var WATCH_BOOKMARK_INTERVAL_MS = 3e4;
82
525
  function orchestrationTransportError(code, message) {
83
526
  return new OrchestrationError({
84
527
  code,
@@ -91,65 +534,6 @@ function assertFrameLimit(limit) {
91
534
  throw new TypeError("maxFrameBytes must be a safe integer of at least 256");
92
535
  }
93
536
  }
94
- async function* encodeLines(values, maxFrameBytes) {
95
- const encoder = new TextEncoder();
96
- for await (const value of values) {
97
- const frame = encoder.encode(`${JSON.stringify(value)}
98
- `);
99
- if (frame.byteLength > maxFrameBytes) {
100
- throw orchestrationTransportError(
101
- "EXHAUSTED",
102
- `device orchestration frame exceeds ${maxFrameBytes} bytes`
103
- );
104
- }
105
- yield frame;
106
- }
107
- }
108
- async function* decodeLines(source, maxFrameBytes) {
109
- const decoder = new TextDecoder();
110
- let buffered = "";
111
- for await (const chunk of source) {
112
- buffered += decoder.decode(chunk, { stream: true });
113
- for (; ; ) {
114
- const newline = buffered.indexOf("\n");
115
- if (newline < 0) break;
116
- const line = buffered.slice(0, newline);
117
- buffered = buffered.slice(newline + 1);
118
- if (!line) continue;
119
- if (new TextEncoder().encode(line).byteLength > maxFrameBytes) {
120
- throw orchestrationTransportError(
121
- "EXHAUSTED",
122
- `device orchestration frame exceeds ${maxFrameBytes} bytes`
123
- );
124
- }
125
- try {
126
- yield JSON.parse(line);
127
- } catch {
128
- throw orchestrationTransportError(
129
- "INVALID",
130
- "device orchestration stream contains invalid JSON"
131
- );
132
- }
133
- }
134
- if (new TextEncoder().encode(buffered).byteLength > maxFrameBytes) {
135
- throw orchestrationTransportError(
136
- "EXHAUSTED",
137
- `device orchestration frame exceeds ${maxFrameBytes} bytes`
138
- );
139
- }
140
- }
141
- buffered += decoder.decode();
142
- if (buffered.trim()) {
143
- try {
144
- yield JSON.parse(buffered);
145
- } catch {
146
- throw orchestrationTransportError(
147
- "INVALID",
148
- "device orchestration stream contains invalid trailing JSON"
149
- );
150
- }
151
- }
152
- }
153
537
  function throwIfAborted(signal) {
154
538
  if (signal?.aborted) {
155
539
  throw orchestrationTransportError("CANCELLED", "device orchestration request was cancelled");
@@ -168,11 +552,41 @@ async function openAuthenticatedStream(options) {
168
552
  }
169
553
  async function writeRequest(stream, request, grant, maxFrameBytes) {
170
554
  const envelope = {
171
- type: "memeloop-device-orchestration-request-v1",
555
+ type: "memeloop-device-orchestration-request-v2",
172
556
  request,
173
557
  ...grant ? { grant } : {}
174
558
  };
175
- await stream.sink(encodeLines([envelope], maxFrameBytes));
559
+ await stream.sink(encodeJsonFrames([envelope], maxFrameBytes));
560
+ }
561
+ function abortStreamOnce(stream) {
562
+ let aborted = false;
563
+ return async (error) => {
564
+ if (aborted) return;
565
+ aborted = true;
566
+ await stream.abort(error);
567
+ };
568
+ }
569
+ function frameReader(stream, maxFrameBytes, idleTimeoutMs, signal, abort) {
570
+ return createJsonFrameReader(stream.source, {
571
+ maxPayloadBytes: maxFrameBytes,
572
+ idleTimeoutMs,
573
+ totalTimeoutMs: REQUEST_TOTAL_TIMEOUT_MS,
574
+ signal,
575
+ abort
576
+ });
577
+ }
578
+ function mapFrameError(error) {
579
+ if (!(error instanceof JsonFrameError)) throw error;
580
+ if (error.code === "ABORTED") {
581
+ throw orchestrationTransportError("CANCELLED", error.message);
582
+ }
583
+ if (error.code === "FRAME_TOO_LARGE") {
584
+ throw orchestrationTransportError("EXHAUSTED", error.message);
585
+ }
586
+ if (error.code === "IDLE_TIMEOUT" || error.code === "TOTAL_TIMEOUT") {
587
+ throw orchestrationTransportError("UNAVAILABLE", error.message);
588
+ }
589
+ throw orchestrationTransportError("INVALID", error.message);
176
590
  }
177
591
  function createDeviceOrchestrationTransport(options) {
178
592
  const maxFrameBytes = options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES;
@@ -181,8 +595,9 @@ function createDeviceOrchestrationTransport(options) {
181
595
  async request(request, transportOptions = {}) {
182
596
  throwIfAborted(transportOptions.signal);
183
597
  const { stream, grant } = await openAuthenticatedStream(options);
598
+ const abort = abortStreamOnce(stream);
184
599
  const closeOnAbort = () => {
185
- void stream.close();
600
+ void abort(orchestrationTransportError("CANCELLED", "device orchestration request was cancelled"));
186
601
  };
187
602
  transportOptions.signal?.addEventListener("abort", closeOnAbort, {
188
603
  once: true
@@ -190,7 +605,13 @@ function createDeviceOrchestrationTransport(options) {
190
605
  try {
191
606
  await writeRequest(stream, request, grant, maxFrameBytes);
192
607
  let response;
193
- for await (const value of decodeLines(stream.source, maxFrameBytes)) {
608
+ for await (const value of frameReader(
609
+ stream,
610
+ maxFrameBytes,
611
+ REQUEST_IDLE_TIMEOUT_MS,
612
+ transportOptions.signal,
613
+ abort
614
+ )) {
194
615
  if (response) {
195
616
  throw orchestrationTransportError(
196
617
  "INVALID",
@@ -207,6 +628,9 @@ function createDeviceOrchestrationTransport(options) {
207
628
  );
208
629
  }
209
630
  return response;
631
+ } catch (error) {
632
+ if (error instanceof JsonFrameError) await abort(error);
633
+ mapFrameError(error);
210
634
  } finally {
211
635
  transportOptions.signal?.removeEventListener("abort", closeOnAbort);
212
636
  await stream.close().catch(() => void 0);
@@ -215,19 +639,29 @@ function createDeviceOrchestrationTransport(options) {
215
639
  async *watch(request, transportOptions = {}) {
216
640
  throwIfAborted(transportOptions.signal);
217
641
  const { stream, grant } = await openAuthenticatedStream(options);
642
+ const abort = abortStreamOnce(stream);
218
643
  const closeOnAbort = () => {
219
- void stream.close();
644
+ void abort(orchestrationTransportError("CANCELLED", "device orchestration watch was cancelled"));
220
645
  };
221
646
  transportOptions.signal?.addEventListener("abort", closeOnAbort, {
222
647
  once: true
223
648
  });
224
649
  try {
225
650
  await writeRequest(stream, request, grant, maxFrameBytes);
226
- for await (const value of decodeLines(stream.source, maxFrameBytes)) {
651
+ for await (const value of frameReader(
652
+ stream,
653
+ maxFrameBytes,
654
+ WATCH_IDLE_TIMEOUT_MS,
655
+ transportOptions.signal,
656
+ abort
657
+ )) {
227
658
  throwIfAborted(transportOptions.signal);
228
659
  yield value;
229
660
  }
230
661
  throwIfAborted(transportOptions.signal);
662
+ } catch (error) {
663
+ if (error instanceof JsonFrameError) await abort(error);
664
+ mapFrameError(error);
231
665
  } finally {
232
666
  transportOptions.signal?.removeEventListener("abort", closeOnAbort);
233
667
  await stream.close().catch(() => void 0);
@@ -236,15 +670,22 @@ function createDeviceOrchestrationTransport(options) {
236
670
  };
237
671
  }
238
672
  async function readSingleRequest(stream, maxFrameBytes) {
673
+ const abort = abortStreamOnce(stream);
239
674
  let envelope;
240
- for await (const value of decodeLines(stream.source, maxFrameBytes)) {
675
+ for await (const value of frameReader(
676
+ stream,
677
+ maxFrameBytes,
678
+ REQUEST_IDLE_TIMEOUT_MS,
679
+ void 0,
680
+ abort
681
+ )) {
241
682
  if (envelope) {
242
683
  throw orchestrationTransportError(
243
684
  "INVALID",
244
685
  "device orchestration stream accepts exactly one request"
245
686
  );
246
687
  }
247
- if (value === null || typeof value !== "object" || Array.isArray(value) || value.type !== "memeloop-device-orchestration-request-v1" || value.request === null || typeof value.request !== "object") {
688
+ if (value === null || typeof value !== "object" || Array.isArray(value) || value.type !== "memeloop-device-orchestration-request-v2" || value.request === null || typeof value.request !== "object") {
248
689
  throw orchestrationTransportError(
249
690
  "INVALID",
250
691
  "device orchestration request envelope is invalid"
@@ -257,6 +698,94 @@ async function readSingleRequest(stream, maxFrameBytes) {
257
698
  }
258
699
  return envelope;
259
700
  }
701
+ async function* withWatchBookmarks(source, request, signal) {
702
+ const iterator = source[Symbol.asyncIterator]();
703
+ let activity;
704
+ let activityWaiter;
705
+ let bookmarkTimer;
706
+ let readInFlight = false;
707
+ let lastResourceVersion = watchResourceVersion(request.payload.options) ?? "0";
708
+ const clearBookmarkTimer = () => {
709
+ if (bookmarkTimer !== void 0) clearTimeout(bookmarkTimer);
710
+ bookmarkTimer = void 0;
711
+ };
712
+ const publishActivity = (value) => {
713
+ if (activityWaiter) {
714
+ const resolve = activityWaiter;
715
+ activityWaiter = void 0;
716
+ clearBookmarkTimer();
717
+ resolve(value);
718
+ return;
719
+ }
720
+ activity = value;
721
+ };
722
+ const readNext = () => {
723
+ if (readInFlight) return;
724
+ readInFlight = true;
725
+ void iterator.next().then(
726
+ (value) => {
727
+ readInFlight = false;
728
+ publishActivity({ kind: "value", value });
729
+ },
730
+ (error) => {
731
+ readInFlight = false;
732
+ publishActivity({ kind: "error", error });
733
+ }
734
+ );
735
+ };
736
+ const waitForActivity = () => {
737
+ if (activity) {
738
+ const value = activity;
739
+ activity = void 0;
740
+ return Promise.resolve(value);
741
+ }
742
+ if (signal.aborted) return Promise.resolve({ kind: "aborted" });
743
+ return new Promise((resolve) => {
744
+ activityWaiter = resolve;
745
+ bookmarkTimer = setTimeout(() => {
746
+ bookmarkTimer = void 0;
747
+ activityWaiter = void 0;
748
+ resolve({ kind: "bookmark" });
749
+ }, WATCH_BOOKMARK_INTERVAL_MS);
750
+ });
751
+ };
752
+ const onAbort = () => {
753
+ publishActivity({ kind: "aborted" });
754
+ };
755
+ signal.addEventListener("abort", onAbort, { once: true });
756
+ readNext();
757
+ try {
758
+ for (; ; ) {
759
+ const result = await waitForActivity();
760
+ if (result.kind === "aborted") return;
761
+ if (result.kind === "bookmark") {
762
+ yield {
763
+ protocol: request.protocol,
764
+ requestId: request.requestId,
765
+ ok: true,
766
+ result: { type: "BOOKMARK", resourceVersion: lastResourceVersion }
767
+ };
768
+ continue;
769
+ }
770
+ if (result.kind === "error") throw result.error;
771
+ if (result.value.done) return;
772
+ const response = result.value.value;
773
+ lastResourceVersion = response.ok ? watchResourceVersion(response.result) ?? lastResourceVersion : lastResourceVersion;
774
+ yield response;
775
+ readNext();
776
+ }
777
+ } finally {
778
+ clearBookmarkTimer();
779
+ activityWaiter = void 0;
780
+ signal.removeEventListener("abort", onAbort);
781
+ void iterator.return?.();
782
+ }
783
+ }
784
+ function watchResourceVersion(value) {
785
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
786
+ const resourceVersion = value.resourceVersion;
787
+ return typeof resourceVersion === "string" && resourceVersion.length > 0 ? resourceVersion : void 0;
788
+ }
260
789
  function createDeviceOrchestrationStreamHandler(options) {
261
790
  const maxFrameBytes = options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES;
262
791
  assertFrameLimit(maxFrameBytes);
@@ -271,10 +800,13 @@ function createDeviceOrchestrationStreamHandler(options) {
271
800
  const handler = await options.resolveHandler(remotePeerId);
272
801
  if (request.operation === "watch") {
273
802
  await stream.sink(
274
- encodeLines(handler.watch(request, { signal: abort.signal }), maxFrameBytes)
803
+ encodeJsonFrames(
804
+ withWatchBookmarks(handler.watch(request, { signal: abort.signal }), request, abort.signal),
805
+ maxFrameBytes
806
+ )
275
807
  );
276
808
  } else {
277
- await stream.sink(encodeLines([await handler.request(request)], maxFrameBytes));
809
+ await stream.sink(encodeJsonFrames([await handler.request(request)], maxFrameBytes));
278
810
  }
279
811
  } finally {
280
812
  abort.abort();
@@ -283,9 +815,23 @@ function createDeviceOrchestrationStreamHandler(options) {
283
815
  };
284
816
  }
285
817
 
818
+ // src/device-network/deviceSyncStateStore.ts
819
+ var MemoryDeviceSyncStateStore = class {
820
+ versionVector;
821
+ constructor(initial = {}) {
822
+ this.versionVector = { ...initial };
823
+ }
824
+ async loadVersionVector() {
825
+ return { ...this.versionVector };
826
+ }
827
+ async saveVersionVector(versionVector) {
828
+ this.versionVector = { ...versionVector };
829
+ }
830
+ };
831
+
286
832
  // src/device-network/libp2pSyncProtocol.ts
287
- var LIBP2P_SYNC_REQUEST_TYPE = "memeloop-sync-request-v1";
288
- var LIBP2P_SYNC_RESPONSE_TYPE = "memeloop-sync-response-v1";
833
+ var LIBP2P_SYNC_REQUEST_TYPE = "memeloop-sync-request-v2";
834
+ var LIBP2P_SYNC_RESPONSE_TYPE = "memeloop-sync-response-v2";
289
835
  var syncMethods = /* @__PURE__ */ new Set([
290
836
  "exchangeVersionVector",
291
837
  "pullMissingMetadata",
@@ -345,8 +891,11 @@ var Libp2pDeviceSyncTransport = class {
345
891
  exchangeVersionVector(peerId, localVersion) {
346
892
  return this.request(peerId, "exchangeVersionVector", { localVersion });
347
893
  }
348
- pullMissingMetadata(peerId, sinceVersion) {
349
- return this.request(peerId, "pullMissingMetadata", { sinceVersion });
894
+ pullMissingMetadata(peerId, sinceVersion, cursor) {
895
+ return this.request(peerId, "pullMissingMetadata", {
896
+ sinceVersion,
897
+ cursor
898
+ });
350
899
  }
351
900
  pullMissingMessages(peerId, conversationId, knownMessageIds) {
352
901
  return this.request(peerId, "pullMissingMessages", { conversationId, knownMessageIds });
@@ -357,7 +906,7 @@ var Libp2pDeviceSyncTransport = class {
357
906
  }
358
907
  async request(peerId, method, parameters) {
359
908
  const grant = await this.grantProvider?.(peerId);
360
- const stream = await this.deviceNetwork.openStream(peerId, "/memeloop/sync/1.0.0", grant);
909
+ const stream = await this.deviceNetwork.openStream(peerId, "/memeloop/sync/2.0.0", grant);
361
910
  const request = {
362
911
  type: LIBP2P_SYNC_REQUEST_TYPE,
363
912
  id: crypto.randomUUID(),
@@ -365,31 +914,34 @@ var Libp2pDeviceSyncTransport = class {
365
914
  params: parameters,
366
915
  grant
367
916
  };
368
- await writeJsonToStream(stream, request);
369
- const response = await readJsonFromStream(stream);
370
- await stream.close();
371
- if (!isLibp2pSyncResponse(response)) throw new Error("invalid_sync_response");
372
- if (response.id !== request.id) throw new Error("sync_response_id_mismatch");
373
- if (!response.ok) throw new Error(response.error);
374
- return response.result;
917
+ try {
918
+ await stream.sink(encodeJsonFrames([request], 16 * 1024 * 1024));
919
+ let response;
920
+ for await (const value of createJsonFrameReader(stream.source, {
921
+ maxPayloadBytes: 16 * 1024 * 1024,
922
+ idleTimeoutMs: 15e3,
923
+ totalTimeoutMs: 12e4,
924
+ abort: async (error) => stream.abort(error)
925
+ })) {
926
+ if (response !== void 0) throw new Error("sync_response_multiple");
927
+ response = value;
928
+ }
929
+ if (!isLibp2pSyncResponse(response)) throw new Error("invalid_sync_response");
930
+ if (response.id !== request.id) throw new Error("sync_response_id_mismatch");
931
+ if (!response.ok) throw new Error(response.error);
932
+ return response.result;
933
+ } catch (error) {
934
+ if (error instanceof JsonFrameError) await stream.abort(error);
935
+ throw error;
936
+ } finally {
937
+ await stream.close().catch(() => void 0);
938
+ }
375
939
  }
376
940
  };
377
- async function writeJsonToStream(stream, message) {
378
- const payload = new TextEncoder().encode(JSON.stringify(message));
379
- await stream.sink((async function* () {
380
- yield payload;
381
- })());
382
- }
383
- async function readJsonFromStream(stream) {
384
- const reader = stream.source[Symbol.asyncIterator]();
385
- const result = await reader.next();
386
- if (result.done || !result.value) throw new Error("sync_response_missing");
387
- return JSON.parse(new TextDecoder().decode(result.value));
388
- }
389
941
 
390
942
  // src/device-network/libp2pRpcProtocol.ts
391
- var LIBP2P_RPC_REQUEST_TYPE = "memeloop-rpc-request-v1";
392
- var LIBP2P_RPC_RESPONSE_TYPE = "memeloop-rpc-response-v1";
943
+ var LIBP2P_RPC_REQUEST_TYPE = "memeloop-rpc-request-v2";
944
+ var LIBP2P_RPC_RESPONSE_TYPE = "memeloop-rpc-response-v2";
393
945
  function isLibp2pRpcRequest(value) {
394
946
  if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
395
947
  const record = value;
@@ -404,7 +956,7 @@ function isLibp2pRpcResponse(value) {
404
956
  }
405
957
 
406
958
  // src/device-network/localTrustDeviceAuthorizer.ts
407
- var PAIRING_PROTOCOL = "/memeloop/pairing/1.0.0";
959
+ var PAIRING_PROTOCOL = "/memeloop/pairing/2.0.0";
408
960
  var LocalTrustDeviceAuthorizer = class {
409
961
  constructor(options = {}) {
410
962
  this.options = options;
@@ -430,7 +982,7 @@ var LocalTrustDeviceAuthorizer = class {
430
982
  };
431
983
 
432
984
  // src/device-network/pairingInvite.ts
433
- var DEVICE_PAIRING_INVITE_PROTOCOL = "memeloop-device-pairing-v1";
985
+ var DEVICE_PAIRING_INVITE_PROTOCOL = "memeloop-device-pairing-v2";
434
986
  var DEVICE_PAIRING_INVITE_TTL_MS = 5 * 6e4;
435
987
  var MAX_INVITE_LENGTH = 16 * 1024;
436
988
  var MAX_ADDRESSES = 8;
@@ -441,37 +993,82 @@ function requireText(value, name, maxLength) {
441
993
  }
442
994
  return trimmed;
443
995
  }
444
- function validateAddresses(addresses) {
996
+ function finalPeerId(address) {
997
+ const parts = address.split("/");
998
+ for (let index = parts.length - 2; index >= 0; index -= 1) {
999
+ if (parts[index] === "p2p") return parts[index + 1];
1000
+ }
1001
+ return void 0;
1002
+ }
1003
+ function validateAddresses(addresses, peerId) {
445
1004
  const unique = [...new Set(addresses.map((address) => address.trim()))].filter(Boolean);
446
1005
  if (unique.length === 0 || unique.length > MAX_ADDRESSES || unique.some(
447
- (address) => address.length > 2048 || !address.startsWith("/") || !address.includes("/ws") && !address.includes("/wss")
1006
+ (address) => address.length > 2048 || !address.startsWith("/") || !address.includes("/ws") && !address.includes("/wss") || finalPeerId(address) !== peerId
448
1007
  )) {
449
- throw new Error("device pairing invite requires WebSocket multiaddrs");
1008
+ throw new Error("device pairing invite requires PeerId-bound WebSocket multiaddrs");
450
1009
  }
451
1010
  return unique;
452
1011
  }
453
- function createDevicePairingInvite(device, options = {}) {
1012
+ function validateLifetime(createdAt, expiresAt, now) {
1013
+ if (!Number.isSafeInteger(createdAt) || !Number.isSafeInteger(expiresAt) || expiresAt <= createdAt || expiresAt - createdAt > DEVICE_PAIRING_INVITE_TTL_MS) {
1014
+ throw new Error("invalid device pairing invite lifetime");
1015
+ }
1016
+ if (expiresAt <= now) throw new Error("device pairing invite has expired");
1017
+ }
1018
+ function unsignedPayload(invite) {
1019
+ const peerId = requireText(invite.peerId, "peerId", 256);
1020
+ return {
1021
+ protocol: DEVICE_PAIRING_INVITE_PROTOCOL,
1022
+ peerId,
1023
+ publicKeyMultibase: requireText(invite.publicKeyMultibase, "publicKeyMultibase", 4096),
1024
+ deviceName: requireText(invite.deviceName, "deviceName", 256),
1025
+ multiaddrs: [...validateAddresses(invite.multiaddrs, peerId)].sort(),
1026
+ createdAt: invite.createdAt,
1027
+ expiresAt: invite.expiresAt
1028
+ };
1029
+ }
1030
+ function canonicalDevicePairingInviteBytes(invite) {
1031
+ const payload = unsignedPayload(invite);
1032
+ const canonical = [
1033
+ DEVICE_PAIRING_INVITE_PROTOCOL,
1034
+ `peerId=${payload.peerId}`,
1035
+ `publicKey=${payload.publicKeyMultibase}`,
1036
+ `deviceName=${payload.deviceName}`,
1037
+ `multiaddrs=${payload.multiaddrs.join(",")}`,
1038
+ `createdAt=${payload.createdAt}`,
1039
+ `expiresAt=${payload.expiresAt}`
1040
+ ].join("\n");
1041
+ return new TextEncoder().encode(canonical);
1042
+ }
1043
+ async function createDevicePairingInvite(device, options) {
454
1044
  const createdAt = options.now ?? Date.now();
455
1045
  const ttlMs = options.ttlMs ?? DEVICE_PAIRING_INVITE_TTL_MS;
456
- if (!Number.isSafeInteger(createdAt) || !Number.isSafeInteger(ttlMs) || ttlMs <= 0 || ttlMs > DEVICE_PAIRING_INVITE_TTL_MS) {
1046
+ if (!Number.isSafeInteger(createdAt) || !Number.isSafeInteger(ttlMs) || ttlMs <= 0) {
457
1047
  throw new Error("invalid device pairing invite lifetime");
458
1048
  }
459
- return {
1049
+ const payload = unsignedPayload({
460
1050
  protocol: DEVICE_PAIRING_INVITE_PROTOCOL,
461
- peerId: requireText(device.peerId, "peerId", 256),
462
- deviceName: requireText(device.displayName, "deviceName", 256),
463
- multiaddrs: validateAddresses(device.multiaddrs ?? []),
1051
+ peerId: device.peerId,
1052
+ publicKeyMultibase: device.publicKeyMultibase,
1053
+ deviceName: device.displayName,
1054
+ multiaddrs: device.multiaddrs ?? [],
464
1055
  createdAt,
465
1056
  expiresAt: createdAt + ttlMs
466
- };
1057
+ });
1058
+ validateLifetime(payload.createdAt, payload.expiresAt, createdAt - 1);
1059
+ const signature = requireText(
1060
+ await options.sign(canonicalDevicePairingInviteBytes(payload)),
1061
+ "signature",
1062
+ 4096
1063
+ );
1064
+ return { ...payload, signature };
467
1065
  }
468
1066
  function encodeDevicePairingInvite(invite) {
469
- const validated = parseDevicePairingInvite(JSON.stringify(invite), {
470
- now: invite.createdAt
471
- });
472
- return JSON.stringify(validated);
1067
+ const payload = unsignedPayload(invite);
1068
+ const signature = requireText(invite.signature, "signature", 4096);
1069
+ return JSON.stringify({ ...payload, signature });
473
1070
  }
474
- function parseDevicePairingInvite(serialized, options = {}) {
1071
+ async function parseDevicePairingInvite(serialized, options) {
475
1072
  if (!serialized || serialized.length > MAX_INVITE_LENGTH) {
476
1073
  throw new Error("invalid device pairing invite length");
477
1074
  }
@@ -485,20 +1082,30 @@ function parseDevicePairingInvite(serialized, options = {}) {
485
1082
  throw new Error("invalid device pairing invite");
486
1083
  }
487
1084
  const record = parsed;
488
- 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) {
1085
+ 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") {
489
1086
  throw new Error("invalid device pairing invite");
490
1087
  }
491
- if (record.expiresAt <= (options.now ?? Date.now())) {
492
- throw new Error("device pairing invite has expired");
493
- }
494
- return {
1088
+ validateLifetime(record.createdAt, record.expiresAt, options.now ?? Date.now());
1089
+ const payload = unsignedPayload({
495
1090
  protocol: DEVICE_PAIRING_INVITE_PROTOCOL,
496
- peerId: requireText(record.peerId, "peerId", 256),
497
- deviceName: requireText(record.deviceName, "deviceName", 256),
498
- multiaddrs: validateAddresses(record.multiaddrs),
1091
+ peerId: record.peerId,
1092
+ publicKeyMultibase: record.publicKeyMultibase,
1093
+ deviceName: record.deviceName,
1094
+ multiaddrs: record.multiaddrs,
499
1095
  createdAt: record.createdAt,
500
1096
  expiresAt: record.expiresAt
1097
+ });
1098
+ const invite = {
1099
+ ...payload,
1100
+ signature: requireText(record.signature, "signature", 4096)
501
1101
  };
1102
+ if (!await options.verifyIdentity({
1103
+ invite,
1104
+ payload: canonicalDevicePairingInviteBytes(payload)
1105
+ })) {
1106
+ throw new Error("device pairing invite identity verification failed");
1107
+ }
1108
+ return invite;
502
1109
  }
503
1110
 
504
1111
  // src/sync/attachmentRpcCodec.ts
@@ -527,43 +1134,66 @@ var ChatSyncEngine = class {
527
1134
  nodeId;
528
1135
  storage;
529
1136
  getPeers;
1137
+ stateStore;
530
1138
  versionVector = {};
1139
+ loadStatePromise;
1140
+ stateMutation = Promise.resolve();
531
1141
  constructor(options) {
532
1142
  this.nodeId = options.nodeId;
533
1143
  this.storage = options.storage;
534
1144
  this.getPeers = options.peers;
535
- this.versionVector[this.nodeId] = 0;
1145
+ this.stateStore = options.stateStore ?? new MemoryDeviceSyncStateStore();
536
1146
  }
537
1147
  /**
538
1148
  * 在本节点新产生一条消息时调用,增加本节点的 lamportClock。
539
1149
  */
540
1150
  bumpLocalVersion() {
541
- this.versionVector[this.nodeId] = (this.versionVector[this.nodeId] ?? 0) + 1;
1151
+ let nextClock = 0;
1152
+ const mutation = this.stateMutation.then(async () => {
1153
+ await this.ensureStateLoaded();
1154
+ nextClock = (this.versionVector[this.nodeId] ?? 0) + 1;
1155
+ this.versionVector[this.nodeId] = nextClock;
1156
+ await this.stateStore.saveVersionVector(this.versionVector);
1157
+ });
1158
+ this.stateMutation = mutation.catch(() => void 0);
1159
+ return mutation.then(() => nextClock);
542
1160
  }
543
1161
  /**
544
1162
  * 执行一次与所有 peer 的增量同步(metadata 级别),并尽力拉取缺失消息。
545
1163
  */
546
1164
  async syncOnce() {
1165
+ await this.ensureStateLoaded();
1166
+ await this.stateMutation;
1167
+ await this.reconcileVersionVectorFromStorage();
547
1168
  const peers = this.getPeers();
548
1169
  if (peers.length === 0) return;
549
- const localVersion = { ...this.versionVector };
550
1170
  const pulledConversations = /* @__PURE__ */ new Set();
551
1171
  for (const peer of peers) {
552
- const { remoteVersion, missingForRemote } = await peer.exchangeVersionVector(localVersion);
553
- const missingForLocal = await peer.pullMissingMetadata(this.versionVector);
554
- for (const meta of missingForLocal) {
555
- await this.storage.upsertConversationMetadata(meta);
556
- pulledConversations.add(meta.conversationId);
557
- }
1172
+ const { remoteVersion } = await peer.exchangeVersionVector({ ...this.versionVector });
1173
+ let cursor;
1174
+ do {
1175
+ const page = await peer.pullMissingMetadata(this.versionVector, cursor);
1176
+ for (const meta of page.items) {
1177
+ await this.storage.upsertConversationMetadata(meta);
1178
+ pulledConversations.add(meta.conversationId);
1179
+ }
1180
+ cursor = page.nextCursor;
1181
+ } while (cursor !== void 0);
558
1182
  for (const [nodeId, clock] of Object.entries(remoteVersion)) {
559
1183
  const current = this.versionVector[nodeId] ?? 0;
560
- if (clock > current) {
561
- this.versionVector[nodeId] = clock;
562
- }
1184
+ if (clock > current) this.versionVector[nodeId] = clock;
563
1185
  }
564
- if (missingForRemote.length > 0) {
565
- this.bumpLocalVersion();
1186
+ await this.stateStore.saveVersionVector(this.versionVector);
1187
+ }
1188
+ const pageSize = 100;
1189
+ let offset = 0;
1190
+ for (; ; ) {
1191
+ const conversations = await this.storage.listConversations({ limit: pageSize, offset });
1192
+ for (const conversation of conversations) {
1193
+ pulledConversations.add(conversation.conversationId);
566
1194
  }
1195
+ if (conversations.length < pageSize) break;
1196
+ offset += conversations.length;
567
1197
  }
568
1198
  for (const conversationId of pulledConversations) {
569
1199
  await this.pullMessagesForConversationFromPeers(conversationId, peers);
@@ -574,11 +1204,6 @@ var ChatSyncEngine = class {
574
1204
  */
575
1205
  async antiEntropyOnce() {
576
1206
  await this.syncOnce();
577
- const list = await this.storage.listConversations({ limit: 500 });
578
- const peers = this.getPeers();
579
- for (const meta of list) {
580
- await this.pullMessagesForConversationFromPeers(meta.conversationId, peers);
581
- }
582
1207
  }
583
1208
  async pullMessagesForConversationFromPeers(conversationId, peers) {
584
1209
  const localMsgs = await this.storage.getMessages(conversationId, { mode: "full-content" });
@@ -631,12 +1256,39 @@ var ChatSyncEngine = class {
631
1256
  }
632
1257
  }
633
1258
  }
634
- getVersionVector() {
1259
+ async getVersionVector() {
1260
+ await this.ensureStateLoaded();
1261
+ await this.stateMutation;
635
1262
  return { ...this.versionVector };
636
1263
  }
637
1264
  getStorage() {
638
1265
  return this.storage;
639
1266
  }
1267
+ ensureStateLoaded() {
1268
+ this.loadStatePromise ??= (async () => {
1269
+ const stored = await this.stateStore.loadVersionVector();
1270
+ this.versionVector = { ...stored, [this.nodeId]: stored[this.nodeId] ?? 0 };
1271
+ })();
1272
+ return this.loadStatePromise;
1273
+ }
1274
+ async reconcileVersionVectorFromStorage() {
1275
+ const pageSize = 100;
1276
+ let offset = 0;
1277
+ let changed = false;
1278
+ for (; ; ) {
1279
+ const conversations = await this.storage.listConversations({ limit: pageSize, offset });
1280
+ for (const conversation of conversations) {
1281
+ const current = this.versionVector[conversation.originNodeId] ?? 0;
1282
+ if (conversation.originClock > current) {
1283
+ this.versionVector[conversation.originNodeId] = conversation.originClock;
1284
+ changed = true;
1285
+ }
1286
+ }
1287
+ if (conversations.length < pageSize) break;
1288
+ offset += conversations.length;
1289
+ }
1290
+ if (changed) await this.stateStore.saveVersionVector(this.versionVector);
1291
+ }
640
1292
  };
641
1293
 
642
1294
  // src/sync/peerNodeAdapter.ts
@@ -650,8 +1302,8 @@ var PeerNodeSyncAdapter = class {
650
1302
  exchangeVersionVector(localVersion) {
651
1303
  return this.transport.exchangeVersionVector(this.nodeId, localVersion);
652
1304
  }
653
- pullMissingMetadata(sinceVersion) {
654
- return this.transport.pullMissingMetadata(this.nodeId, sinceVersion);
1305
+ pullMissingMetadata(sinceVersion, cursor) {
1306
+ return this.transport.pullMissingMetadata(this.nodeId, sinceVersion, cursor);
655
1307
  }
656
1308
  pullMissingMessages(conversationId, knownMessageIds) {
657
1309
  const function_ = this.transport.pullMissingMessages?.bind(this.transport);
@@ -673,7 +1325,17 @@ var PeerNodeSyncAdapter = class {
673
1325
  function isConversationMeta(value) {
674
1326
  if (value === null || typeof value !== "object") return false;
675
1327
  const o = value;
676
- 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";
1328
+ 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";
1329
+ }
1330
+ function computeMissingVersionRanges(currentVersion, availableVersion) {
1331
+ const ranges = [];
1332
+ for (const [originNodeId, availableClock] of Object.entries(availableVersion)) {
1333
+ const currentClock = currentVersion[originNodeId] ?? 0;
1334
+ if (availableClock > currentClock) {
1335
+ ranges.push({ originNodeId, fromExclusive: currentClock, toInclusive: availableClock });
1336
+ }
1337
+ }
1338
+ return ranges.sort((left, right) => left.originNodeId.localeCompare(right.originNodeId));
677
1339
  }
678
1340
 
679
1341
  // src/device-network/memoryDeviceNetworkService.ts
@@ -813,6 +1475,8 @@ var MemoryDeviceNetworkService = class {
813
1475
  async sink() {
814
1476
  },
815
1477
  async close() {
1478
+ },
1479
+ abort() {
816
1480
  }
817
1481
  };
818
1482
  }
@@ -847,11 +1511,21 @@ var MemoryDeviceNetworkService = class {
847
1511
  // src/device-network/syncCloudDevices.ts
848
1512
  async function syncCloudDevices(input) {
849
1513
  const cloudDevices = await input.cloudClient.listDevices();
850
- if (cloudDevices.length === 0) return [];
851
1514
  const existingRecords = await input.trustStore.loadTrustedDevices();
852
1515
  const existingByPeerId = new Map(existingRecords.map((r) => [r.peerId, r]));
853
- for (const cloudDevice of cloudDevices) {
1516
+ const excludedPeerIds = new Set(input.excludePeerIds ?? []);
1517
+ const activeCloudDevices = cloudDevices.filter(
1518
+ (device) => device.revokedAt === void 0 && !excludedPeerIds.has(device.peerId)
1519
+ );
1520
+ const visibleCloudPeerIds = new Set(activeCloudDevices.map((device) => device.peerId));
1521
+ for (const existing of existingRecords) {
1522
+ if (existing.trustMode === "cloud-account" && !visibleCloudPeerIds.has(existing.peerId)) {
1523
+ await input.trustStore.removeTrustedDevice(existing.peerId);
1524
+ }
1525
+ }
1526
+ for (const cloudDevice of activeCloudDevices) {
854
1527
  const existing = existingByPeerId.get(cloudDevice.peerId);
1528
+ if (existing?.trustMode === "local-pairing") continue;
855
1529
  const record = {
856
1530
  peerId: cloudDevice.peerId,
857
1531
  publicKeyMultibase: cloudDevice.publicKeyMultibase,
@@ -863,17 +1537,14 @@ async function syncCloudDevices(input) {
863
1537
  lastSeen: cloudDevice.lastSeen,
864
1538
  revokedAt: cloudDevice.revokedAt
865
1539
  };
866
- if (existing) {
867
- existing.lastSeen = cloudDevice.lastSeen;
868
- existing.revokedAt = cloudDevice.revokedAt;
869
- if (existing.publicKeyMultibase !== record.publicKeyMultibase || existing.deviceName !== record.deviceName || existing.platform !== record.platform) {
870
- await input.trustStore.saveTrustedDevice(record);
871
- }
872
- } else {
1540
+ if (!existing || !sameTrustedDevice(existing, record)) {
873
1541
  await input.trustStore.saveTrustedDevice(record);
874
1542
  }
875
1543
  }
876
- return cloudDevices;
1544
+ return activeCloudDevices;
1545
+ }
1546
+ function sameTrustedDevice(left, right) {
1547
+ return left.peerId === right.peerId && left.publicKeyMultibase === right.publicKeyMultibase && left.deviceName === right.deviceName && left.platform === right.platform && left.trustMode === right.trustMode && left.accountId === right.accountId && left.createdAt === right.createdAt && left.lastSeen === right.lastSeen && left.revokedAt === right.revokedAt;
877
1548
  }
878
1549
  // Annotate the CommonJS export names for ESM import in node:
879
1550
  0 && (module.exports = {
@@ -881,6 +1552,8 @@ async function syncCloudDevices(input) {
881
1552
  DEVICE_ORCHESTRATION_PROTOCOL,
882
1553
  DEVICE_PAIRING_INVITE_PROTOCOL,
883
1554
  DEVICE_PAIRING_INVITE_TTL_MS,
1555
+ DeviceCloudConnectionCoordinator,
1556
+ JsonFrameError,
884
1557
  LIBP2P_RPC_REQUEST_TYPE,
885
1558
  LIBP2P_RPC_RESPONSE_TYPE,
886
1559
  LIBP2P_SYNC_REQUEST_TYPE,
@@ -888,13 +1561,19 @@ async function syncCloudDevices(input) {
888
1561
  Libp2pDeviceSyncTransport,
889
1562
  LocalTrustDeviceAuthorizer,
890
1563
  MemoryDeviceNetworkService,
1564
+ MemoryDeviceSyncStateStore,
891
1565
  PeerNodeSyncAdapter,
892
1566
  attachmentBlobFromWire,
1567
+ canonicalDevicePairingInviteBytes,
1568
+ computeMissingVersionRanges,
893
1569
  createDeviceOrchestrationStreamHandler,
894
1570
  createDeviceOrchestrationTransport,
895
1571
  createDevicePairingInvite,
1572
+ createJsonFrameReader,
896
1573
  decodeAttachmentBlobRpc,
897
1574
  encodeDevicePairingInvite,
1575
+ encodeJsonFrame,
1576
+ encodeJsonFrames,
898
1577
  isConversationMeta,
899
1578
  isLibp2pRpcRequest,
900
1579
  isLibp2pRpcResponse,