memeloop 0.1.2 → 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-MAJWHAKR.js → chunk-5VYZV33O.js} +3 -2
- package/dist/{chunk-MAJWHAKR.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-KFS2EJT6.js → chunk-K5KZJNDP.js} +2 -2
- package/dist/{chunk-2C6BJZI3.js → chunk-KV6TC5UJ.js} +3 -3
- package/dist/{chunk-T7FSXWFR.js → chunk-LGUGYRR4.js} +4 -4
- package/dist/{chunk-T7FSXWFR.js.map → chunk-LGUGYRR4.js.map} +1 -1
- 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 +785 -158
- 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 +4 -3
- 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 +4 -4
- package/dist/mobile.cjs +3 -3
- package/dist/mobile.cjs.map +1 -1
- package/dist/mobile.d.cts +5 -5
- package/dist/mobile.d.ts +5 -5
- package/dist/mobile.js +2 -2
- 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-PGWR6FIV.js → runtime-6EELHWVB.js} +3 -3
- 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-KFS2EJT6.js.map → chunk-K5KZJNDP.js.map} +0 -0
- /package/dist/{chunk-2C6BJZI3.js.map → chunk-KV6TC5UJ.js.map} +0 -0
- /package/dist/{chunk-VZKQX5PP.js.map → chunk-MXRXYGWW.js.map} +0 -0
- /package/dist/{runtime-PGWR6FIV.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,224 @@ __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
|
+
this.controller.abort(new Error("device cloud coordinator stopped"));
|
|
128
|
+
await this.inFlight?.catch(() => void 0);
|
|
129
|
+
}
|
|
130
|
+
async setConfiguration(configuration) {
|
|
131
|
+
this.configuration = configuration;
|
|
132
|
+
this.generation += 1;
|
|
133
|
+
this.consecutiveFailures = 0;
|
|
134
|
+
this.clearTimer();
|
|
135
|
+
this.controller.abort(new Error("device cloud configuration changed"));
|
|
136
|
+
this.controller = new AbortController();
|
|
137
|
+
this.inFlight = void 0;
|
|
138
|
+
this.resetSnapshot();
|
|
139
|
+
if (this.started && this.options.adapter.isConfigured(configuration)) await this.runNow();
|
|
140
|
+
}
|
|
141
|
+
runNow() {
|
|
142
|
+
if (this.inFlight) return this.inFlight;
|
|
143
|
+
if (!this.options.adapter.isConfigured(this.configuration)) {
|
|
144
|
+
this.resetSnapshot();
|
|
145
|
+
return Promise.resolve();
|
|
146
|
+
}
|
|
147
|
+
const generation = this.generation;
|
|
148
|
+
const configuration = this.configuration;
|
|
149
|
+
const signal = this.controller.signal;
|
|
150
|
+
const run = this.maintain(configuration, generation, signal);
|
|
151
|
+
const tracked = run.finally(() => {
|
|
152
|
+
if (this.inFlight === tracked) this.inFlight = void 0;
|
|
153
|
+
});
|
|
154
|
+
this.inFlight = tracked;
|
|
155
|
+
return tracked;
|
|
156
|
+
}
|
|
157
|
+
async maintain(configuration, generation, signal) {
|
|
158
|
+
if (!this.isCurrent(generation, signal)) return;
|
|
159
|
+
await this.updateSnapshot(generation, {
|
|
160
|
+
status: "connecting",
|
|
161
|
+
lastError: void 0,
|
|
162
|
+
nextRetryAt: void 0
|
|
163
|
+
});
|
|
164
|
+
try {
|
|
165
|
+
if (this.snapshotValue.components.authorizer !== "ready") {
|
|
166
|
+
await this.runStep("authorizer", generation, signal, () => this.options.adapter.ensureAuthorizer(configuration, signal));
|
|
167
|
+
}
|
|
168
|
+
if (this.snapshotValue.components.registration !== "ready") {
|
|
169
|
+
await this.runStep("registration", generation, signal, () => this.options.adapter.registerDevice(configuration, signal));
|
|
170
|
+
}
|
|
171
|
+
let relayError;
|
|
172
|
+
try {
|
|
173
|
+
await this.runStep("relay", generation, signal, () => this.options.adapter.ensureRelay(configuration, signal));
|
|
174
|
+
} catch (error) {
|
|
175
|
+
relayError = error;
|
|
176
|
+
this.options.logWarning?.("Cloud relay maintenance failed", error);
|
|
177
|
+
}
|
|
178
|
+
await this.runStep("heartbeat", generation, signal, () => this.options.adapter.heartbeat(configuration, signal));
|
|
179
|
+
let directoryError;
|
|
180
|
+
try {
|
|
181
|
+
await this.runStep("directory", generation, signal, () => this.options.adapter.syncDirectory(configuration, signal));
|
|
182
|
+
} catch (error) {
|
|
183
|
+
directoryError = error;
|
|
184
|
+
this.options.logWarning?.("Cloud directory synchronization failed", error);
|
|
185
|
+
}
|
|
186
|
+
if (!this.isCurrent(generation, signal)) return;
|
|
187
|
+
this.consecutiveFailures = 0;
|
|
188
|
+
const relayRequired = this.options.adapter.relayRequiredForOnline(configuration);
|
|
189
|
+
await this.updateSnapshot(generation, {
|
|
190
|
+
status: directoryError || relayRequired && relayError ? "degraded" : "online",
|
|
191
|
+
lastError: directoryError ?? relayError,
|
|
192
|
+
nextRetryAt: this.now() + this.heartbeatIntervalMs
|
|
193
|
+
});
|
|
194
|
+
this.schedule(this.heartbeatIntervalMs, generation);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (!this.isCurrent(generation, signal)) return;
|
|
197
|
+
this.consecutiveFailures += 1;
|
|
198
|
+
if (this.snapshotValue.components.heartbeat === "failed") {
|
|
199
|
+
this.snapshotValue.components.registration = "not-run";
|
|
200
|
+
}
|
|
201
|
+
const status = this.options.adapter.classifyError?.(error) ?? "offline";
|
|
202
|
+
const delay = this.backoffDelay();
|
|
203
|
+
await this.updateSnapshot(generation, {
|
|
204
|
+
status,
|
|
205
|
+
lastError: error,
|
|
206
|
+
nextRetryAt: this.now() + delay
|
|
207
|
+
});
|
|
208
|
+
this.schedule(delay, generation);
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async runStep(component, generation, signal, operation) {
|
|
213
|
+
if (!this.isCurrent(generation, signal)) return;
|
|
214
|
+
await this.setComponent(component, "pending", generation);
|
|
215
|
+
try {
|
|
216
|
+
const result = await operation();
|
|
217
|
+
if (!this.isCurrent(generation, signal)) return;
|
|
218
|
+
await result?.commit?.();
|
|
219
|
+
if (!this.isCurrent(generation, signal)) return;
|
|
220
|
+
await this.setComponent(component, "ready", generation);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
if (this.isCurrent(generation, signal)) await this.setComponent(component, "failed", generation);
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
backoffDelay() {
|
|
227
|
+
const exponential = Math.min(
|
|
228
|
+
this.maxBackoffMs,
|
|
229
|
+
this.initialBackoffMs * 2 ** Math.max(0, this.consecutiveFailures - 1)
|
|
230
|
+
);
|
|
231
|
+
const jitter = exponential * this.jitterRatio * (this.random() * 2 - 1);
|
|
232
|
+
return Math.max(0, Math.round(exponential + jitter));
|
|
233
|
+
}
|
|
234
|
+
schedule(delayMs, generation) {
|
|
235
|
+
if (!this.started || generation !== this.generation) return;
|
|
236
|
+
this.clearTimer();
|
|
237
|
+
this.timer = setTimeout(() => {
|
|
238
|
+
if (generation !== this.generation) return;
|
|
239
|
+
void this.runNow().catch((error) => {
|
|
240
|
+
this.options.logWarning?.("Cloud maintenance failed", error);
|
|
241
|
+
});
|
|
242
|
+
}, delayMs);
|
|
243
|
+
}
|
|
244
|
+
clearTimer() {
|
|
245
|
+
if (this.timer) clearTimeout(this.timer);
|
|
246
|
+
this.timer = void 0;
|
|
247
|
+
}
|
|
248
|
+
isCurrent(generation, signal) {
|
|
249
|
+
return generation === this.generation && !signal.aborted;
|
|
250
|
+
}
|
|
251
|
+
async setComponent(component, status, generation) {
|
|
252
|
+
if (generation !== this.generation) return;
|
|
253
|
+
this.snapshotValue = {
|
|
254
|
+
...this.snapshotValue,
|
|
255
|
+
components: { ...this.snapshotValue.components, [component]: status }
|
|
256
|
+
};
|
|
257
|
+
await this.emit();
|
|
258
|
+
}
|
|
259
|
+
async updateSnapshot(generation, update) {
|
|
260
|
+
if (generation !== this.generation) return;
|
|
261
|
+
this.snapshotValue = { ...this.snapshotValue, ...update };
|
|
262
|
+
await this.emit();
|
|
263
|
+
}
|
|
264
|
+
resetSnapshot() {
|
|
265
|
+
this.snapshotValue = {
|
|
266
|
+
status: this.options.adapter.isConfigured(this.configuration) ? "offline" : "not-configured",
|
|
267
|
+
generation: this.generation,
|
|
268
|
+
components: emptyComponents()
|
|
269
|
+
};
|
|
270
|
+
void this.emit();
|
|
271
|
+
}
|
|
272
|
+
async emit() {
|
|
273
|
+
await this.options.onStatus?.(this.snapshot);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
51
277
|
// src/orchestration/errors.ts
|
|
52
278
|
var OrchestrationError = class extends Error {
|
|
53
279
|
code;
|
|
@@ -76,9 +302,222 @@ var OrchestrationError = class extends Error {
|
|
|
76
302
|
}
|
|
77
303
|
};
|
|
78
304
|
|
|
305
|
+
// src/device-network/jsonFrame.ts
|
|
306
|
+
var JsonFrameError = class extends Error {
|
|
307
|
+
code;
|
|
308
|
+
constructor(code, message, options) {
|
|
309
|
+
super(message, options);
|
|
310
|
+
this.name = "JsonFrameError";
|
|
311
|
+
this.code = code;
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
var HEADER_BYTES = 4;
|
|
315
|
+
var MAX_UINT32 = 4294967295;
|
|
316
|
+
function assertPositiveInteger(value, name) {
|
|
317
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
318
|
+
throw new TypeError(`${name} must be a positive safe integer`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function validateMaxPayloadBytes(maxPayloadBytes) {
|
|
322
|
+
assertPositiveInteger(maxPayloadBytes, "maxPayloadBytes");
|
|
323
|
+
if (maxPayloadBytes > MAX_UINT32) {
|
|
324
|
+
throw new TypeError("maxPayloadBytes must fit in an unsigned 32-bit frame length");
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function encodeJsonFrame(value, maxPayloadBytes = MAX_UINT32) {
|
|
328
|
+
validateMaxPayloadBytes(maxPayloadBytes);
|
|
329
|
+
let serialized;
|
|
330
|
+
try {
|
|
331
|
+
serialized = JSON.stringify(value);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
throw new JsonFrameError("INVALID_JSON", "JSON frame value cannot be serialized", {
|
|
334
|
+
cause: error
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
if (serialized === void 0) {
|
|
338
|
+
throw new JsonFrameError("INVALID_JSON", "JSON frame value cannot be serialized");
|
|
339
|
+
}
|
|
340
|
+
const payload = new TextEncoder().encode(serialized);
|
|
341
|
+
if (payload.byteLength > maxPayloadBytes) {
|
|
342
|
+
throw new JsonFrameError(
|
|
343
|
+
"FRAME_TOO_LARGE",
|
|
344
|
+
`JSON frame payload exceeds ${maxPayloadBytes} bytes`
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
const frame = new Uint8Array(HEADER_BYTES + payload.byteLength);
|
|
348
|
+
new DataView(frame.buffer, frame.byteOffset, HEADER_BYTES).setUint32(0, payload.byteLength, false);
|
|
349
|
+
frame.set(payload, HEADER_BYTES);
|
|
350
|
+
return frame;
|
|
351
|
+
}
|
|
352
|
+
async function* encodeJsonFrames(values, maxPayloadBytes = MAX_UINT32) {
|
|
353
|
+
for await (const value of values) {
|
|
354
|
+
yield encodeJsonFrame(value, maxPayloadBytes);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function concatBytes(left, right) {
|
|
358
|
+
if (left.byteLength === 0) return right.slice();
|
|
359
|
+
if (right.byteLength === 0) return left;
|
|
360
|
+
const combined = new Uint8Array(left.byteLength + right.byteLength);
|
|
361
|
+
combined.set(left);
|
|
362
|
+
combined.set(right, left.byteLength);
|
|
363
|
+
return combined;
|
|
364
|
+
}
|
|
365
|
+
function abortedError(signal) {
|
|
366
|
+
return new JsonFrameError("ABORTED", "JSON frame reading was aborted", {
|
|
367
|
+
cause: signal?.reason
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
async function readNextChunk(iterator, idleDeadline, totalDeadline, signal) {
|
|
371
|
+
if (signal?.aborted) throw abortedError(signal);
|
|
372
|
+
const now = Date.now();
|
|
373
|
+
const deadline = totalDeadline === void 0 ? idleDeadline : Math.min(idleDeadline, totalDeadline);
|
|
374
|
+
if (deadline <= now) {
|
|
375
|
+
throw new JsonFrameError(
|
|
376
|
+
totalDeadline !== void 0 && totalDeadline <= idleDeadline ? "TOTAL_TIMEOUT" : "IDLE_TIMEOUT",
|
|
377
|
+
totalDeadline !== void 0 && totalDeadline <= idleDeadline ? "JSON frame total timeout expired" : "JSON frame idle timeout expired"
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
return new Promise((resolve, reject) => {
|
|
381
|
+
let settled = false;
|
|
382
|
+
const finish = (callback) => {
|
|
383
|
+
if (settled) return;
|
|
384
|
+
settled = true;
|
|
385
|
+
clearTimeout(timer);
|
|
386
|
+
signal?.removeEventListener("abort", onAbort);
|
|
387
|
+
callback();
|
|
388
|
+
};
|
|
389
|
+
const onAbort = () => {
|
|
390
|
+
finish(() => {
|
|
391
|
+
reject(abortedError(signal));
|
|
392
|
+
});
|
|
393
|
+
};
|
|
394
|
+
const timer = setTimeout(() => {
|
|
395
|
+
const currentTime = Date.now();
|
|
396
|
+
const totalExpired = totalDeadline !== void 0 && currentTime >= totalDeadline;
|
|
397
|
+
finish(() => {
|
|
398
|
+
reject(
|
|
399
|
+
new JsonFrameError(
|
|
400
|
+
totalExpired ? "TOTAL_TIMEOUT" : "IDLE_TIMEOUT",
|
|
401
|
+
totalExpired ? "JSON frame total timeout expired" : "JSON frame idle timeout expired"
|
|
402
|
+
)
|
|
403
|
+
);
|
|
404
|
+
});
|
|
405
|
+
}, Math.max(1, deadline - now));
|
|
406
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
407
|
+
void iterator.next().then(
|
|
408
|
+
(result) => {
|
|
409
|
+
finish(() => {
|
|
410
|
+
resolve(result);
|
|
411
|
+
});
|
|
412
|
+
},
|
|
413
|
+
(error) => {
|
|
414
|
+
finish(() => {
|
|
415
|
+
reject(
|
|
416
|
+
error instanceof Error ? error : new Error("JSON frame source failed", {
|
|
417
|
+
cause: error
|
|
418
|
+
})
|
|
419
|
+
);
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
);
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
function createJsonFrameReader(source, options) {
|
|
426
|
+
validateMaxPayloadBytes(options.maxPayloadBytes);
|
|
427
|
+
assertPositiveInteger(options.idleTimeoutMs, "idleTimeoutMs");
|
|
428
|
+
assertPositiveInteger(options.totalTimeoutMs, "totalTimeoutMs");
|
|
429
|
+
return (async function* readFrames() {
|
|
430
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
431
|
+
let buffered = new Uint8Array(0);
|
|
432
|
+
let expectedPayloadBytes;
|
|
433
|
+
let idleDeadline = Date.now() + options.idleTimeoutMs;
|
|
434
|
+
let totalDeadline;
|
|
435
|
+
let abortCalled = false;
|
|
436
|
+
const abortOnce = async (error) => {
|
|
437
|
+
if (abortCalled) return;
|
|
438
|
+
abortCalled = true;
|
|
439
|
+
await Promise.resolve(options.abort?.(error)).catch(() => void 0);
|
|
440
|
+
};
|
|
441
|
+
try {
|
|
442
|
+
for (; ; ) {
|
|
443
|
+
if (expectedPayloadBytes === void 0 && buffered.byteLength >= HEADER_BYTES) {
|
|
444
|
+
expectedPayloadBytes = new DataView(
|
|
445
|
+
buffered.buffer,
|
|
446
|
+
buffered.byteOffset,
|
|
447
|
+
HEADER_BYTES
|
|
448
|
+
).getUint32(0, false);
|
|
449
|
+
buffered = buffered.slice(HEADER_BYTES);
|
|
450
|
+
totalDeadline ??= Date.now() + options.totalTimeoutMs;
|
|
451
|
+
if (expectedPayloadBytes > options.maxPayloadBytes) {
|
|
452
|
+
throw new JsonFrameError(
|
|
453
|
+
"FRAME_TOO_LARGE",
|
|
454
|
+
`JSON frame payload exceeds ${options.maxPayloadBytes} bytes`
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (expectedPayloadBytes !== void 0 && buffered.byteLength >= expectedPayloadBytes) {
|
|
459
|
+
const payload = buffered.slice(0, expectedPayloadBytes);
|
|
460
|
+
buffered = buffered.slice(expectedPayloadBytes);
|
|
461
|
+
expectedPayloadBytes = void 0;
|
|
462
|
+
totalDeadline = buffered.byteLength > 0 ? Date.now() + options.totalTimeoutMs : void 0;
|
|
463
|
+
let text;
|
|
464
|
+
try {
|
|
465
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(payload);
|
|
466
|
+
} catch (error) {
|
|
467
|
+
throw new JsonFrameError("INVALID_UTF8", "JSON frame payload is not valid UTF-8", {
|
|
468
|
+
cause: error
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
let value;
|
|
472
|
+
try {
|
|
473
|
+
value = JSON.parse(text);
|
|
474
|
+
} catch (error) {
|
|
475
|
+
throw new JsonFrameError("INVALID_JSON", "JSON frame payload is not valid JSON", {
|
|
476
|
+
cause: error
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
yield value;
|
|
480
|
+
idleDeadline = Date.now() + options.idleTimeoutMs;
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
const result = await readNextChunk(
|
|
484
|
+
iterator,
|
|
485
|
+
idleDeadline,
|
|
486
|
+
totalDeadline,
|
|
487
|
+
options.signal
|
|
488
|
+
);
|
|
489
|
+
if (result.done) {
|
|
490
|
+
if (buffered.byteLength > 0 || expectedPayloadBytes !== void 0) {
|
|
491
|
+
throw new JsonFrameError("TRUNCATED_FRAME", "JSON frame stream ended mid-frame");
|
|
492
|
+
}
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
if (!(result.value instanceof Uint8Array)) {
|
|
496
|
+
throw new TypeError("JSON frame source must yield Uint8Array chunks");
|
|
497
|
+
}
|
|
498
|
+
if (result.value.byteLength === 0) continue;
|
|
499
|
+
const now = Date.now();
|
|
500
|
+
idleDeadline = now + options.idleTimeoutMs;
|
|
501
|
+
totalDeadline ??= now + options.totalTimeoutMs;
|
|
502
|
+
buffered = concatBytes(buffered, result.value);
|
|
503
|
+
}
|
|
504
|
+
} catch (error) {
|
|
505
|
+
const frameError = error instanceof JsonFrameError ? error : new JsonFrameError("ABORTED", "JSON frame source failed", { cause: error });
|
|
506
|
+
await abortOnce(frameError);
|
|
507
|
+
throw frameError;
|
|
508
|
+
} finally {
|
|
509
|
+
void iterator.return?.().catch(() => void 0);
|
|
510
|
+
}
|
|
511
|
+
})();
|
|
512
|
+
}
|
|
513
|
+
|
|
79
514
|
// src/device-network/deviceOrchestrationTransport.ts
|
|
80
|
-
var DEVICE_ORCHESTRATION_PROTOCOL = "/memeloop/orchestration/
|
|
515
|
+
var DEVICE_ORCHESTRATION_PROTOCOL = "/memeloop/orchestration/2.0.0";
|
|
81
516
|
var DEFAULT_MAX_FRAME_BYTES = 1024 * 1024;
|
|
517
|
+
var REQUEST_IDLE_TIMEOUT_MS = 1e4;
|
|
518
|
+
var REQUEST_TOTAL_TIMEOUT_MS = 3e4;
|
|
519
|
+
var WATCH_IDLE_TIMEOUT_MS = 9e4;
|
|
520
|
+
var WATCH_BOOKMARK_INTERVAL_MS = 3e4;
|
|
82
521
|
function orchestrationTransportError(code, message) {
|
|
83
522
|
return new OrchestrationError({
|
|
84
523
|
code,
|
|
@@ -91,65 +530,6 @@ function assertFrameLimit(limit) {
|
|
|
91
530
|
throw new TypeError("maxFrameBytes must be a safe integer of at least 256");
|
|
92
531
|
}
|
|
93
532
|
}
|
|
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
533
|
function throwIfAborted(signal) {
|
|
154
534
|
if (signal?.aborted) {
|
|
155
535
|
throw orchestrationTransportError("CANCELLED", "device orchestration request was cancelled");
|
|
@@ -168,11 +548,41 @@ async function openAuthenticatedStream(options) {
|
|
|
168
548
|
}
|
|
169
549
|
async function writeRequest(stream, request, grant, maxFrameBytes) {
|
|
170
550
|
const envelope = {
|
|
171
|
-
type: "memeloop-device-orchestration-request-
|
|
551
|
+
type: "memeloop-device-orchestration-request-v2",
|
|
172
552
|
request,
|
|
173
553
|
...grant ? { grant } : {}
|
|
174
554
|
};
|
|
175
|
-
await stream.sink(
|
|
555
|
+
await stream.sink(encodeJsonFrames([envelope], maxFrameBytes));
|
|
556
|
+
}
|
|
557
|
+
function abortStreamOnce(stream) {
|
|
558
|
+
let aborted = false;
|
|
559
|
+
return async (error) => {
|
|
560
|
+
if (aborted) return;
|
|
561
|
+
aborted = true;
|
|
562
|
+
await stream.abort(error);
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
function frameReader(stream, maxFrameBytes, idleTimeoutMs, signal, abort) {
|
|
566
|
+
return createJsonFrameReader(stream.source, {
|
|
567
|
+
maxPayloadBytes: maxFrameBytes,
|
|
568
|
+
idleTimeoutMs,
|
|
569
|
+
totalTimeoutMs: REQUEST_TOTAL_TIMEOUT_MS,
|
|
570
|
+
signal,
|
|
571
|
+
abort
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
function mapFrameError(error) {
|
|
575
|
+
if (!(error instanceof JsonFrameError)) throw error;
|
|
576
|
+
if (error.code === "ABORTED") {
|
|
577
|
+
throw orchestrationTransportError("CANCELLED", error.message);
|
|
578
|
+
}
|
|
579
|
+
if (error.code === "FRAME_TOO_LARGE") {
|
|
580
|
+
throw orchestrationTransportError("EXHAUSTED", error.message);
|
|
581
|
+
}
|
|
582
|
+
if (error.code === "IDLE_TIMEOUT" || error.code === "TOTAL_TIMEOUT") {
|
|
583
|
+
throw orchestrationTransportError("UNAVAILABLE", error.message);
|
|
584
|
+
}
|
|
585
|
+
throw orchestrationTransportError("INVALID", error.message);
|
|
176
586
|
}
|
|
177
587
|
function createDeviceOrchestrationTransport(options) {
|
|
178
588
|
const maxFrameBytes = options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES;
|
|
@@ -181,8 +591,9 @@ function createDeviceOrchestrationTransport(options) {
|
|
|
181
591
|
async request(request, transportOptions = {}) {
|
|
182
592
|
throwIfAborted(transportOptions.signal);
|
|
183
593
|
const { stream, grant } = await openAuthenticatedStream(options);
|
|
594
|
+
const abort = abortStreamOnce(stream);
|
|
184
595
|
const closeOnAbort = () => {
|
|
185
|
-
void
|
|
596
|
+
void abort(orchestrationTransportError("CANCELLED", "device orchestration request was cancelled"));
|
|
186
597
|
};
|
|
187
598
|
transportOptions.signal?.addEventListener("abort", closeOnAbort, {
|
|
188
599
|
once: true
|
|
@@ -190,7 +601,13 @@ function createDeviceOrchestrationTransport(options) {
|
|
|
190
601
|
try {
|
|
191
602
|
await writeRequest(stream, request, grant, maxFrameBytes);
|
|
192
603
|
let response;
|
|
193
|
-
for await (const value of
|
|
604
|
+
for await (const value of frameReader(
|
|
605
|
+
stream,
|
|
606
|
+
maxFrameBytes,
|
|
607
|
+
REQUEST_IDLE_TIMEOUT_MS,
|
|
608
|
+
transportOptions.signal,
|
|
609
|
+
abort
|
|
610
|
+
)) {
|
|
194
611
|
if (response) {
|
|
195
612
|
throw orchestrationTransportError(
|
|
196
613
|
"INVALID",
|
|
@@ -207,6 +624,9 @@ function createDeviceOrchestrationTransport(options) {
|
|
|
207
624
|
);
|
|
208
625
|
}
|
|
209
626
|
return response;
|
|
627
|
+
} catch (error) {
|
|
628
|
+
if (error instanceof JsonFrameError) await abort(error);
|
|
629
|
+
mapFrameError(error);
|
|
210
630
|
} finally {
|
|
211
631
|
transportOptions.signal?.removeEventListener("abort", closeOnAbort);
|
|
212
632
|
await stream.close().catch(() => void 0);
|
|
@@ -215,19 +635,29 @@ function createDeviceOrchestrationTransport(options) {
|
|
|
215
635
|
async *watch(request, transportOptions = {}) {
|
|
216
636
|
throwIfAborted(transportOptions.signal);
|
|
217
637
|
const { stream, grant } = await openAuthenticatedStream(options);
|
|
638
|
+
const abort = abortStreamOnce(stream);
|
|
218
639
|
const closeOnAbort = () => {
|
|
219
|
-
void
|
|
640
|
+
void abort(orchestrationTransportError("CANCELLED", "device orchestration watch was cancelled"));
|
|
220
641
|
};
|
|
221
642
|
transportOptions.signal?.addEventListener("abort", closeOnAbort, {
|
|
222
643
|
once: true
|
|
223
644
|
});
|
|
224
645
|
try {
|
|
225
646
|
await writeRequest(stream, request, grant, maxFrameBytes);
|
|
226
|
-
for await (const value of
|
|
647
|
+
for await (const value of frameReader(
|
|
648
|
+
stream,
|
|
649
|
+
maxFrameBytes,
|
|
650
|
+
WATCH_IDLE_TIMEOUT_MS,
|
|
651
|
+
transportOptions.signal,
|
|
652
|
+
abort
|
|
653
|
+
)) {
|
|
227
654
|
throwIfAborted(transportOptions.signal);
|
|
228
655
|
yield value;
|
|
229
656
|
}
|
|
230
657
|
throwIfAborted(transportOptions.signal);
|
|
658
|
+
} catch (error) {
|
|
659
|
+
if (error instanceof JsonFrameError) await abort(error);
|
|
660
|
+
mapFrameError(error);
|
|
231
661
|
} finally {
|
|
232
662
|
transportOptions.signal?.removeEventListener("abort", closeOnAbort);
|
|
233
663
|
await stream.close().catch(() => void 0);
|
|
@@ -236,15 +666,22 @@ function createDeviceOrchestrationTransport(options) {
|
|
|
236
666
|
};
|
|
237
667
|
}
|
|
238
668
|
async function readSingleRequest(stream, maxFrameBytes) {
|
|
669
|
+
const abort = abortStreamOnce(stream);
|
|
239
670
|
let envelope;
|
|
240
|
-
for await (const value of
|
|
671
|
+
for await (const value of frameReader(
|
|
672
|
+
stream,
|
|
673
|
+
maxFrameBytes,
|
|
674
|
+
REQUEST_IDLE_TIMEOUT_MS,
|
|
675
|
+
void 0,
|
|
676
|
+
abort
|
|
677
|
+
)) {
|
|
241
678
|
if (envelope) {
|
|
242
679
|
throw orchestrationTransportError(
|
|
243
680
|
"INVALID",
|
|
244
681
|
"device orchestration stream accepts exactly one request"
|
|
245
682
|
);
|
|
246
683
|
}
|
|
247
|
-
if (value === null || typeof value !== "object" || Array.isArray(value) || value.type !== "memeloop-device-orchestration-request-
|
|
684
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || value.type !== "memeloop-device-orchestration-request-v2" || value.request === null || typeof value.request !== "object") {
|
|
248
685
|
throw orchestrationTransportError(
|
|
249
686
|
"INVALID",
|
|
250
687
|
"device orchestration request envelope is invalid"
|
|
@@ -257,6 +694,41 @@ async function readSingleRequest(stream, maxFrameBytes) {
|
|
|
257
694
|
}
|
|
258
695
|
return envelope;
|
|
259
696
|
}
|
|
697
|
+
async function* withWatchBookmarks(source, request, signal) {
|
|
698
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
699
|
+
let pending = iterator.next();
|
|
700
|
+
try {
|
|
701
|
+
for (; ; ) {
|
|
702
|
+
const result = await Promise.race([
|
|
703
|
+
pending.then((value) => ({ kind: "value", value })),
|
|
704
|
+
new Promise((resolve) => {
|
|
705
|
+
const timer = setTimeout(() => {
|
|
706
|
+
resolve({ kind: "bookmark" });
|
|
707
|
+
}, WATCH_BOOKMARK_INTERVAL_MS);
|
|
708
|
+
signal.addEventListener("abort", () => {
|
|
709
|
+
clearTimeout(timer);
|
|
710
|
+
resolve({ kind: "bookmark" });
|
|
711
|
+
}, { once: true });
|
|
712
|
+
})
|
|
713
|
+
]);
|
|
714
|
+
if (signal.aborted) return;
|
|
715
|
+
if (result.kind === "bookmark") {
|
|
716
|
+
yield {
|
|
717
|
+
protocol: request.protocol,
|
|
718
|
+
requestId: request.requestId,
|
|
719
|
+
ok: true,
|
|
720
|
+
result: { type: "BOOKMARK", resourceVersion: String(Date.now()) }
|
|
721
|
+
};
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
if (result.value.done) return;
|
|
725
|
+
yield result.value.value;
|
|
726
|
+
pending = iterator.next();
|
|
727
|
+
}
|
|
728
|
+
} finally {
|
|
729
|
+
void iterator.return?.();
|
|
730
|
+
}
|
|
731
|
+
}
|
|
260
732
|
function createDeviceOrchestrationStreamHandler(options) {
|
|
261
733
|
const maxFrameBytes = options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES;
|
|
262
734
|
assertFrameLimit(maxFrameBytes);
|
|
@@ -271,10 +743,13 @@ function createDeviceOrchestrationStreamHandler(options) {
|
|
|
271
743
|
const handler = await options.resolveHandler(remotePeerId);
|
|
272
744
|
if (request.operation === "watch") {
|
|
273
745
|
await stream.sink(
|
|
274
|
-
|
|
746
|
+
encodeJsonFrames(
|
|
747
|
+
withWatchBookmarks(handler.watch(request, { signal: abort.signal }), request, abort.signal),
|
|
748
|
+
maxFrameBytes
|
|
749
|
+
)
|
|
275
750
|
);
|
|
276
751
|
} else {
|
|
277
|
-
await stream.sink(
|
|
752
|
+
await stream.sink(encodeJsonFrames([await handler.request(request)], maxFrameBytes));
|
|
278
753
|
}
|
|
279
754
|
} finally {
|
|
280
755
|
abort.abort();
|
|
@@ -283,9 +758,23 @@ function createDeviceOrchestrationStreamHandler(options) {
|
|
|
283
758
|
};
|
|
284
759
|
}
|
|
285
760
|
|
|
761
|
+
// src/device-network/deviceSyncStateStore.ts
|
|
762
|
+
var MemoryDeviceSyncStateStore = class {
|
|
763
|
+
versionVector;
|
|
764
|
+
constructor(initial = {}) {
|
|
765
|
+
this.versionVector = { ...initial };
|
|
766
|
+
}
|
|
767
|
+
async loadVersionVector() {
|
|
768
|
+
return { ...this.versionVector };
|
|
769
|
+
}
|
|
770
|
+
async saveVersionVector(versionVector) {
|
|
771
|
+
this.versionVector = { ...versionVector };
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
|
|
286
775
|
// src/device-network/libp2pSyncProtocol.ts
|
|
287
|
-
var LIBP2P_SYNC_REQUEST_TYPE = "memeloop-sync-request-
|
|
288
|
-
var LIBP2P_SYNC_RESPONSE_TYPE = "memeloop-sync-response-
|
|
776
|
+
var LIBP2P_SYNC_REQUEST_TYPE = "memeloop-sync-request-v2";
|
|
777
|
+
var LIBP2P_SYNC_RESPONSE_TYPE = "memeloop-sync-response-v2";
|
|
289
778
|
var syncMethods = /* @__PURE__ */ new Set([
|
|
290
779
|
"exchangeVersionVector",
|
|
291
780
|
"pullMissingMetadata",
|
|
@@ -345,8 +834,11 @@ var Libp2pDeviceSyncTransport = class {
|
|
|
345
834
|
exchangeVersionVector(peerId, localVersion) {
|
|
346
835
|
return this.request(peerId, "exchangeVersionVector", { localVersion });
|
|
347
836
|
}
|
|
348
|
-
pullMissingMetadata(peerId, sinceVersion) {
|
|
349
|
-
return this.request(peerId, "pullMissingMetadata", {
|
|
837
|
+
pullMissingMetadata(peerId, sinceVersion, cursor) {
|
|
838
|
+
return this.request(peerId, "pullMissingMetadata", {
|
|
839
|
+
sinceVersion,
|
|
840
|
+
cursor
|
|
841
|
+
});
|
|
350
842
|
}
|
|
351
843
|
pullMissingMessages(peerId, conversationId, knownMessageIds) {
|
|
352
844
|
return this.request(peerId, "pullMissingMessages", { conversationId, knownMessageIds });
|
|
@@ -357,7 +849,7 @@ var Libp2pDeviceSyncTransport = class {
|
|
|
357
849
|
}
|
|
358
850
|
async request(peerId, method, parameters) {
|
|
359
851
|
const grant = await this.grantProvider?.(peerId);
|
|
360
|
-
const stream = await this.deviceNetwork.openStream(peerId, "/memeloop/sync/
|
|
852
|
+
const stream = await this.deviceNetwork.openStream(peerId, "/memeloop/sync/2.0.0", grant);
|
|
361
853
|
const request = {
|
|
362
854
|
type: LIBP2P_SYNC_REQUEST_TYPE,
|
|
363
855
|
id: crypto.randomUUID(),
|
|
@@ -365,31 +857,34 @@ var Libp2pDeviceSyncTransport = class {
|
|
|
365
857
|
params: parameters,
|
|
366
858
|
grant
|
|
367
859
|
};
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
860
|
+
try {
|
|
861
|
+
await stream.sink(encodeJsonFrames([request], 16 * 1024 * 1024));
|
|
862
|
+
let response;
|
|
863
|
+
for await (const value of createJsonFrameReader(stream.source, {
|
|
864
|
+
maxPayloadBytes: 16 * 1024 * 1024,
|
|
865
|
+
idleTimeoutMs: 15e3,
|
|
866
|
+
totalTimeoutMs: 12e4,
|
|
867
|
+
abort: async (error) => stream.abort(error)
|
|
868
|
+
})) {
|
|
869
|
+
if (response !== void 0) throw new Error("sync_response_multiple");
|
|
870
|
+
response = value;
|
|
871
|
+
}
|
|
872
|
+
if (!isLibp2pSyncResponse(response)) throw new Error("invalid_sync_response");
|
|
873
|
+
if (response.id !== request.id) throw new Error("sync_response_id_mismatch");
|
|
874
|
+
if (!response.ok) throw new Error(response.error);
|
|
875
|
+
return response.result;
|
|
876
|
+
} catch (error) {
|
|
877
|
+
if (error instanceof JsonFrameError) await stream.abort(error);
|
|
878
|
+
throw error;
|
|
879
|
+
} finally {
|
|
880
|
+
await stream.close().catch(() => void 0);
|
|
881
|
+
}
|
|
375
882
|
}
|
|
376
883
|
};
|
|
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
884
|
|
|
390
885
|
// src/device-network/libp2pRpcProtocol.ts
|
|
391
|
-
var LIBP2P_RPC_REQUEST_TYPE = "memeloop-rpc-request-
|
|
392
|
-
var LIBP2P_RPC_RESPONSE_TYPE = "memeloop-rpc-response-
|
|
886
|
+
var LIBP2P_RPC_REQUEST_TYPE = "memeloop-rpc-request-v2";
|
|
887
|
+
var LIBP2P_RPC_RESPONSE_TYPE = "memeloop-rpc-response-v2";
|
|
393
888
|
function isLibp2pRpcRequest(value) {
|
|
394
889
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
395
890
|
const record = value;
|
|
@@ -404,7 +899,7 @@ function isLibp2pRpcResponse(value) {
|
|
|
404
899
|
}
|
|
405
900
|
|
|
406
901
|
// src/device-network/localTrustDeviceAuthorizer.ts
|
|
407
|
-
var PAIRING_PROTOCOL = "/memeloop/pairing/
|
|
902
|
+
var PAIRING_PROTOCOL = "/memeloop/pairing/2.0.0";
|
|
408
903
|
var LocalTrustDeviceAuthorizer = class {
|
|
409
904
|
constructor(options = {}) {
|
|
410
905
|
this.options = options;
|
|
@@ -430,7 +925,7 @@ var LocalTrustDeviceAuthorizer = class {
|
|
|
430
925
|
};
|
|
431
926
|
|
|
432
927
|
// src/device-network/pairingInvite.ts
|
|
433
|
-
var DEVICE_PAIRING_INVITE_PROTOCOL = "memeloop-device-pairing-
|
|
928
|
+
var DEVICE_PAIRING_INVITE_PROTOCOL = "memeloop-device-pairing-v2";
|
|
434
929
|
var DEVICE_PAIRING_INVITE_TTL_MS = 5 * 6e4;
|
|
435
930
|
var MAX_INVITE_LENGTH = 16 * 1024;
|
|
436
931
|
var MAX_ADDRESSES = 8;
|
|
@@ -441,37 +936,82 @@ function requireText(value, name, maxLength) {
|
|
|
441
936
|
}
|
|
442
937
|
return trimmed;
|
|
443
938
|
}
|
|
444
|
-
function
|
|
939
|
+
function finalPeerId(address) {
|
|
940
|
+
const parts = address.split("/");
|
|
941
|
+
for (let index = parts.length - 2; index >= 0; index -= 1) {
|
|
942
|
+
if (parts[index] === "p2p") return parts[index + 1];
|
|
943
|
+
}
|
|
944
|
+
return void 0;
|
|
945
|
+
}
|
|
946
|
+
function validateAddresses(addresses, peerId) {
|
|
445
947
|
const unique = [...new Set(addresses.map((address) => address.trim()))].filter(Boolean);
|
|
446
948
|
if (unique.length === 0 || unique.length > MAX_ADDRESSES || unique.some(
|
|
447
|
-
(address) => address.length > 2048 || !address.startsWith("/") || !address.includes("/ws") && !address.includes("/wss")
|
|
949
|
+
(address) => address.length > 2048 || !address.startsWith("/") || !address.includes("/ws") && !address.includes("/wss") || finalPeerId(address) !== peerId
|
|
448
950
|
)) {
|
|
449
|
-
throw new Error("device pairing invite requires WebSocket multiaddrs");
|
|
951
|
+
throw new Error("device pairing invite requires PeerId-bound WebSocket multiaddrs");
|
|
450
952
|
}
|
|
451
953
|
return unique;
|
|
452
954
|
}
|
|
453
|
-
function
|
|
955
|
+
function validateLifetime(createdAt, expiresAt, now) {
|
|
956
|
+
if (!Number.isSafeInteger(createdAt) || !Number.isSafeInteger(expiresAt) || expiresAt <= createdAt || expiresAt - createdAt > DEVICE_PAIRING_INVITE_TTL_MS) {
|
|
957
|
+
throw new Error("invalid device pairing invite lifetime");
|
|
958
|
+
}
|
|
959
|
+
if (expiresAt <= now) throw new Error("device pairing invite has expired");
|
|
960
|
+
}
|
|
961
|
+
function unsignedPayload(invite) {
|
|
962
|
+
const peerId = requireText(invite.peerId, "peerId", 256);
|
|
963
|
+
return {
|
|
964
|
+
protocol: DEVICE_PAIRING_INVITE_PROTOCOL,
|
|
965
|
+
peerId,
|
|
966
|
+
publicKeyMultibase: requireText(invite.publicKeyMultibase, "publicKeyMultibase", 4096),
|
|
967
|
+
deviceName: requireText(invite.deviceName, "deviceName", 256),
|
|
968
|
+
multiaddrs: [...validateAddresses(invite.multiaddrs, peerId)].sort(),
|
|
969
|
+
createdAt: invite.createdAt,
|
|
970
|
+
expiresAt: invite.expiresAt
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
function canonicalDevicePairingInviteBytes(invite) {
|
|
974
|
+
const payload = unsignedPayload(invite);
|
|
975
|
+
const canonical = [
|
|
976
|
+
DEVICE_PAIRING_INVITE_PROTOCOL,
|
|
977
|
+
`peerId=${payload.peerId}`,
|
|
978
|
+
`publicKey=${payload.publicKeyMultibase}`,
|
|
979
|
+
`deviceName=${payload.deviceName}`,
|
|
980
|
+
`multiaddrs=${payload.multiaddrs.join(",")}`,
|
|
981
|
+
`createdAt=${payload.createdAt}`,
|
|
982
|
+
`expiresAt=${payload.expiresAt}`
|
|
983
|
+
].join("\n");
|
|
984
|
+
return new TextEncoder().encode(canonical);
|
|
985
|
+
}
|
|
986
|
+
async function createDevicePairingInvite(device, options) {
|
|
454
987
|
const createdAt = options.now ?? Date.now();
|
|
455
988
|
const ttlMs = options.ttlMs ?? DEVICE_PAIRING_INVITE_TTL_MS;
|
|
456
|
-
if (!Number.isSafeInteger(createdAt) || !Number.isSafeInteger(ttlMs) || ttlMs <= 0
|
|
989
|
+
if (!Number.isSafeInteger(createdAt) || !Number.isSafeInteger(ttlMs) || ttlMs <= 0) {
|
|
457
990
|
throw new Error("invalid device pairing invite lifetime");
|
|
458
991
|
}
|
|
459
|
-
|
|
992
|
+
const payload = unsignedPayload({
|
|
460
993
|
protocol: DEVICE_PAIRING_INVITE_PROTOCOL,
|
|
461
|
-
peerId:
|
|
462
|
-
|
|
463
|
-
|
|
994
|
+
peerId: device.peerId,
|
|
995
|
+
publicKeyMultibase: device.publicKeyMultibase,
|
|
996
|
+
deviceName: device.displayName,
|
|
997
|
+
multiaddrs: device.multiaddrs ?? [],
|
|
464
998
|
createdAt,
|
|
465
999
|
expiresAt: createdAt + ttlMs
|
|
466
|
-
};
|
|
1000
|
+
});
|
|
1001
|
+
validateLifetime(payload.createdAt, payload.expiresAt, createdAt - 1);
|
|
1002
|
+
const signature = requireText(
|
|
1003
|
+
await options.sign(canonicalDevicePairingInviteBytes(payload)),
|
|
1004
|
+
"signature",
|
|
1005
|
+
4096
|
|
1006
|
+
);
|
|
1007
|
+
return { ...payload, signature };
|
|
467
1008
|
}
|
|
468
1009
|
function encodeDevicePairingInvite(invite) {
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
});
|
|
472
|
-
return JSON.stringify(validated);
|
|
1010
|
+
const payload = unsignedPayload(invite);
|
|
1011
|
+
const signature = requireText(invite.signature, "signature", 4096);
|
|
1012
|
+
return JSON.stringify({ ...payload, signature });
|
|
473
1013
|
}
|
|
474
|
-
function parseDevicePairingInvite(serialized, options
|
|
1014
|
+
async function parseDevicePairingInvite(serialized, options) {
|
|
475
1015
|
if (!serialized || serialized.length > MAX_INVITE_LENGTH) {
|
|
476
1016
|
throw new Error("invalid device pairing invite length");
|
|
477
1017
|
}
|
|
@@ -485,20 +1025,30 @@ function parseDevicePairingInvite(serialized, options = {}) {
|
|
|
485
1025
|
throw new Error("invalid device pairing invite");
|
|
486
1026
|
}
|
|
487
1027
|
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" ||
|
|
1028
|
+
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
1029
|
throw new Error("invalid device pairing invite");
|
|
490
1030
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
}
|
|
494
|
-
return {
|
|
1031
|
+
validateLifetime(record.createdAt, record.expiresAt, options.now ?? Date.now());
|
|
1032
|
+
const payload = unsignedPayload({
|
|
495
1033
|
protocol: DEVICE_PAIRING_INVITE_PROTOCOL,
|
|
496
|
-
peerId:
|
|
497
|
-
|
|
498
|
-
|
|
1034
|
+
peerId: record.peerId,
|
|
1035
|
+
publicKeyMultibase: record.publicKeyMultibase,
|
|
1036
|
+
deviceName: record.deviceName,
|
|
1037
|
+
multiaddrs: record.multiaddrs,
|
|
499
1038
|
createdAt: record.createdAt,
|
|
500
1039
|
expiresAt: record.expiresAt
|
|
1040
|
+
});
|
|
1041
|
+
const invite = {
|
|
1042
|
+
...payload,
|
|
1043
|
+
signature: requireText(record.signature, "signature", 4096)
|
|
501
1044
|
};
|
|
1045
|
+
if (!await options.verifyIdentity({
|
|
1046
|
+
invite,
|
|
1047
|
+
payload: canonicalDevicePairingInviteBytes(payload)
|
|
1048
|
+
})) {
|
|
1049
|
+
throw new Error("device pairing invite identity verification failed");
|
|
1050
|
+
}
|
|
1051
|
+
return invite;
|
|
502
1052
|
}
|
|
503
1053
|
|
|
504
1054
|
// src/sync/attachmentRpcCodec.ts
|
|
@@ -527,43 +1077,66 @@ var ChatSyncEngine = class {
|
|
|
527
1077
|
nodeId;
|
|
528
1078
|
storage;
|
|
529
1079
|
getPeers;
|
|
1080
|
+
stateStore;
|
|
530
1081
|
versionVector = {};
|
|
1082
|
+
loadStatePromise;
|
|
1083
|
+
stateMutation = Promise.resolve();
|
|
531
1084
|
constructor(options) {
|
|
532
1085
|
this.nodeId = options.nodeId;
|
|
533
1086
|
this.storage = options.storage;
|
|
534
1087
|
this.getPeers = options.peers;
|
|
535
|
-
this.
|
|
1088
|
+
this.stateStore = options.stateStore ?? new MemoryDeviceSyncStateStore();
|
|
536
1089
|
}
|
|
537
1090
|
/**
|
|
538
1091
|
* 在本节点新产生一条消息时调用,增加本节点的 lamportClock。
|
|
539
1092
|
*/
|
|
540
1093
|
bumpLocalVersion() {
|
|
541
|
-
|
|
1094
|
+
let nextClock = 0;
|
|
1095
|
+
const mutation = this.stateMutation.then(async () => {
|
|
1096
|
+
await this.ensureStateLoaded();
|
|
1097
|
+
nextClock = (this.versionVector[this.nodeId] ?? 0) + 1;
|
|
1098
|
+
this.versionVector[this.nodeId] = nextClock;
|
|
1099
|
+
await this.stateStore.saveVersionVector(this.versionVector);
|
|
1100
|
+
});
|
|
1101
|
+
this.stateMutation = mutation.catch(() => void 0);
|
|
1102
|
+
return mutation.then(() => nextClock);
|
|
542
1103
|
}
|
|
543
1104
|
/**
|
|
544
1105
|
* 执行一次与所有 peer 的增量同步(metadata 级别),并尽力拉取缺失消息。
|
|
545
1106
|
*/
|
|
546
1107
|
async syncOnce() {
|
|
1108
|
+
await this.ensureStateLoaded();
|
|
1109
|
+
await this.stateMutation;
|
|
1110
|
+
await this.reconcileVersionVectorFromStorage();
|
|
547
1111
|
const peers = this.getPeers();
|
|
548
1112
|
if (peers.length === 0) return;
|
|
549
|
-
const localVersion = { ...this.versionVector };
|
|
550
1113
|
const pulledConversations = /* @__PURE__ */ new Set();
|
|
551
1114
|
for (const peer of peers) {
|
|
552
|
-
const { remoteVersion
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
await this.
|
|
556
|
-
|
|
557
|
-
|
|
1115
|
+
const { remoteVersion } = await peer.exchangeVersionVector({ ...this.versionVector });
|
|
1116
|
+
let cursor;
|
|
1117
|
+
do {
|
|
1118
|
+
const page = await peer.pullMissingMetadata(this.versionVector, cursor);
|
|
1119
|
+
for (const meta of page.items) {
|
|
1120
|
+
await this.storage.upsertConversationMetadata(meta);
|
|
1121
|
+
pulledConversations.add(meta.conversationId);
|
|
1122
|
+
}
|
|
1123
|
+
cursor = page.nextCursor;
|
|
1124
|
+
} while (cursor !== void 0);
|
|
558
1125
|
for (const [nodeId, clock] of Object.entries(remoteVersion)) {
|
|
559
1126
|
const current = this.versionVector[nodeId] ?? 0;
|
|
560
|
-
if (clock > current)
|
|
561
|
-
this.versionVector[nodeId] = clock;
|
|
562
|
-
}
|
|
1127
|
+
if (clock > current) this.versionVector[nodeId] = clock;
|
|
563
1128
|
}
|
|
564
|
-
|
|
565
|
-
|
|
1129
|
+
await this.stateStore.saveVersionVector(this.versionVector);
|
|
1130
|
+
}
|
|
1131
|
+
const pageSize = 100;
|
|
1132
|
+
let offset = 0;
|
|
1133
|
+
for (; ; ) {
|
|
1134
|
+
const conversations = await this.storage.listConversations({ limit: pageSize, offset });
|
|
1135
|
+
for (const conversation of conversations) {
|
|
1136
|
+
pulledConversations.add(conversation.conversationId);
|
|
566
1137
|
}
|
|
1138
|
+
if (conversations.length < pageSize) break;
|
|
1139
|
+
offset += conversations.length;
|
|
567
1140
|
}
|
|
568
1141
|
for (const conversationId of pulledConversations) {
|
|
569
1142
|
await this.pullMessagesForConversationFromPeers(conversationId, peers);
|
|
@@ -574,11 +1147,6 @@ var ChatSyncEngine = class {
|
|
|
574
1147
|
*/
|
|
575
1148
|
async antiEntropyOnce() {
|
|
576
1149
|
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
1150
|
}
|
|
583
1151
|
async pullMessagesForConversationFromPeers(conversationId, peers) {
|
|
584
1152
|
const localMsgs = await this.storage.getMessages(conversationId, { mode: "full-content" });
|
|
@@ -631,12 +1199,39 @@ var ChatSyncEngine = class {
|
|
|
631
1199
|
}
|
|
632
1200
|
}
|
|
633
1201
|
}
|
|
634
|
-
getVersionVector() {
|
|
1202
|
+
async getVersionVector() {
|
|
1203
|
+
await this.ensureStateLoaded();
|
|
1204
|
+
await this.stateMutation;
|
|
635
1205
|
return { ...this.versionVector };
|
|
636
1206
|
}
|
|
637
1207
|
getStorage() {
|
|
638
1208
|
return this.storage;
|
|
639
1209
|
}
|
|
1210
|
+
ensureStateLoaded() {
|
|
1211
|
+
this.loadStatePromise ??= (async () => {
|
|
1212
|
+
const stored = await this.stateStore.loadVersionVector();
|
|
1213
|
+
this.versionVector = { ...stored, [this.nodeId]: stored[this.nodeId] ?? 0 };
|
|
1214
|
+
})();
|
|
1215
|
+
return this.loadStatePromise;
|
|
1216
|
+
}
|
|
1217
|
+
async reconcileVersionVectorFromStorage() {
|
|
1218
|
+
const pageSize = 100;
|
|
1219
|
+
let offset = 0;
|
|
1220
|
+
let changed = false;
|
|
1221
|
+
for (; ; ) {
|
|
1222
|
+
const conversations = await this.storage.listConversations({ limit: pageSize, offset });
|
|
1223
|
+
for (const conversation of conversations) {
|
|
1224
|
+
const current = this.versionVector[conversation.originNodeId] ?? 0;
|
|
1225
|
+
if (conversation.originClock > current) {
|
|
1226
|
+
this.versionVector[conversation.originNodeId] = conversation.originClock;
|
|
1227
|
+
changed = true;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
if (conversations.length < pageSize) break;
|
|
1231
|
+
offset += conversations.length;
|
|
1232
|
+
}
|
|
1233
|
+
if (changed) await this.stateStore.saveVersionVector(this.versionVector);
|
|
1234
|
+
}
|
|
640
1235
|
};
|
|
641
1236
|
|
|
642
1237
|
// src/sync/peerNodeAdapter.ts
|
|
@@ -650,8 +1245,8 @@ var PeerNodeSyncAdapter = class {
|
|
|
650
1245
|
exchangeVersionVector(localVersion) {
|
|
651
1246
|
return this.transport.exchangeVersionVector(this.nodeId, localVersion);
|
|
652
1247
|
}
|
|
653
|
-
pullMissingMetadata(sinceVersion) {
|
|
654
|
-
return this.transport.pullMissingMetadata(this.nodeId, sinceVersion);
|
|
1248
|
+
pullMissingMetadata(sinceVersion, cursor) {
|
|
1249
|
+
return this.transport.pullMissingMetadata(this.nodeId, sinceVersion, cursor);
|
|
655
1250
|
}
|
|
656
1251
|
pullMissingMessages(conversationId, knownMessageIds) {
|
|
657
1252
|
const function_ = this.transport.pullMissingMessages?.bind(this.transport);
|
|
@@ -673,7 +1268,17 @@ var PeerNodeSyncAdapter = class {
|
|
|
673
1268
|
function isConversationMeta(value) {
|
|
674
1269
|
if (value === null || typeof value !== "object") return false;
|
|
675
1270
|
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";
|
|
1271
|
+
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";
|
|
1272
|
+
}
|
|
1273
|
+
function computeMissingVersionRanges(currentVersion, availableVersion) {
|
|
1274
|
+
const ranges = [];
|
|
1275
|
+
for (const [originNodeId, availableClock] of Object.entries(availableVersion)) {
|
|
1276
|
+
const currentClock = currentVersion[originNodeId] ?? 0;
|
|
1277
|
+
if (availableClock > currentClock) {
|
|
1278
|
+
ranges.push({ originNodeId, fromExclusive: currentClock, toInclusive: availableClock });
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
return ranges.sort((left, right) => left.originNodeId.localeCompare(right.originNodeId));
|
|
677
1282
|
}
|
|
678
1283
|
|
|
679
1284
|
// src/device-network/memoryDeviceNetworkService.ts
|
|
@@ -813,6 +1418,8 @@ var MemoryDeviceNetworkService = class {
|
|
|
813
1418
|
async sink() {
|
|
814
1419
|
},
|
|
815
1420
|
async close() {
|
|
1421
|
+
},
|
|
1422
|
+
abort() {
|
|
816
1423
|
}
|
|
817
1424
|
};
|
|
818
1425
|
}
|
|
@@ -847,11 +1454,21 @@ var MemoryDeviceNetworkService = class {
|
|
|
847
1454
|
// src/device-network/syncCloudDevices.ts
|
|
848
1455
|
async function syncCloudDevices(input) {
|
|
849
1456
|
const cloudDevices = await input.cloudClient.listDevices();
|
|
850
|
-
if (cloudDevices.length === 0) return [];
|
|
851
1457
|
const existingRecords = await input.trustStore.loadTrustedDevices();
|
|
852
1458
|
const existingByPeerId = new Map(existingRecords.map((r) => [r.peerId, r]));
|
|
853
|
-
|
|
1459
|
+
const excludedPeerIds = new Set(input.excludePeerIds ?? []);
|
|
1460
|
+
const activeCloudDevices = cloudDevices.filter(
|
|
1461
|
+
(device) => device.revokedAt === void 0 && !excludedPeerIds.has(device.peerId)
|
|
1462
|
+
);
|
|
1463
|
+
const visibleCloudPeerIds = new Set(activeCloudDevices.map((device) => device.peerId));
|
|
1464
|
+
for (const existing of existingRecords) {
|
|
1465
|
+
if (existing.trustMode === "cloud-account" && !visibleCloudPeerIds.has(existing.peerId)) {
|
|
1466
|
+
await input.trustStore.removeTrustedDevice(existing.peerId);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
for (const cloudDevice of activeCloudDevices) {
|
|
854
1470
|
const existing = existingByPeerId.get(cloudDevice.peerId);
|
|
1471
|
+
if (existing?.trustMode === "local-pairing") continue;
|
|
855
1472
|
const record = {
|
|
856
1473
|
peerId: cloudDevice.peerId,
|
|
857
1474
|
publicKeyMultibase: cloudDevice.publicKeyMultibase,
|
|
@@ -863,17 +1480,14 @@ async function syncCloudDevices(input) {
|
|
|
863
1480
|
lastSeen: cloudDevice.lastSeen,
|
|
864
1481
|
revokedAt: cloudDevice.revokedAt
|
|
865
1482
|
};
|
|
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 {
|
|
1483
|
+
if (!existing || !sameTrustedDevice(existing, record)) {
|
|
873
1484
|
await input.trustStore.saveTrustedDevice(record);
|
|
874
1485
|
}
|
|
875
1486
|
}
|
|
876
|
-
return
|
|
1487
|
+
return activeCloudDevices;
|
|
1488
|
+
}
|
|
1489
|
+
function sameTrustedDevice(left, right) {
|
|
1490
|
+
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
1491
|
}
|
|
878
1492
|
// Annotate the CommonJS export names for ESM import in node:
|
|
879
1493
|
0 && (module.exports = {
|
|
@@ -881,6 +1495,8 @@ async function syncCloudDevices(input) {
|
|
|
881
1495
|
DEVICE_ORCHESTRATION_PROTOCOL,
|
|
882
1496
|
DEVICE_PAIRING_INVITE_PROTOCOL,
|
|
883
1497
|
DEVICE_PAIRING_INVITE_TTL_MS,
|
|
1498
|
+
DeviceCloudConnectionCoordinator,
|
|
1499
|
+
JsonFrameError,
|
|
884
1500
|
LIBP2P_RPC_REQUEST_TYPE,
|
|
885
1501
|
LIBP2P_RPC_RESPONSE_TYPE,
|
|
886
1502
|
LIBP2P_SYNC_REQUEST_TYPE,
|
|
@@ -888,13 +1504,19 @@ async function syncCloudDevices(input) {
|
|
|
888
1504
|
Libp2pDeviceSyncTransport,
|
|
889
1505
|
LocalTrustDeviceAuthorizer,
|
|
890
1506
|
MemoryDeviceNetworkService,
|
|
1507
|
+
MemoryDeviceSyncStateStore,
|
|
891
1508
|
PeerNodeSyncAdapter,
|
|
892
1509
|
attachmentBlobFromWire,
|
|
1510
|
+
canonicalDevicePairingInviteBytes,
|
|
1511
|
+
computeMissingVersionRanges,
|
|
893
1512
|
createDeviceOrchestrationStreamHandler,
|
|
894
1513
|
createDeviceOrchestrationTransport,
|
|
895
1514
|
createDevicePairingInvite,
|
|
1515
|
+
createJsonFrameReader,
|
|
896
1516
|
decodeAttachmentBlobRpc,
|
|
897
1517
|
encodeDevicePairingInvite,
|
|
1518
|
+
encodeJsonFrame,
|
|
1519
|
+
encodeJsonFrames,
|
|
898
1520
|
isConversationMeta,
|
|
899
1521
|
isLibp2pRpcRequest,
|
|
900
1522
|
isLibp2pRpcResponse,
|