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.
- package/dist/browser.cjs +1 -1
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.cts +15 -15
- package/dist/browser.d.ts +15 -15
- package/dist/browser.js +1 -1
- package/dist/{chunk-5JEV7632.js → chunk-5VYZV33O.js} +2 -1
- package/dist/{chunk-5JEV7632.js.map → chunk-5VYZV33O.js.map} +1 -1
- package/dist/chunk-E5R24U4I.js +1292 -0
- package/dist/chunk-E5R24U4I.js.map +1 -0
- package/dist/{chunk-MRKY4HGF.js → chunk-EPHRIRAL.js} +20 -11
- package/dist/chunk-EPHRIRAL.js.map +1 -0
- package/dist/{chunk-P6L3444O.js → chunk-KV6TC5UJ.js} +2 -2
- package/dist/chunk-MXRXYGWW.js +1 -0
- package/dist/{chunk-CCEFAOSH.js → chunk-WHXFUMDE.js} +2 -2
- package/dist/{chunk-CCEFAOSH.js.map → chunk-WHXFUMDE.js.map} +1 -1
- package/dist/conversation.d.cts +3 -3
- package/dist/conversation.d.ts +3 -3
- package/dist/device-network-portable.cjs +814 -144
- package/dist/device-network-portable.cjs.map +1 -1
- package/dist/device-network-portable.d.cts +24 -14
- package/dist/device-network-portable.d.ts +24 -14
- package/dist/device-network-portable.js +18 -2
- package/dist/device-network.cjs +833 -154
- package/dist/device-network.cjs.map +1 -1
- package/dist/device-network.d.cts +5 -5
- package/dist/device-network.d.ts +5 -5
- package/dist/device-network.js +19 -3
- package/dist/{fetchProvider-WWnWnQFP.d.ts → fetchProvider-BhqRqi5-.d.ts} +1 -1
- package/dist/{fetchProvider-toTHy5Nu.d.cts → fetchProvider-HJ_FkORN.d.cts} +1 -1
- package/dist/index.cjs +841 -157
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -16
- package/dist/index.d.ts +16 -16
- package/dist/index.js +26 -8
- package/dist/index.js.map +1 -1
- package/dist/{interface-IrKBFtL1.d.ts → interface-3C3eG8Hn.d.cts} +13 -1
- package/dist/{interface-IrKBFtL1.d.cts → interface-3C3eG8Hn.d.ts} +13 -1
- package/dist/llm-providers.d.cts +2 -2
- package/dist/llm-providers.d.ts +2 -2
- package/dist/loop-api.cjs +1 -0
- package/dist/loop-api.cjs.map +1 -1
- package/dist/loop-api.d.cts +7 -7
- package/dist/loop-api.d.ts +7 -7
- package/dist/loop-api.js +2 -2
- package/dist/mobile.d.cts +5 -5
- package/dist/mobile.d.ts +5 -5
- package/dist/{orchestration-portable-BHB_YrNN.d.ts → orchestration-portable-B4udCSVd.d.ts} +4 -4
- package/dist/{orchestration-portable-mvYBvGBb.d.cts → orchestration-portable-BWaCsCGK.d.cts} +4 -4
- package/dist/orchestration-portable.cjs +1 -1
- package/dist/orchestration-portable.cjs.map +1 -1
- package/dist/orchestration-portable.d.cts +5 -5
- package/dist/orchestration-portable.d.ts +5 -5
- package/dist/orchestration-portable.js +1 -1
- package/dist/pairingInvite-DENOiqa0.d.cts +271 -0
- package/dist/pairingInvite-DZy77i0p.d.ts +271 -0
- package/dist/{promptConcat-0WNFTg4l.d.ts → promptConcat-CUEIA_3T.d.ts} +3 -3
- package/dist/{promptConcat-D6wr5V-V.d.cts → promptConcat-VLe5SEN4.d.cts} +3 -3
- package/dist/{providerRegistry-CPsktuiX.d.cts → providerRegistry-BDGGs5gz.d.cts} +2 -2
- package/dist/{providerRegistry-Bi6pAvSj.d.ts → providerRegistry-CaFK_aUd.d.ts} +2 -2
- package/dist/{registry-D0kbxksn.d.cts → registry-DNSEVYug.d.cts} +3 -3
- package/dist/{registry-DDbvhlhs.d.ts → registry-yGnYHWhq.d.ts} +3 -3
- package/dist/{remoteClient-CBggnRy3.d.cts → remoteClient-DFIyCZEv.d.cts} +2 -2
- package/dist/{remoteClient-B_spK019.d.ts → remoteClient-DwBCoHpc.d.ts} +2 -2
- package/dist/{runtime-UONPTAYK.js → runtime-6EELHWVB.js} +2 -2
- package/dist/{scriptDeploymentPipeline-CXJcC82H.d.ts → scriptDeploymentPipeline-B1q-UhrO.d.ts} +1 -1
- package/dist/{scriptDeploymentPipeline-yG1ZhVVI.d.cts → scriptDeploymentPipeline-XJzQyYys.d.cts} +1 -1
- package/dist/{syncCloudDevices-Ds_Kcgrv.d.cts → syncCloudDevices-BFw_c1g5.d.cts} +6 -5
- package/dist/{syncCloudDevices-BEqwgrhD.d.ts → syncCloudDevices-C8fCUhnX.d.ts} +6 -5
- package/dist/{types-CdFVSntj.d.ts → types-C8gnhG9M.d.cts} +6 -4
- package/dist/{types-7pYti-k9.d.cts → types-CcFFIk8W.d.ts} +6 -4
- package/package.json +1 -1
- package/dist/chunk-426CJUZP.js +0 -630
- package/dist/chunk-426CJUZP.js.map +0 -1
- package/dist/chunk-MRKY4HGF.js.map +0 -1
- package/dist/chunk-VZKQX5PP.js +0 -1
- package/dist/pairingInvite-BXPWY3aM.d.cts +0 -149
- package/dist/pairingInvite-Cuz0wuq9.d.ts +0 -149
- /package/dist/{chunk-P6L3444O.js.map → chunk-KV6TC5UJ.js.map} +0 -0
- /package/dist/{chunk-VZKQX5PP.js.map → chunk-MXRXYGWW.js.map} +0 -0
- /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/
|
|
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-
|
|
553
|
+
type: "memeloop-device-orchestration-request-v2",
|
|
170
554
|
request,
|
|
171
555
|
...grant ? { grant } : {}
|
|
172
556
|
};
|
|
173
|
-
await stream.sink(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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
|
-
|
|
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(
|
|
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-
|
|
286
|
-
var LIBP2P_SYNC_RESPONSE_TYPE = "memeloop-sync-response-
|
|
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", {
|
|
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/
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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-
|
|
390
|
-
var LIBP2P_RPC_RESPONSE_TYPE = "memeloop-rpc-response-
|
|
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/
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
1044
|
+
if (!Number.isSafeInteger(createdAt) || !Number.isSafeInteger(ttlMs) || ttlMs <= 0) {
|
|
455
1045
|
throw new Error("invalid device pairing invite lifetime");
|
|
456
1046
|
}
|
|
457
|
-
|
|
1047
|
+
const payload = unsignedPayload({
|
|
458
1048
|
protocol: DEVICE_PAIRING_INVITE_PROTOCOL,
|
|
459
|
-
peerId:
|
|
460
|
-
|
|
461
|
-
|
|
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
|
|
468
|
-
|
|
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" ||
|
|
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
|
-
|
|
490
|
-
|
|
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:
|
|
495
|
-
|
|
496
|
-
|
|
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.
|
|
1143
|
+
this.stateStore = options.stateStore ?? new MemoryDeviceSyncStateStore();
|
|
534
1144
|
}
|
|
535
1145
|
/**
|
|
536
1146
|
* 在本节点新产生一条消息时调用,增加本节点的 lamportClock。
|
|
537
1147
|
*/
|
|
538
1148
|
bumpLocalVersion() {
|
|
539
|
-
|
|
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
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
await this.
|
|
554
|
-
|
|
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
|
-
|
|
563
|
-
|
|
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,
|