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
package/dist/device-network.cjs
CHANGED
|
@@ -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/
|
|
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-
|
|
555
|
+
type: "memeloop-device-orchestration-request-v2",
|
|
172
556
|
request,
|
|
173
557
|
...grant ? { grant } : {}
|
|
174
558
|
};
|
|
175
|
-
await stream.sink(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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
|
-
|
|
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(
|
|
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-
|
|
288
|
-
var LIBP2P_SYNC_RESPONSE_TYPE = "memeloop-sync-response-
|
|
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", {
|
|
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/
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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-
|
|
392
|
-
var LIBP2P_RPC_RESPONSE_TYPE = "memeloop-rpc-response-
|
|
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/
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
1046
|
+
if (!Number.isSafeInteger(createdAt) || !Number.isSafeInteger(ttlMs) || ttlMs <= 0) {
|
|
457
1047
|
throw new Error("invalid device pairing invite lifetime");
|
|
458
1048
|
}
|
|
459
|
-
|
|
1049
|
+
const payload = unsignedPayload({
|
|
460
1050
|
protocol: DEVICE_PAIRING_INVITE_PROTOCOL,
|
|
461
|
-
peerId:
|
|
462
|
-
|
|
463
|
-
|
|
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
|
|
470
|
-
|
|
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" ||
|
|
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
|
-
|
|
492
|
-
|
|
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:
|
|
497
|
-
|
|
498
|
-
|
|
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.
|
|
1145
|
+
this.stateStore = options.stateStore ?? new MemoryDeviceSyncStateStore();
|
|
536
1146
|
}
|
|
537
1147
|
/**
|
|
538
1148
|
* 在本节点新产生一条消息时调用,增加本节点的 lamportClock。
|
|
539
1149
|
*/
|
|
540
1150
|
bumpLocalVersion() {
|
|
541
|
-
|
|
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
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
await this.
|
|
556
|
-
|
|
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
|
-
|
|
565
|
-
|
|
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
|
-
|
|
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
|
|
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,
|