@lark-sh/client 0.1.7 → 0.1.9
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/index.d.mts +66 -8
- package/dist/index.d.ts +66 -8
- package/dist/index.js +565 -149
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +565 -149
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
package/dist/index.mjs
CHANGED
|
@@ -60,6 +60,331 @@ var Coordinator = class {
|
|
|
60
60
|
}
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
+
// src/connection/WebSocketTransport.ts
|
|
64
|
+
import WebSocketNode from "ws";
|
|
65
|
+
var WebSocketImpl = typeof WebSocket !== "undefined" ? WebSocket : WebSocketNode;
|
|
66
|
+
var WebSocketTransport = class {
|
|
67
|
+
constructor(options) {
|
|
68
|
+
this.ws = null;
|
|
69
|
+
this._state = "disconnected";
|
|
70
|
+
this.options = options;
|
|
71
|
+
}
|
|
72
|
+
get state() {
|
|
73
|
+
return this._state;
|
|
74
|
+
}
|
|
75
|
+
get connected() {
|
|
76
|
+
return this._state === "connected";
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Connect to a WebSocket server.
|
|
80
|
+
*/
|
|
81
|
+
connect(url) {
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
if (this._state !== "disconnected") {
|
|
84
|
+
reject(new Error("Already connected or connecting"));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
this._state = "connecting";
|
|
88
|
+
try {
|
|
89
|
+
this.ws = new WebSocketImpl(url);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
this._state = "disconnected";
|
|
92
|
+
reject(err);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const onOpen = () => {
|
|
96
|
+
cleanup();
|
|
97
|
+
this._state = "connected";
|
|
98
|
+
this.setupEventHandlers();
|
|
99
|
+
resolve();
|
|
100
|
+
this.options.onOpen();
|
|
101
|
+
};
|
|
102
|
+
const onError = (event) => {
|
|
103
|
+
cleanup();
|
|
104
|
+
this._state = "disconnected";
|
|
105
|
+
this.ws = null;
|
|
106
|
+
const error = new Error("WebSocket connection failed");
|
|
107
|
+
reject(error);
|
|
108
|
+
this.options.onError(error);
|
|
109
|
+
};
|
|
110
|
+
const onClose = (event) => {
|
|
111
|
+
cleanup();
|
|
112
|
+
this._state = "disconnected";
|
|
113
|
+
this.ws = null;
|
|
114
|
+
reject(new Error(`WebSocket closed: ${event.code} ${event.reason}`));
|
|
115
|
+
};
|
|
116
|
+
const cleanup = () => {
|
|
117
|
+
this.ws?.removeEventListener("open", onOpen);
|
|
118
|
+
this.ws?.removeEventListener("error", onError);
|
|
119
|
+
this.ws?.removeEventListener("close", onClose);
|
|
120
|
+
};
|
|
121
|
+
this.ws.addEventListener("open", onOpen);
|
|
122
|
+
this.ws.addEventListener("error", onError);
|
|
123
|
+
this.ws.addEventListener("close", onClose);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Set up persistent event handlers after connection is established.
|
|
128
|
+
*/
|
|
129
|
+
setupEventHandlers() {
|
|
130
|
+
if (!this.ws) return;
|
|
131
|
+
this.ws.addEventListener("message", (event) => {
|
|
132
|
+
this.options.onMessage(event.data);
|
|
133
|
+
});
|
|
134
|
+
this.ws.addEventListener("close", (event) => {
|
|
135
|
+
this._state = "disconnected";
|
|
136
|
+
this.ws = null;
|
|
137
|
+
this.options.onClose(event.code, event.reason);
|
|
138
|
+
});
|
|
139
|
+
this.ws.addEventListener("error", () => {
|
|
140
|
+
this.options.onError(new Error("WebSocket error"));
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Send a message over the WebSocket.
|
|
145
|
+
*/
|
|
146
|
+
send(data) {
|
|
147
|
+
if (!this.ws || this._state !== "connected") {
|
|
148
|
+
throw new Error("WebSocket not connected");
|
|
149
|
+
}
|
|
150
|
+
this.ws.send(data);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Send a volatile message.
|
|
154
|
+
* WebSocket doesn't have a true unreliable channel, so this just calls send().
|
|
155
|
+
*/
|
|
156
|
+
sendVolatile(data) {
|
|
157
|
+
this.send(data);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Close the WebSocket connection.
|
|
161
|
+
*/
|
|
162
|
+
close(code = 1e3, reason = "Client disconnect") {
|
|
163
|
+
if (this.ws) {
|
|
164
|
+
this.ws.close(code, reason);
|
|
165
|
+
this.ws = null;
|
|
166
|
+
}
|
|
167
|
+
this._state = "disconnected";
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// src/connection/WebTransportClient.ts
|
|
172
|
+
var WebTransportClient = class {
|
|
173
|
+
constructor(options) {
|
|
174
|
+
this.transport = null;
|
|
175
|
+
this.writer = null;
|
|
176
|
+
this.datagramWriter = null;
|
|
177
|
+
this._state = "disconnected";
|
|
178
|
+
this.readLoopActive = false;
|
|
179
|
+
this.datagramLoopActive = false;
|
|
180
|
+
this.options = options;
|
|
181
|
+
}
|
|
182
|
+
get state() {
|
|
183
|
+
return this._state;
|
|
184
|
+
}
|
|
185
|
+
get connected() {
|
|
186
|
+
return this._state === "connected";
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Connect to a WebTransport server.
|
|
190
|
+
*/
|
|
191
|
+
async connect(url) {
|
|
192
|
+
if (this._state !== "disconnected") {
|
|
193
|
+
throw new Error("Already connected or connecting");
|
|
194
|
+
}
|
|
195
|
+
this._state = "connecting";
|
|
196
|
+
try {
|
|
197
|
+
this.transport = new WebTransport(url);
|
|
198
|
+
await this.transport.ready;
|
|
199
|
+
const stream = await this.transport.createBidirectionalStream();
|
|
200
|
+
this.writer = stream.writable.getWriter();
|
|
201
|
+
this.datagramWriter = this.transport.datagrams.writable.getWriter();
|
|
202
|
+
this._state = "connected";
|
|
203
|
+
this.readLoop(stream.readable.getReader());
|
|
204
|
+
this.datagramLoop(this.transport.datagrams.readable.getReader());
|
|
205
|
+
this.transport.closed.then(({ closeCode, reason }) => {
|
|
206
|
+
this.handleClose(closeCode, reason);
|
|
207
|
+
}).catch(() => {
|
|
208
|
+
this.handleClose(0, "Connection failed");
|
|
209
|
+
});
|
|
210
|
+
this.options.onOpen();
|
|
211
|
+
} catch (err) {
|
|
212
|
+
this._state = "disconnected";
|
|
213
|
+
this.transport = null;
|
|
214
|
+
this.writer = null;
|
|
215
|
+
this.datagramWriter = null;
|
|
216
|
+
throw err;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Read messages from the reliable stream.
|
|
221
|
+
* Messages are framed with 4-byte big-endian length prefix.
|
|
222
|
+
*/
|
|
223
|
+
async readLoop(reader) {
|
|
224
|
+
this.readLoopActive = true;
|
|
225
|
+
let buffer = new Uint8Array(0);
|
|
226
|
+
try {
|
|
227
|
+
while (this.readLoopActive) {
|
|
228
|
+
const { value, done } = await reader.read();
|
|
229
|
+
if (done) break;
|
|
230
|
+
const newBuffer = new Uint8Array(buffer.length + value.length);
|
|
231
|
+
newBuffer.set(buffer);
|
|
232
|
+
newBuffer.set(value, buffer.length);
|
|
233
|
+
buffer = newBuffer;
|
|
234
|
+
while (buffer.length >= 4) {
|
|
235
|
+
const view = new DataView(buffer.buffer, buffer.byteOffset);
|
|
236
|
+
const msgLength = view.getUint32(0, false);
|
|
237
|
+
if (buffer.length < 4 + msgLength) break;
|
|
238
|
+
const msgBytes = buffer.slice(4, 4 + msgLength);
|
|
239
|
+
const json = new TextDecoder().decode(msgBytes);
|
|
240
|
+
try {
|
|
241
|
+
this.options.onMessage(json);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
console.error("Error handling message:", err);
|
|
244
|
+
}
|
|
245
|
+
buffer = buffer.slice(4 + msgLength);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} catch (err) {
|
|
249
|
+
if (this.readLoopActive) {
|
|
250
|
+
console.error("Read loop error:", err);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Read volatile events from datagrams.
|
|
256
|
+
* No framing needed - each datagram is a complete JSON message.
|
|
257
|
+
*/
|
|
258
|
+
async datagramLoop(reader) {
|
|
259
|
+
this.datagramLoopActive = true;
|
|
260
|
+
try {
|
|
261
|
+
while (this.datagramLoopActive) {
|
|
262
|
+
const { value, done } = await reader.read();
|
|
263
|
+
if (done) break;
|
|
264
|
+
const json = new TextDecoder().decode(value);
|
|
265
|
+
try {
|
|
266
|
+
this.options.onMessage(json);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
console.error("Error handling datagram:", err);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
} catch (err) {
|
|
272
|
+
if (this.datagramLoopActive) {
|
|
273
|
+
console.error("Datagram loop error:", err);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Handle connection close.
|
|
279
|
+
*/
|
|
280
|
+
handleClose(code, reason) {
|
|
281
|
+
if (this._state === "disconnected") return;
|
|
282
|
+
this.readLoopActive = false;
|
|
283
|
+
this.datagramLoopActive = false;
|
|
284
|
+
this._state = "disconnected";
|
|
285
|
+
this.transport = null;
|
|
286
|
+
this.writer = null;
|
|
287
|
+
this.datagramWriter = null;
|
|
288
|
+
this.options.onClose(code, reason);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Send a message over the reliable stream.
|
|
292
|
+
* Uses 4-byte big-endian length prefix.
|
|
293
|
+
*/
|
|
294
|
+
send(data) {
|
|
295
|
+
if (!this.writer || this._state !== "connected") {
|
|
296
|
+
throw new Error("WebTransport not connected");
|
|
297
|
+
}
|
|
298
|
+
const encoder = new TextEncoder();
|
|
299
|
+
const msgBytes = encoder.encode(data);
|
|
300
|
+
const lengthBuffer = new ArrayBuffer(4);
|
|
301
|
+
new DataView(lengthBuffer).setUint32(0, msgBytes.length, false);
|
|
302
|
+
this.writer.write(new Uint8Array(lengthBuffer)).catch((err) => {
|
|
303
|
+
console.error("Failed to write length prefix:", err);
|
|
304
|
+
});
|
|
305
|
+
this.writer.write(msgBytes).catch((err) => {
|
|
306
|
+
console.error("Failed to write message:", err);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Send a volatile message via datagram (unreliable).
|
|
311
|
+
* No framing needed - each datagram is a complete message.
|
|
312
|
+
*/
|
|
313
|
+
sendVolatile(data) {
|
|
314
|
+
if (!this.datagramWriter || this._state !== "connected") {
|
|
315
|
+
throw new Error("WebTransport not connected");
|
|
316
|
+
}
|
|
317
|
+
const encoder = new TextEncoder();
|
|
318
|
+
const msgBytes = encoder.encode(data);
|
|
319
|
+
this.datagramWriter.write(msgBytes).catch(() => {
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Close the WebTransport connection.
|
|
324
|
+
*/
|
|
325
|
+
close(code = 0, reason = "Client disconnect") {
|
|
326
|
+
this.readLoopActive = false;
|
|
327
|
+
this.datagramLoopActive = false;
|
|
328
|
+
if (this.writer) {
|
|
329
|
+
this.writer.releaseLock();
|
|
330
|
+
this.writer = null;
|
|
331
|
+
}
|
|
332
|
+
if (this.datagramWriter) {
|
|
333
|
+
this.datagramWriter.releaseLock();
|
|
334
|
+
this.datagramWriter = null;
|
|
335
|
+
}
|
|
336
|
+
if (this.transport) {
|
|
337
|
+
this.transport.close({ closeCode: code, reason });
|
|
338
|
+
this.transport = null;
|
|
339
|
+
}
|
|
340
|
+
this._state = "disconnected";
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
function isWebTransportAvailable() {
|
|
344
|
+
return typeof globalThis !== "undefined" && "WebTransport" in globalThis;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/connection/createTransport.ts
|
|
348
|
+
function wsUrlToWtUrl(wsUrl) {
|
|
349
|
+
const url = new URL(wsUrl);
|
|
350
|
+
url.protocol = url.protocol === "wss:" ? "https:" : "http:";
|
|
351
|
+
const port = 7778 + Math.floor(Math.random() * 32);
|
|
352
|
+
url.port = String(port);
|
|
353
|
+
url.pathname = "/wt";
|
|
354
|
+
return url.toString();
|
|
355
|
+
}
|
|
356
|
+
async function createTransport(wsUrl, transportOptions, options = {}) {
|
|
357
|
+
const preferredType = options.transport || "auto";
|
|
358
|
+
const shouldTryWebTransport = (preferredType === "auto" || preferredType === "webtransport") && isWebTransportAvailable();
|
|
359
|
+
if (preferredType === "webtransport" && !isWebTransportAvailable()) {
|
|
360
|
+
throw new Error("WebTransport is not available in this environment");
|
|
361
|
+
}
|
|
362
|
+
if (shouldTryWebTransport) {
|
|
363
|
+
const wtUrl = wsUrlToWtUrl(wsUrl);
|
|
364
|
+
const wtTransport = new WebTransportClient(transportOptions);
|
|
365
|
+
try {
|
|
366
|
+
await wtTransport.connect(wtUrl);
|
|
367
|
+
return {
|
|
368
|
+
transport: wtTransport,
|
|
369
|
+
type: "webtransport",
|
|
370
|
+
url: wtUrl
|
|
371
|
+
};
|
|
372
|
+
} catch (err) {
|
|
373
|
+
if (preferredType === "webtransport") {
|
|
374
|
+
throw err;
|
|
375
|
+
}
|
|
376
|
+
console.warn("WebTransport connection failed, falling back to WebSocket:", err);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const wsTransport = new WebSocketTransport(transportOptions);
|
|
380
|
+
await wsTransport.connect(wsUrl);
|
|
381
|
+
return {
|
|
382
|
+
transport: wsTransport,
|
|
383
|
+
type: "websocket",
|
|
384
|
+
url: wsUrl
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
63
388
|
// src/LarkError.ts
|
|
64
389
|
var LarkError = class _LarkError extends Error {
|
|
65
390
|
constructor(code, message) {
|
|
@@ -1025,13 +1350,15 @@ var SubscriptionManager = class {
|
|
|
1025
1350
|
}
|
|
1026
1351
|
/**
|
|
1027
1352
|
* Handle an incoming event message from the server.
|
|
1028
|
-
* Server sends 'put' or '
|
|
1353
|
+
* Server sends 'put', 'patch', or 'vb' (volatile batch) events; we generate child_* events client-side.
|
|
1029
1354
|
*/
|
|
1030
1355
|
handleEvent(message) {
|
|
1031
1356
|
if (message.ev === "put") {
|
|
1032
1357
|
this.handlePutEvent(message);
|
|
1033
1358
|
} else if (message.ev === "patch") {
|
|
1034
1359
|
this.handlePatchEvent(message);
|
|
1360
|
+
} else if (message.ev === "vb") {
|
|
1361
|
+
this.handleVolatileBatchEvent(message);
|
|
1035
1362
|
} else {
|
|
1036
1363
|
console.warn("Unknown event type:", message.ev);
|
|
1037
1364
|
}
|
|
@@ -1085,6 +1412,26 @@ var SubscriptionManager = class {
|
|
|
1085
1412
|
}
|
|
1086
1413
|
this.applyWriteToView(view, updates, isVolatile, serverTimestamp);
|
|
1087
1414
|
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Handle a 'vb' (volatile batch) event - batched volatile updates across subscriptions.
|
|
1417
|
+
* Server batches volatile events in 50ms intervals to reduce message overhead.
|
|
1418
|
+
* Format: { ev: 'vb', b: { subscriptionPath: { relativePath: value } }, ts: timestamp }
|
|
1419
|
+
*/
|
|
1420
|
+
handleVolatileBatchEvent(message) {
|
|
1421
|
+
const batch = message.b;
|
|
1422
|
+
const serverTimestamp = message.ts;
|
|
1423
|
+
if (!batch) return;
|
|
1424
|
+
for (const [subscriptionPath, updates] of Object.entries(batch)) {
|
|
1425
|
+
const view = this.views.get(subscriptionPath);
|
|
1426
|
+
if (!view) continue;
|
|
1427
|
+
if (view.recovering) continue;
|
|
1428
|
+
const updatesList = [];
|
|
1429
|
+
for (const [relativePath, value] of Object.entries(updates)) {
|
|
1430
|
+
updatesList.push({ relativePath, value });
|
|
1431
|
+
}
|
|
1432
|
+
this.applyWriteToView(view, updatesList, true, serverTimestamp);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1088
1435
|
/**
|
|
1089
1436
|
* Detect and fire child_moved events for children that changed position.
|
|
1090
1437
|
*/
|
|
@@ -1301,6 +1648,34 @@ var SubscriptionManager = class {
|
|
|
1301
1648
|
// ============================================
|
|
1302
1649
|
// Shared Write Application (used by server events and optimistic writes)
|
|
1303
1650
|
// ============================================
|
|
1651
|
+
/**
|
|
1652
|
+
* Recursively sort object keys for consistent comparison.
|
|
1653
|
+
* This ensures {a:1, b:2} and {b:2, a:1} compare as equal.
|
|
1654
|
+
* Uses simple alphabetical sorting (not Firebase key sorting) since we're
|
|
1655
|
+
* just checking data equality, not display order.
|
|
1656
|
+
*/
|
|
1657
|
+
sortKeysForComparison(value) {
|
|
1658
|
+
if (value === null || typeof value !== "object") {
|
|
1659
|
+
return value;
|
|
1660
|
+
}
|
|
1661
|
+
if (Array.isArray(value)) {
|
|
1662
|
+
return value.map((item) => this.sortKeysForComparison(item));
|
|
1663
|
+
}
|
|
1664
|
+
const obj = value;
|
|
1665
|
+
const sortedKeys = Object.keys(obj).sort();
|
|
1666
|
+
const sorted = {};
|
|
1667
|
+
for (const key of sortedKeys) {
|
|
1668
|
+
sorted[key] = this.sortKeysForComparison(obj[key]);
|
|
1669
|
+
}
|
|
1670
|
+
return sorted;
|
|
1671
|
+
}
|
|
1672
|
+
/**
|
|
1673
|
+
* Serialize cache with sorted keys for consistent comparison.
|
|
1674
|
+
*/
|
|
1675
|
+
serializeCacheForComparison(cache) {
|
|
1676
|
+
const sorted = this.sortKeysForComparison(cache);
|
|
1677
|
+
return JSON.stringify(sorted);
|
|
1678
|
+
}
|
|
1304
1679
|
/**
|
|
1305
1680
|
* Apply write(s) to a View's cache and fire appropriate events.
|
|
1306
1681
|
* This is the core logic shared between server events and optimistic writes.
|
|
@@ -1313,7 +1688,11 @@ var SubscriptionManager = class {
|
|
|
1313
1688
|
applyWriteToView(view, updates, isVolatile, serverTimestamp) {
|
|
1314
1689
|
const previousOrder = view.orderedChildren;
|
|
1315
1690
|
const previousChildSet = new Set(previousOrder);
|
|
1316
|
-
const
|
|
1691
|
+
const isFirstSnapshot = !view.hasReceivedInitialSnapshot;
|
|
1692
|
+
let previousCacheJson = null;
|
|
1693
|
+
if (!isVolatile) {
|
|
1694
|
+
previousCacheJson = this.serializeCacheForComparison(view.getCache());
|
|
1695
|
+
}
|
|
1317
1696
|
const affectedChildren = /* @__PURE__ */ new Set();
|
|
1318
1697
|
let isFullSnapshot = false;
|
|
1319
1698
|
for (const { relativePath, value } of updates) {
|
|
@@ -1334,9 +1713,11 @@ var SubscriptionManager = class {
|
|
|
1334
1713
|
}
|
|
1335
1714
|
const currentOrder = view.orderedChildren;
|
|
1336
1715
|
const currentChildSet = new Set(currentOrder);
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1716
|
+
if (!isVolatile && !isFirstSnapshot && previousCacheJson !== null) {
|
|
1717
|
+
const currentCacheJson = this.serializeCacheForComparison(view.getCache());
|
|
1718
|
+
if (previousCacheJson === currentCacheJson) {
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1340
1721
|
}
|
|
1341
1722
|
const valueSubs = view.getCallbacks("value");
|
|
1342
1723
|
if (valueSubs.length > 0) {
|
|
@@ -1546,106 +1927,6 @@ var SubscriptionManager = class {
|
|
|
1546
1927
|
}
|
|
1547
1928
|
};
|
|
1548
1929
|
|
|
1549
|
-
// src/connection/WebSocketClient.ts
|
|
1550
|
-
import WebSocketNode from "ws";
|
|
1551
|
-
var WebSocketImpl = typeof WebSocket !== "undefined" ? WebSocket : WebSocketNode;
|
|
1552
|
-
var WebSocketClient = class {
|
|
1553
|
-
constructor(options) {
|
|
1554
|
-
this.ws = null;
|
|
1555
|
-
this._state = "disconnected";
|
|
1556
|
-
this.options = options;
|
|
1557
|
-
}
|
|
1558
|
-
get state() {
|
|
1559
|
-
return this._state;
|
|
1560
|
-
}
|
|
1561
|
-
get connected() {
|
|
1562
|
-
return this._state === "connected";
|
|
1563
|
-
}
|
|
1564
|
-
/**
|
|
1565
|
-
* Connect to a WebSocket server.
|
|
1566
|
-
*/
|
|
1567
|
-
connect(url) {
|
|
1568
|
-
return new Promise((resolve, reject) => {
|
|
1569
|
-
if (this._state !== "disconnected") {
|
|
1570
|
-
reject(new Error("Already connected or connecting"));
|
|
1571
|
-
return;
|
|
1572
|
-
}
|
|
1573
|
-
this._state = "connecting";
|
|
1574
|
-
try {
|
|
1575
|
-
this.ws = new WebSocketImpl(url);
|
|
1576
|
-
} catch (err) {
|
|
1577
|
-
this._state = "disconnected";
|
|
1578
|
-
reject(err);
|
|
1579
|
-
return;
|
|
1580
|
-
}
|
|
1581
|
-
const onOpen = () => {
|
|
1582
|
-
cleanup();
|
|
1583
|
-
this._state = "connected";
|
|
1584
|
-
this.setupEventHandlers();
|
|
1585
|
-
resolve();
|
|
1586
|
-
this.options.onOpen();
|
|
1587
|
-
};
|
|
1588
|
-
const onError = (event) => {
|
|
1589
|
-
cleanup();
|
|
1590
|
-
this._state = "disconnected";
|
|
1591
|
-
this.ws = null;
|
|
1592
|
-
reject(new Error("WebSocket connection failed"));
|
|
1593
|
-
this.options.onError(event);
|
|
1594
|
-
};
|
|
1595
|
-
const onClose = (event) => {
|
|
1596
|
-
cleanup();
|
|
1597
|
-
this._state = "disconnected";
|
|
1598
|
-
this.ws = null;
|
|
1599
|
-
reject(new Error(`WebSocket closed: ${event.code} ${event.reason}`));
|
|
1600
|
-
};
|
|
1601
|
-
const cleanup = () => {
|
|
1602
|
-
this.ws?.removeEventListener("open", onOpen);
|
|
1603
|
-
this.ws?.removeEventListener("error", onError);
|
|
1604
|
-
this.ws?.removeEventListener("close", onClose);
|
|
1605
|
-
};
|
|
1606
|
-
this.ws.addEventListener("open", onOpen);
|
|
1607
|
-
this.ws.addEventListener("error", onError);
|
|
1608
|
-
this.ws.addEventListener("close", onClose);
|
|
1609
|
-
});
|
|
1610
|
-
}
|
|
1611
|
-
/**
|
|
1612
|
-
* Set up persistent event handlers after connection is established.
|
|
1613
|
-
*/
|
|
1614
|
-
setupEventHandlers() {
|
|
1615
|
-
if (!this.ws) return;
|
|
1616
|
-
this.ws.addEventListener("message", (event) => {
|
|
1617
|
-
this.options.onMessage(event.data);
|
|
1618
|
-
});
|
|
1619
|
-
this.ws.addEventListener("close", (event) => {
|
|
1620
|
-
this._state = "disconnected";
|
|
1621
|
-
this.ws = null;
|
|
1622
|
-
this.options.onClose(event.code, event.reason);
|
|
1623
|
-
});
|
|
1624
|
-
this.ws.addEventListener("error", (event) => {
|
|
1625
|
-
this.options.onError(event);
|
|
1626
|
-
});
|
|
1627
|
-
}
|
|
1628
|
-
/**
|
|
1629
|
-
* Send a message over the WebSocket.
|
|
1630
|
-
*/
|
|
1631
|
-
send(data) {
|
|
1632
|
-
if (!this.ws || this._state !== "connected") {
|
|
1633
|
-
throw new Error("WebSocket not connected");
|
|
1634
|
-
}
|
|
1635
|
-
this.ws.send(data);
|
|
1636
|
-
}
|
|
1637
|
-
/**
|
|
1638
|
-
* Close the WebSocket connection.
|
|
1639
|
-
*/
|
|
1640
|
-
close(code = 1e3, reason = "Client disconnect") {
|
|
1641
|
-
if (this.ws) {
|
|
1642
|
-
this.ws.close(code, reason);
|
|
1643
|
-
this.ws = null;
|
|
1644
|
-
}
|
|
1645
|
-
this._state = "disconnected";
|
|
1646
|
-
}
|
|
1647
|
-
};
|
|
1648
|
-
|
|
1649
1930
|
// src/OnDisconnect.ts
|
|
1650
1931
|
var OnDisconnect = class {
|
|
1651
1932
|
constructor(db, path) {
|
|
@@ -1693,6 +1974,27 @@ var OnDisconnect = class {
|
|
|
1693
1974
|
}
|
|
1694
1975
|
};
|
|
1695
1976
|
|
|
1977
|
+
// src/utils/hash.ts
|
|
1978
|
+
import canonicalize from "canonicalize";
|
|
1979
|
+
async function hashValue(value) {
|
|
1980
|
+
const canonical = canonicalize(value);
|
|
1981
|
+
if (canonical === void 0) {
|
|
1982
|
+
return hashString("");
|
|
1983
|
+
}
|
|
1984
|
+
return hashString(canonical);
|
|
1985
|
+
}
|
|
1986
|
+
async function hashString(str) {
|
|
1987
|
+
const encoder = new TextEncoder();
|
|
1988
|
+
const data = encoder.encode(str);
|
|
1989
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
1990
|
+
const hashArray = new Uint8Array(hashBuffer);
|
|
1991
|
+
const hashHex = Array.from(hashArray).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1992
|
+
return hashHex;
|
|
1993
|
+
}
|
|
1994
|
+
function isPrimitive(value) {
|
|
1995
|
+
return value === null || typeof value !== "object";
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1696
1998
|
// src/utils/pushid.ts
|
|
1697
1999
|
var PUSH_CHARS = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz";
|
|
1698
2000
|
var lastPushTime = 0;
|
|
@@ -1777,8 +2079,15 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
1777
2079
|
// ============================================
|
|
1778
2080
|
/**
|
|
1779
2081
|
* Set the data at this location, overwriting any existing data.
|
|
2082
|
+
*
|
|
2083
|
+
* For volatile paths (high-frequency updates), this is fire-and-forget
|
|
2084
|
+
* and resolves immediately without waiting for server confirmation.
|
|
1780
2085
|
*/
|
|
1781
2086
|
async set(value) {
|
|
2087
|
+
if (this._db.isVolatilePath(this._path)) {
|
|
2088
|
+
this._db._sendVolatileSet(this._path, value);
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
1782
2091
|
await this._db._sendSet(this._path, value);
|
|
1783
2092
|
}
|
|
1784
2093
|
/**
|
|
@@ -1815,13 +2124,23 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
1815
2124
|
}
|
|
1816
2125
|
await this._db._sendTransaction(ops);
|
|
1817
2126
|
} else {
|
|
2127
|
+
if (this._db.isVolatilePath(this._path)) {
|
|
2128
|
+
this._db._sendVolatileUpdate(this._path, values);
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
1818
2131
|
await this._db._sendUpdate(this._path, values);
|
|
1819
2132
|
}
|
|
1820
2133
|
}
|
|
1821
2134
|
/**
|
|
1822
2135
|
* Remove the data at this location.
|
|
2136
|
+
*
|
|
2137
|
+
* For volatile paths, this is fire-and-forget.
|
|
1823
2138
|
*/
|
|
1824
2139
|
async remove() {
|
|
2140
|
+
if (this._db.isVolatilePath(this._path)) {
|
|
2141
|
+
this._db._sendVolatileDelete(this._path);
|
|
2142
|
+
return;
|
|
2143
|
+
}
|
|
1825
2144
|
await this._db._sendDelete(this._path);
|
|
1826
2145
|
}
|
|
1827
2146
|
/**
|
|
@@ -1906,10 +2225,14 @@ var DatabaseReference = class _DatabaseReference {
|
|
|
1906
2225
|
snapshot: currentSnapshot
|
|
1907
2226
|
};
|
|
1908
2227
|
}
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
{ o: "
|
|
1912
|
-
|
|
2228
|
+
let condition;
|
|
2229
|
+
if (isPrimitive(currentValue)) {
|
|
2230
|
+
condition = { o: "c", p: this._path, v: currentValue };
|
|
2231
|
+
} else {
|
|
2232
|
+
const hash = await hashValue(currentValue);
|
|
2233
|
+
condition = { o: "c", p: this._path, h: hash };
|
|
2234
|
+
}
|
|
2235
|
+
const ops = [condition, { o: "s", p: this._path, v: newValue }];
|
|
1913
2236
|
try {
|
|
1914
2237
|
await this._db._sendTransaction(ops);
|
|
1915
2238
|
const finalSnapshot = await this.once();
|
|
@@ -2274,6 +2597,28 @@ function decodeJwtPayload(token) {
|
|
|
2274
2597
|
return JSON.parse(decoded);
|
|
2275
2598
|
}
|
|
2276
2599
|
|
|
2600
|
+
// src/utils/volatile.ts
|
|
2601
|
+
function isVolatilePath(path, patterns) {
|
|
2602
|
+
if (!patterns || patterns.length === 0) {
|
|
2603
|
+
return false;
|
|
2604
|
+
}
|
|
2605
|
+
const segments = path.replace(/^\//, "").split("/").filter((s) => s.length > 0);
|
|
2606
|
+
return patterns.some((pattern) => {
|
|
2607
|
+
const patternSegments = pattern.replace(/^\//, "").split("/").filter((s) => s.length > 0);
|
|
2608
|
+
if (segments.length < patternSegments.length) {
|
|
2609
|
+
return false;
|
|
2610
|
+
}
|
|
2611
|
+
for (let i = 0; i < patternSegments.length; i++) {
|
|
2612
|
+
const p = patternSegments[i];
|
|
2613
|
+
const s = segments[i];
|
|
2614
|
+
if (p !== "*" && p !== s) {
|
|
2615
|
+
return false;
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
return true;
|
|
2619
|
+
});
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2277
2622
|
// src/LarkDatabase.ts
|
|
2278
2623
|
var RECONNECT_BASE_DELAY_MS = 1e3;
|
|
2279
2624
|
var RECONNECT_MAX_DELAY_MS = 3e4;
|
|
@@ -2285,13 +2630,14 @@ var LarkDatabase = class {
|
|
|
2285
2630
|
this._databaseId = null;
|
|
2286
2631
|
this._coordinatorUrl = null;
|
|
2287
2632
|
this._volatilePaths = [];
|
|
2633
|
+
this._transportType = null;
|
|
2288
2634
|
// Reconnection state
|
|
2289
2635
|
this._connectionId = null;
|
|
2290
2636
|
this._connectOptions = null;
|
|
2291
2637
|
this._intentionalDisconnect = false;
|
|
2292
2638
|
this._reconnectAttempt = 0;
|
|
2293
2639
|
this._reconnectTimer = null;
|
|
2294
|
-
this.
|
|
2640
|
+
this.transport = null;
|
|
2295
2641
|
// Event callbacks
|
|
2296
2642
|
this.connectCallbacks = /* @__PURE__ */ new Set();
|
|
2297
2643
|
this.disconnectCallbacks = /* @__PURE__ */ new Set();
|
|
@@ -2340,11 +2686,17 @@ var LarkDatabase = class {
|
|
|
2340
2686
|
/**
|
|
2341
2687
|
* Get the volatile path patterns from the server.
|
|
2342
2688
|
* These patterns indicate which paths should use unreliable transport.
|
|
2343
|
-
* WebSocket always uses reliable transport, but this is stored for future UDP support.
|
|
2344
2689
|
*/
|
|
2345
2690
|
get volatilePaths() {
|
|
2346
2691
|
return this._volatilePaths;
|
|
2347
2692
|
}
|
|
2693
|
+
/**
|
|
2694
|
+
* Get the current transport type.
|
|
2695
|
+
* Returns 'websocket' or 'webtransport', or null if not connected.
|
|
2696
|
+
*/
|
|
2697
|
+
get transportType() {
|
|
2698
|
+
return this._transportType;
|
|
2699
|
+
}
|
|
2348
2700
|
/**
|
|
2349
2701
|
* Check if there are any pending writes waiting for acknowledgment.
|
|
2350
2702
|
* Useful for showing "saving..." indicators in UI.
|
|
@@ -2398,15 +2750,20 @@ var LarkDatabase = class {
|
|
|
2398
2750
|
anonymous: options.anonymous
|
|
2399
2751
|
});
|
|
2400
2752
|
const wsUrl = connectResponse.ws_url;
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2753
|
+
const transportResult = await createTransport(
|
|
2754
|
+
wsUrl,
|
|
2755
|
+
{
|
|
2756
|
+
onMessage: this.handleMessage.bind(this),
|
|
2757
|
+
onOpen: () => {
|
|
2758
|
+
},
|
|
2759
|
+
// Handled in connect flow
|
|
2760
|
+
onClose: this.handleClose.bind(this),
|
|
2761
|
+
onError: this.handleError.bind(this)
|
|
2404
2762
|
},
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
await this.ws.connect(wsUrl);
|
|
2763
|
+
{ transport: options.transport }
|
|
2764
|
+
);
|
|
2765
|
+
this.transport = transportResult.transport;
|
|
2766
|
+
this._transportType = transportResult.type;
|
|
2410
2767
|
const requestId = this.messageQueue.nextRequestId();
|
|
2411
2768
|
const joinMessage = {
|
|
2412
2769
|
o: "j",
|
|
@@ -2450,8 +2807,9 @@ var LarkDatabase = class {
|
|
|
2450
2807
|
this._databaseId = null;
|
|
2451
2808
|
this._connectOptions = null;
|
|
2452
2809
|
this._connectionId = null;
|
|
2453
|
-
this.
|
|
2454
|
-
this.
|
|
2810
|
+
this._transportType = null;
|
|
2811
|
+
this.transport?.close();
|
|
2812
|
+
this.transport = null;
|
|
2455
2813
|
throw error;
|
|
2456
2814
|
}
|
|
2457
2815
|
}
|
|
@@ -2469,7 +2827,7 @@ var LarkDatabase = class {
|
|
|
2469
2827
|
clearTimeout(this._reconnectTimer);
|
|
2470
2828
|
this._reconnectTimer = null;
|
|
2471
2829
|
}
|
|
2472
|
-
if (wasConnected && this.
|
|
2830
|
+
if (wasConnected && this.transport) {
|
|
2473
2831
|
try {
|
|
2474
2832
|
const requestId = this.messageQueue.nextRequestId();
|
|
2475
2833
|
this.send({ o: "l", r: requestId });
|
|
@@ -2490,8 +2848,8 @@ var LarkDatabase = class {
|
|
|
2490
2848
|
* Used for intentional disconnect.
|
|
2491
2849
|
*/
|
|
2492
2850
|
cleanupFull() {
|
|
2493
|
-
this.
|
|
2494
|
-
this.
|
|
2851
|
+
this.transport?.close();
|
|
2852
|
+
this.transport = null;
|
|
2495
2853
|
this._state = "disconnected";
|
|
2496
2854
|
this._auth = null;
|
|
2497
2855
|
this._databaseId = null;
|
|
@@ -2499,6 +2857,7 @@ var LarkDatabase = class {
|
|
|
2499
2857
|
this._coordinatorUrl = null;
|
|
2500
2858
|
this._connectionId = null;
|
|
2501
2859
|
this._connectOptions = null;
|
|
2860
|
+
this._transportType = null;
|
|
2502
2861
|
this._reconnectAttempt = 0;
|
|
2503
2862
|
this.subscriptionManager.clear();
|
|
2504
2863
|
this.messageQueue.rejectAll(new Error("Connection closed"));
|
|
@@ -2509,8 +2868,8 @@ var LarkDatabase = class {
|
|
|
2509
2868
|
* Used for unexpected disconnect.
|
|
2510
2869
|
*/
|
|
2511
2870
|
cleanupForReconnect() {
|
|
2512
|
-
this.
|
|
2513
|
-
this.
|
|
2871
|
+
this.transport?.close();
|
|
2872
|
+
this.transport = null;
|
|
2514
2873
|
this._auth = null;
|
|
2515
2874
|
this.subscriptionManager.clearCacheOnly();
|
|
2516
2875
|
this.messageQueue.rejectAll(new Error("Connection closed"));
|
|
@@ -2646,7 +3005,7 @@ var LarkDatabase = class {
|
|
|
2646
3005
|
async transaction(operations) {
|
|
2647
3006
|
let txOps;
|
|
2648
3007
|
if (Array.isArray(operations)) {
|
|
2649
|
-
txOps = operations.map((op) => this.convertToTxOp(op));
|
|
3008
|
+
txOps = await Promise.all(operations.map((op) => this.convertToTxOp(op)));
|
|
2650
3009
|
} else {
|
|
2651
3010
|
txOps = this.convertObjectToTxOps(operations);
|
|
2652
3011
|
}
|
|
@@ -2654,8 +3013,9 @@ var LarkDatabase = class {
|
|
|
2654
3013
|
}
|
|
2655
3014
|
/**
|
|
2656
3015
|
* Convert a public TransactionOp to wire format TxOperation.
|
|
3016
|
+
* Async because condition operations may require hash computation.
|
|
2657
3017
|
*/
|
|
2658
|
-
convertToTxOp(op) {
|
|
3018
|
+
async convertToTxOp(op) {
|
|
2659
3019
|
const path = normalizePath(op.path) || "/";
|
|
2660
3020
|
switch (op.op) {
|
|
2661
3021
|
case "set":
|
|
@@ -2665,7 +3025,12 @@ var LarkDatabase = class {
|
|
|
2665
3025
|
case "delete":
|
|
2666
3026
|
return { o: "d", p: path };
|
|
2667
3027
|
case "condition":
|
|
2668
|
-
|
|
3028
|
+
if (isPrimitive(op.value)) {
|
|
3029
|
+
return { o: "c", p: path, v: op.value };
|
|
3030
|
+
} else {
|
|
3031
|
+
const hash = await hashValue(op.value);
|
|
3032
|
+
return { o: "c", p: path, h: hash };
|
|
3033
|
+
}
|
|
2669
3034
|
default:
|
|
2670
3035
|
throw new Error(`Unknown transaction operation: ${op.op}`);
|
|
2671
3036
|
}
|
|
@@ -2748,7 +3113,7 @@ var LarkDatabase = class {
|
|
|
2748
3113
|
return;
|
|
2749
3114
|
}
|
|
2750
3115
|
if (isPingMessage(message)) {
|
|
2751
|
-
this.
|
|
3116
|
+
this.transport?.send(JSON.stringify({ o: "po" }));
|
|
2752
3117
|
return;
|
|
2753
3118
|
}
|
|
2754
3119
|
if (isAckMessage(message)) {
|
|
@@ -2815,18 +3180,17 @@ var LarkDatabase = class {
|
|
|
2815
3180
|
}
|
|
2816
3181
|
}
|
|
2817
3182
|
}
|
|
2818
|
-
handleError(
|
|
2819
|
-
const error = new Error("WebSocket error");
|
|
3183
|
+
handleError(error) {
|
|
2820
3184
|
this.errorCallbacks.forEach((cb) => cb(error));
|
|
2821
3185
|
}
|
|
2822
3186
|
// ============================================
|
|
2823
3187
|
// Internal: Sending Messages
|
|
2824
3188
|
// ============================================
|
|
2825
3189
|
send(message) {
|
|
2826
|
-
if (!this.
|
|
3190
|
+
if (!this.transport || !this.transport.connected) {
|
|
2827
3191
|
throw new LarkError("not_connected", "Not connected to database");
|
|
2828
3192
|
}
|
|
2829
|
-
this.
|
|
3193
|
+
this.transport.send(JSON.stringify(message));
|
|
2830
3194
|
}
|
|
2831
3195
|
/**
|
|
2832
3196
|
* @internal Send a set operation.
|
|
@@ -2897,6 +3261,73 @@ var LarkDatabase = class {
|
|
|
2897
3261
|
this.send(message);
|
|
2898
3262
|
await this.messageQueue.registerRequest(requestId);
|
|
2899
3263
|
}
|
|
3264
|
+
// ============================================
|
|
3265
|
+
// Volatile Write Operations (Fire-and-Forget)
|
|
3266
|
+
// ============================================
|
|
3267
|
+
/**
|
|
3268
|
+
* @internal Send a volatile set operation (fire-and-forget).
|
|
3269
|
+
*
|
|
3270
|
+
* Volatile writes skip:
|
|
3271
|
+
* - Recovery checks (volatile paths don't participate in recovery)
|
|
3272
|
+
* - Request ID generation (no ack expected)
|
|
3273
|
+
* - Pending write tracking (no retry on reconnect)
|
|
3274
|
+
* - pw field (no dependency tracking)
|
|
3275
|
+
*
|
|
3276
|
+
* The write is applied optimistically to local cache for UI feedback,
|
|
3277
|
+
* but we don't await server confirmation.
|
|
3278
|
+
*
|
|
3279
|
+
* When using WebTransport, volatile writes are sent via datagrams (UDP).
|
|
3280
|
+
*/
|
|
3281
|
+
_sendVolatileSet(path, value) {
|
|
3282
|
+
const normalizedPath = normalizePath(path) || "/";
|
|
3283
|
+
this.subscriptionManager.applyOptimisticWrite(normalizedPath, value, "", "set");
|
|
3284
|
+
if (!this.transport || !this.transport.connected) {
|
|
3285
|
+
return;
|
|
3286
|
+
}
|
|
3287
|
+
const message = {
|
|
3288
|
+
o: "s",
|
|
3289
|
+
p: normalizedPath,
|
|
3290
|
+
v: value
|
|
3291
|
+
};
|
|
3292
|
+
this.transport.sendVolatile(JSON.stringify(message));
|
|
3293
|
+
}
|
|
3294
|
+
/**
|
|
3295
|
+
* @internal Send a volatile update operation (fire-and-forget).
|
|
3296
|
+
*/
|
|
3297
|
+
_sendVolatileUpdate(path, values) {
|
|
3298
|
+
const normalizedPath = normalizePath(path) || "/";
|
|
3299
|
+
this.subscriptionManager.applyOptimisticWrite(normalizedPath, values, "", "update");
|
|
3300
|
+
if (!this.transport || !this.transport.connected) {
|
|
3301
|
+
return;
|
|
3302
|
+
}
|
|
3303
|
+
const message = {
|
|
3304
|
+
o: "u",
|
|
3305
|
+
p: normalizedPath,
|
|
3306
|
+
v: values
|
|
3307
|
+
};
|
|
3308
|
+
this.transport.sendVolatile(JSON.stringify(message));
|
|
3309
|
+
}
|
|
3310
|
+
/**
|
|
3311
|
+
* @internal Send a volatile delete operation (fire-and-forget).
|
|
3312
|
+
*/
|
|
3313
|
+
_sendVolatileDelete(path) {
|
|
3314
|
+
const normalizedPath = normalizePath(path) || "/";
|
|
3315
|
+
this.subscriptionManager.applyOptimisticWrite(normalizedPath, null, "", "delete");
|
|
3316
|
+
if (!this.transport || !this.transport.connected) {
|
|
3317
|
+
return;
|
|
3318
|
+
}
|
|
3319
|
+
const message = {
|
|
3320
|
+
o: "d",
|
|
3321
|
+
p: normalizedPath
|
|
3322
|
+
};
|
|
3323
|
+
this.transport.sendVolatile(JSON.stringify(message));
|
|
3324
|
+
}
|
|
3325
|
+
/**
|
|
3326
|
+
* Check if a path is a volatile path (high-frequency, fire-and-forget).
|
|
3327
|
+
*/
|
|
3328
|
+
isVolatilePath(path) {
|
|
3329
|
+
return isVolatilePath(path, this._volatilePaths);
|
|
3330
|
+
}
|
|
2900
3331
|
/**
|
|
2901
3332
|
* @internal Send a once (read) operation.
|
|
2902
3333
|
*
|
|
@@ -3005,21 +3436,6 @@ var LarkDatabase = class {
|
|
|
3005
3436
|
this.subscriptionManager.unsubscribeAll(path);
|
|
3006
3437
|
}
|
|
3007
3438
|
};
|
|
3008
|
-
|
|
3009
|
-
// src/utils/volatile.ts
|
|
3010
|
-
function isVolatilePath(path, patterns) {
|
|
3011
|
-
if (!patterns || patterns.length === 0) {
|
|
3012
|
-
return false;
|
|
3013
|
-
}
|
|
3014
|
-
const segments = path.replace(/^\//, "").split("/");
|
|
3015
|
-
return patterns.some((pattern) => {
|
|
3016
|
-
const patternSegments = pattern.split("/");
|
|
3017
|
-
if (segments.length !== patternSegments.length) {
|
|
3018
|
-
return false;
|
|
3019
|
-
}
|
|
3020
|
-
return patternSegments.every((p, i) => p === "*" || p === segments[i]);
|
|
3021
|
-
});
|
|
3022
|
-
}
|
|
3023
3439
|
export {
|
|
3024
3440
|
DataSnapshot,
|
|
3025
3441
|
DatabaseReference,
|