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