@lark-sh/client 0.1.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/README.md +52 -0
- package/dist/index.d.mts +415 -0
- package/dist/index.d.ts +415 -0
- package/dist/index.js +1391 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1349 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +45 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1391 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
DataSnapshot: () => DataSnapshot,
|
|
34
|
+
DatabaseReference: () => DatabaseReference,
|
|
35
|
+
LarkDatabase: () => LarkDatabase,
|
|
36
|
+
LarkError: () => LarkError,
|
|
37
|
+
OnDisconnect: () => OnDisconnect,
|
|
38
|
+
generatePushId: () => generatePushId
|
|
39
|
+
});
|
|
40
|
+
module.exports = __toCommonJS(index_exports);
|
|
41
|
+
|
|
42
|
+
// src/protocol/constants.ts
|
|
43
|
+
var OnDisconnectAction = {
|
|
44
|
+
SET: "s",
|
|
45
|
+
UPDATE: "u",
|
|
46
|
+
DELETE: "d",
|
|
47
|
+
CANCEL: "c"
|
|
48
|
+
};
|
|
49
|
+
var eventTypeFromShort = {
|
|
50
|
+
v: "value",
|
|
51
|
+
ca: "child_added",
|
|
52
|
+
cc: "child_changed",
|
|
53
|
+
cr: "child_removed"
|
|
54
|
+
};
|
|
55
|
+
var eventTypeToShort = {
|
|
56
|
+
value: "v",
|
|
57
|
+
child_added: "ca",
|
|
58
|
+
child_changed: "cc",
|
|
59
|
+
child_removed: "cr"
|
|
60
|
+
};
|
|
61
|
+
var DEFAULT_COORDINATOR_URL = "https://db.lark.dev";
|
|
62
|
+
|
|
63
|
+
// src/connection/Coordinator.ts
|
|
64
|
+
var Coordinator = class {
|
|
65
|
+
constructor(baseUrl = DEFAULT_COORDINATOR_URL) {
|
|
66
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Request connection details from the coordinator.
|
|
70
|
+
*
|
|
71
|
+
* @param database - Database ID in format "project/database"
|
|
72
|
+
* @param options - Either a user token or anonymous flag
|
|
73
|
+
* @returns Connection details including host, port, and connection token
|
|
74
|
+
*/
|
|
75
|
+
async connect(database, options = {}) {
|
|
76
|
+
const body = { database };
|
|
77
|
+
if (options.token) {
|
|
78
|
+
body.token = options.token;
|
|
79
|
+
} else if (options.anonymous) {
|
|
80
|
+
body.anonymous = true;
|
|
81
|
+
} else {
|
|
82
|
+
body.anonymous = true;
|
|
83
|
+
}
|
|
84
|
+
const response = await fetch(`${this.baseUrl}/connect`, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: {
|
|
87
|
+
"Content-Type": "application/json"
|
|
88
|
+
},
|
|
89
|
+
body: JSON.stringify(body)
|
|
90
|
+
});
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
let errorMessage = `Coordinator request failed: ${response.status}`;
|
|
93
|
+
try {
|
|
94
|
+
const errorBody = await response.json();
|
|
95
|
+
if (errorBody.message) {
|
|
96
|
+
errorMessage = errorBody.message;
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
}
|
|
100
|
+
throw new Error(errorMessage);
|
|
101
|
+
}
|
|
102
|
+
const data = await response.json();
|
|
103
|
+
return data;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// src/LarkError.ts
|
|
108
|
+
var LarkError = class _LarkError extends Error {
|
|
109
|
+
constructor(code, message) {
|
|
110
|
+
super(message || code);
|
|
111
|
+
this.code = code;
|
|
112
|
+
this.name = "LarkError";
|
|
113
|
+
const ErrorWithCapture = Error;
|
|
114
|
+
if (ErrorWithCapture.captureStackTrace) {
|
|
115
|
+
ErrorWithCapture.captureStackTrace(this, _LarkError);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// src/protocol/messages.ts
|
|
121
|
+
function isAckMessage(msg) {
|
|
122
|
+
return "a" in msg && !("uid" in msg);
|
|
123
|
+
}
|
|
124
|
+
function isJoinAckMessage(msg) {
|
|
125
|
+
return "a" in msg && "uid" in msg;
|
|
126
|
+
}
|
|
127
|
+
function isNackMessage(msg) {
|
|
128
|
+
return "n" in msg;
|
|
129
|
+
}
|
|
130
|
+
function isEventMessage(msg) {
|
|
131
|
+
return "ev" in msg;
|
|
132
|
+
}
|
|
133
|
+
function isOnceResponseMessage(msg) {
|
|
134
|
+
return "oc" in msg;
|
|
135
|
+
}
|
|
136
|
+
function isPushAckMessage(msg) {
|
|
137
|
+
return "pa" in msg;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/connection/MessageQueue.ts
|
|
141
|
+
var MessageQueue = class {
|
|
142
|
+
constructor(defaultTimeout = 3e4) {
|
|
143
|
+
this.nextId = 1;
|
|
144
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
145
|
+
this.defaultTimeout = defaultTimeout;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Generate a new unique request ID.
|
|
149
|
+
*/
|
|
150
|
+
nextRequestId() {
|
|
151
|
+
return String(this.nextId++);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Register a pending request that expects a response.
|
|
155
|
+
* Returns a promise that resolves when ack is received, or rejects on nack/timeout.
|
|
156
|
+
*/
|
|
157
|
+
registerRequest(requestId, timeout) {
|
|
158
|
+
return new Promise((resolve, reject) => {
|
|
159
|
+
const timeoutMs = timeout ?? this.defaultTimeout;
|
|
160
|
+
const timeoutHandle = setTimeout(() => {
|
|
161
|
+
this.pending.delete(requestId);
|
|
162
|
+
reject(new LarkError("timeout", `Request ${requestId} timed out after ${timeoutMs}ms`));
|
|
163
|
+
}, timeoutMs);
|
|
164
|
+
this.pending.set(requestId, {
|
|
165
|
+
resolve,
|
|
166
|
+
reject,
|
|
167
|
+
timeout: timeoutHandle
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Handle an incoming server message. If it's a response to a pending request,
|
|
173
|
+
* resolve or reject the corresponding promise.
|
|
174
|
+
*
|
|
175
|
+
* @returns true if the message was handled (was a response), false otherwise
|
|
176
|
+
*/
|
|
177
|
+
handleMessage(message) {
|
|
178
|
+
if (isJoinAckMessage(message)) {
|
|
179
|
+
const pending = this.pending.get(message.a);
|
|
180
|
+
if (pending) {
|
|
181
|
+
clearTimeout(pending.timeout);
|
|
182
|
+
this.pending.delete(message.a);
|
|
183
|
+
pending.resolve({
|
|
184
|
+
uid: message.uid,
|
|
185
|
+
provider: message.provider,
|
|
186
|
+
token: message.token
|
|
187
|
+
});
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (isAckMessage(message)) {
|
|
192
|
+
const pending = this.pending.get(message.a);
|
|
193
|
+
if (pending) {
|
|
194
|
+
clearTimeout(pending.timeout);
|
|
195
|
+
this.pending.delete(message.a);
|
|
196
|
+
pending.resolve(void 0);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (isNackMessage(message)) {
|
|
201
|
+
const pending = this.pending.get(message.n);
|
|
202
|
+
if (pending) {
|
|
203
|
+
clearTimeout(pending.timeout);
|
|
204
|
+
this.pending.delete(message.n);
|
|
205
|
+
pending.reject(new LarkError(message.e, message.m));
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (isOnceResponseMessage(message)) {
|
|
210
|
+
const pending = this.pending.get(message.oc);
|
|
211
|
+
if (pending) {
|
|
212
|
+
clearTimeout(pending.timeout);
|
|
213
|
+
this.pending.delete(message.oc);
|
|
214
|
+
pending.resolve(message.ov);
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (isPushAckMessage(message)) {
|
|
219
|
+
const pending = this.pending.get(message.pa);
|
|
220
|
+
if (pending) {
|
|
221
|
+
clearTimeout(pending.timeout);
|
|
222
|
+
this.pending.delete(message.pa);
|
|
223
|
+
pending.resolve(message.pk);
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Reject all pending requests (e.g., on disconnect).
|
|
231
|
+
*/
|
|
232
|
+
rejectAll(error) {
|
|
233
|
+
for (const [, pending] of this.pending) {
|
|
234
|
+
clearTimeout(pending.timeout);
|
|
235
|
+
pending.reject(error);
|
|
236
|
+
}
|
|
237
|
+
this.pending.clear();
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Get the number of pending requests.
|
|
241
|
+
*/
|
|
242
|
+
get pendingCount() {
|
|
243
|
+
return this.pending.size;
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// src/connection/SubscriptionManager.ts
|
|
248
|
+
var SubscriptionManager = class {
|
|
249
|
+
constructor() {
|
|
250
|
+
// path -> eventType -> array of subscriptions
|
|
251
|
+
this.subscriptions = /* @__PURE__ */ new Map();
|
|
252
|
+
// Callback to send subscribe message to server
|
|
253
|
+
this.sendSubscribe = null;
|
|
254
|
+
// Callback to send unsubscribe message to server
|
|
255
|
+
this.sendUnsubscribe = null;
|
|
256
|
+
// Callback to create DataSnapshot from event data
|
|
257
|
+
this.createSnapshot = null;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Initialize the manager with server communication callbacks.
|
|
261
|
+
*/
|
|
262
|
+
initialize(options) {
|
|
263
|
+
this.sendSubscribe = options.sendSubscribe;
|
|
264
|
+
this.sendUnsubscribe = options.sendUnsubscribe;
|
|
265
|
+
this.createSnapshot = options.createSnapshot;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Subscribe to events at a path.
|
|
269
|
+
* Returns an unsubscribe function.
|
|
270
|
+
*/
|
|
271
|
+
subscribe(path, eventType, callback) {
|
|
272
|
+
const normalizedPath = path;
|
|
273
|
+
const isFirstForPath = !this.subscriptions.has(normalizedPath);
|
|
274
|
+
const existingEventTypes = this.getEventTypesForPath(normalizedPath);
|
|
275
|
+
const isFirstForEventType = !existingEventTypes.includes(eventType);
|
|
276
|
+
if (!this.subscriptions.has(normalizedPath)) {
|
|
277
|
+
this.subscriptions.set(normalizedPath, /* @__PURE__ */ new Map());
|
|
278
|
+
}
|
|
279
|
+
const pathSubs = this.subscriptions.get(normalizedPath);
|
|
280
|
+
if (!pathSubs.has(eventType)) {
|
|
281
|
+
pathSubs.set(eventType, []);
|
|
282
|
+
}
|
|
283
|
+
const eventSubs = pathSubs.get(eventType);
|
|
284
|
+
const unsubscribe = () => {
|
|
285
|
+
this.unsubscribeCallback(normalizedPath, eventType, callback);
|
|
286
|
+
};
|
|
287
|
+
eventSubs.push({ callback, unsubscribe });
|
|
288
|
+
if (isFirstForPath || isFirstForEventType) {
|
|
289
|
+
const allEventTypes = this.getEventTypesForPath(normalizedPath);
|
|
290
|
+
const shortEventTypes = allEventTypes.map((et) => eventTypeToShort[et]);
|
|
291
|
+
this.sendSubscribe?.(normalizedPath, shortEventTypes).catch((err) => {
|
|
292
|
+
console.error("Failed to subscribe:", err);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
return unsubscribe;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Remove a specific callback from a subscription.
|
|
299
|
+
*/
|
|
300
|
+
unsubscribeCallback(path, eventType, callback) {
|
|
301
|
+
const pathSubs = this.subscriptions.get(path);
|
|
302
|
+
if (!pathSubs) return;
|
|
303
|
+
const eventSubs = pathSubs.get(eventType);
|
|
304
|
+
if (!eventSubs) return;
|
|
305
|
+
const index = eventSubs.findIndex((entry) => entry.callback === callback);
|
|
306
|
+
if (index !== -1) {
|
|
307
|
+
eventSubs.splice(index, 1);
|
|
308
|
+
}
|
|
309
|
+
if (eventSubs.length === 0) {
|
|
310
|
+
pathSubs.delete(eventType);
|
|
311
|
+
}
|
|
312
|
+
if (pathSubs.size === 0) {
|
|
313
|
+
this.subscriptions.delete(path);
|
|
314
|
+
this.sendUnsubscribe?.(path).catch((err) => {
|
|
315
|
+
console.error("Failed to unsubscribe:", err);
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Remove all subscriptions of a specific event type at a path.
|
|
321
|
+
*/
|
|
322
|
+
unsubscribeEventType(path, eventType) {
|
|
323
|
+
const normalizedPath = path;
|
|
324
|
+
const pathSubs = this.subscriptions.get(normalizedPath);
|
|
325
|
+
if (!pathSubs) return;
|
|
326
|
+
pathSubs.delete(eventType);
|
|
327
|
+
if (pathSubs.size === 0) {
|
|
328
|
+
this.subscriptions.delete(normalizedPath);
|
|
329
|
+
this.sendUnsubscribe?.(normalizedPath).catch((err) => {
|
|
330
|
+
console.error("Failed to unsubscribe:", err);
|
|
331
|
+
});
|
|
332
|
+
} else {
|
|
333
|
+
const remainingEventTypes = this.getEventTypesForPath(normalizedPath);
|
|
334
|
+
const shortEventTypes = remainingEventTypes.map((et) => eventTypeToShort[et]);
|
|
335
|
+
this.sendSubscribe?.(normalizedPath, shortEventTypes).catch((err) => {
|
|
336
|
+
console.error("Failed to update subscription:", err);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Remove ALL subscriptions at a path.
|
|
342
|
+
*/
|
|
343
|
+
unsubscribeAll(path) {
|
|
344
|
+
const normalizedPath = path;
|
|
345
|
+
if (!this.subscriptions.has(normalizedPath)) return;
|
|
346
|
+
this.subscriptions.delete(normalizedPath);
|
|
347
|
+
this.sendUnsubscribe?.(normalizedPath).catch((err) => {
|
|
348
|
+
console.error("Failed to unsubscribe:", err);
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Handle an incoming event message from the server.
|
|
353
|
+
*/
|
|
354
|
+
handleEvent(message) {
|
|
355
|
+
const eventType = eventTypeFromShort[message.ev];
|
|
356
|
+
if (!eventType) {
|
|
357
|
+
console.warn("Unknown event type:", message.ev);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const path = message.p;
|
|
361
|
+
const pathSubs = this.subscriptions.get(path);
|
|
362
|
+
if (!pathSubs) return;
|
|
363
|
+
const eventSubs = pathSubs.get(eventType);
|
|
364
|
+
if (!eventSubs || eventSubs.length === 0) return;
|
|
365
|
+
let snapshotPath;
|
|
366
|
+
let snapshotValue;
|
|
367
|
+
if (eventType === "value") {
|
|
368
|
+
snapshotPath = path;
|
|
369
|
+
snapshotValue = message.v;
|
|
370
|
+
} else {
|
|
371
|
+
snapshotPath = path === "/" ? `/${message.k}` : `${path}/${message.k}`;
|
|
372
|
+
snapshotValue = message.v;
|
|
373
|
+
}
|
|
374
|
+
const snapshot = this.createSnapshot?.(snapshotPath, snapshotValue, message.x ?? false);
|
|
375
|
+
if (!snapshot) return;
|
|
376
|
+
for (const entry of eventSubs) {
|
|
377
|
+
try {
|
|
378
|
+
entry.callback(snapshot, void 0);
|
|
379
|
+
} catch (err) {
|
|
380
|
+
console.error("Error in subscription callback:", err);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Get all event types currently subscribed at a path.
|
|
386
|
+
*/
|
|
387
|
+
getEventTypesForPath(path) {
|
|
388
|
+
const pathSubs = this.subscriptions.get(path);
|
|
389
|
+
if (!pathSubs) return [];
|
|
390
|
+
return Array.from(pathSubs.keys());
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Clear all subscriptions (e.g., on disconnect).
|
|
394
|
+
*/
|
|
395
|
+
clear() {
|
|
396
|
+
this.subscriptions.clear();
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Check if there are any subscriptions at a path.
|
|
400
|
+
*/
|
|
401
|
+
hasSubscriptions(path) {
|
|
402
|
+
return this.subscriptions.has(path);
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
// src/connection/WebSocketClient.ts
|
|
407
|
+
var import_ws = __toESM(require("ws"));
|
|
408
|
+
var WebSocketImpl = typeof WebSocket !== "undefined" ? WebSocket : import_ws.default;
|
|
409
|
+
var WebSocketClient = class {
|
|
410
|
+
constructor(options) {
|
|
411
|
+
this.ws = null;
|
|
412
|
+
this._state = "disconnected";
|
|
413
|
+
this.options = options;
|
|
414
|
+
}
|
|
415
|
+
get state() {
|
|
416
|
+
return this._state;
|
|
417
|
+
}
|
|
418
|
+
get connected() {
|
|
419
|
+
return this._state === "connected";
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Connect to a WebSocket server.
|
|
423
|
+
*/
|
|
424
|
+
connect(url) {
|
|
425
|
+
return new Promise((resolve, reject) => {
|
|
426
|
+
if (this._state !== "disconnected") {
|
|
427
|
+
reject(new Error("Already connected or connecting"));
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
this._state = "connecting";
|
|
431
|
+
try {
|
|
432
|
+
this.ws = new WebSocketImpl(url);
|
|
433
|
+
} catch (err) {
|
|
434
|
+
this._state = "disconnected";
|
|
435
|
+
reject(err);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const onOpen = () => {
|
|
439
|
+
cleanup();
|
|
440
|
+
this._state = "connected";
|
|
441
|
+
this.setupEventHandlers();
|
|
442
|
+
resolve();
|
|
443
|
+
this.options.onOpen();
|
|
444
|
+
};
|
|
445
|
+
const onError = (event) => {
|
|
446
|
+
cleanup();
|
|
447
|
+
this._state = "disconnected";
|
|
448
|
+
this.ws = null;
|
|
449
|
+
reject(new Error("WebSocket connection failed"));
|
|
450
|
+
this.options.onError(event);
|
|
451
|
+
};
|
|
452
|
+
const onClose = (event) => {
|
|
453
|
+
cleanup();
|
|
454
|
+
this._state = "disconnected";
|
|
455
|
+
this.ws = null;
|
|
456
|
+
reject(new Error(`WebSocket closed: ${event.code} ${event.reason}`));
|
|
457
|
+
};
|
|
458
|
+
const cleanup = () => {
|
|
459
|
+
this.ws?.removeEventListener("open", onOpen);
|
|
460
|
+
this.ws?.removeEventListener("error", onError);
|
|
461
|
+
this.ws?.removeEventListener("close", onClose);
|
|
462
|
+
};
|
|
463
|
+
this.ws.addEventListener("open", onOpen);
|
|
464
|
+
this.ws.addEventListener("error", onError);
|
|
465
|
+
this.ws.addEventListener("close", onClose);
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Set up persistent event handlers after connection is established.
|
|
470
|
+
*/
|
|
471
|
+
setupEventHandlers() {
|
|
472
|
+
if (!this.ws) return;
|
|
473
|
+
this.ws.addEventListener("message", (event) => {
|
|
474
|
+
this.options.onMessage(event.data);
|
|
475
|
+
});
|
|
476
|
+
this.ws.addEventListener("close", (event) => {
|
|
477
|
+
this._state = "disconnected";
|
|
478
|
+
this.ws = null;
|
|
479
|
+
this.options.onClose(event.code, event.reason);
|
|
480
|
+
});
|
|
481
|
+
this.ws.addEventListener("error", (event) => {
|
|
482
|
+
this.options.onError(event);
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Send a message over the WebSocket.
|
|
487
|
+
*/
|
|
488
|
+
send(data) {
|
|
489
|
+
if (!this.ws || this._state !== "connected") {
|
|
490
|
+
throw new Error("WebSocket not connected");
|
|
491
|
+
}
|
|
492
|
+
this.ws.send(data);
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Close the WebSocket connection.
|
|
496
|
+
*/
|
|
497
|
+
close(code = 1e3, reason = "Client disconnect") {
|
|
498
|
+
if (this.ws) {
|
|
499
|
+
this.ws.close(code, reason);
|
|
500
|
+
this.ws = null;
|
|
501
|
+
}
|
|
502
|
+
this._state = "disconnected";
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
// src/OnDisconnect.ts
|
|
507
|
+
var OnDisconnect = class {
|
|
508
|
+
constructor(db, path) {
|
|
509
|
+
this._db = db;
|
|
510
|
+
this._path = path;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Set a value when disconnected.
|
|
514
|
+
*/
|
|
515
|
+
async set(value) {
|
|
516
|
+
await this._db._sendOnDisconnect(this._path, OnDisconnectAction.SET, value);
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Update values when disconnected.
|
|
520
|
+
*/
|
|
521
|
+
async update(values) {
|
|
522
|
+
await this._db._sendOnDisconnect(this._path, OnDisconnectAction.UPDATE, values);
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Remove data when disconnected.
|
|
526
|
+
*/
|
|
527
|
+
async remove() {
|
|
528
|
+
await this._db._sendOnDisconnect(this._path, OnDisconnectAction.DELETE);
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Cancel any pending onDisconnect handlers at this location.
|
|
532
|
+
*/
|
|
533
|
+
async cancel() {
|
|
534
|
+
await this._db._sendOnDisconnect(this._path, OnDisconnectAction.CANCEL);
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Set a value with priority when disconnected.
|
|
538
|
+
*/
|
|
539
|
+
async setWithPriority(value, priority) {
|
|
540
|
+
await this._db._sendOnDisconnect(this._path, OnDisconnectAction.SET, value, priority);
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
// src/utils/path.ts
|
|
545
|
+
function normalizePath(path) {
|
|
546
|
+
if (!path || path === "/") return "/";
|
|
547
|
+
const segments = path.split("/").filter((segment) => segment.length > 0);
|
|
548
|
+
if (segments.length === 0) return "/";
|
|
549
|
+
return "/" + segments.join("/");
|
|
550
|
+
}
|
|
551
|
+
function joinPath(...segments) {
|
|
552
|
+
const allParts = [];
|
|
553
|
+
for (const segment of segments) {
|
|
554
|
+
if (!segment || segment === "/") continue;
|
|
555
|
+
const parts = segment.split("/").filter((p) => p.length > 0);
|
|
556
|
+
allParts.push(...parts);
|
|
557
|
+
}
|
|
558
|
+
if (allParts.length === 0) return "/";
|
|
559
|
+
return "/" + allParts.join("/");
|
|
560
|
+
}
|
|
561
|
+
function getParentPath(path) {
|
|
562
|
+
const normalized = normalizePath(path);
|
|
563
|
+
if (normalized === "/") return "/";
|
|
564
|
+
const lastSlash = normalized.lastIndexOf("/");
|
|
565
|
+
if (lastSlash <= 0) return "/";
|
|
566
|
+
return normalized.substring(0, lastSlash);
|
|
567
|
+
}
|
|
568
|
+
function getKey(path) {
|
|
569
|
+
const normalized = normalizePath(path);
|
|
570
|
+
if (normalized === "/") return null;
|
|
571
|
+
const lastSlash = normalized.lastIndexOf("/");
|
|
572
|
+
return normalized.substring(lastSlash + 1);
|
|
573
|
+
}
|
|
574
|
+
function getValueAtPath(obj, path) {
|
|
575
|
+
const normalized = normalizePath(path);
|
|
576
|
+
if (normalized === "/") return obj;
|
|
577
|
+
const segments = normalized.split("/").filter((s) => s.length > 0);
|
|
578
|
+
let current = obj;
|
|
579
|
+
for (const segment of segments) {
|
|
580
|
+
if (current === null || current === void 0) {
|
|
581
|
+
return void 0;
|
|
582
|
+
}
|
|
583
|
+
if (typeof current !== "object") {
|
|
584
|
+
return void 0;
|
|
585
|
+
}
|
|
586
|
+
current = current[segment];
|
|
587
|
+
}
|
|
588
|
+
return current;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// src/utils/pushid.ts
|
|
592
|
+
var PUSH_CHARS = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz";
|
|
593
|
+
var lastPushTime = 0;
|
|
594
|
+
var lastRandChars = [];
|
|
595
|
+
function generatePushId() {
|
|
596
|
+
let now = Date.now();
|
|
597
|
+
const duplicateTime = now === lastPushTime;
|
|
598
|
+
lastPushTime = now;
|
|
599
|
+
const timeChars = new Array(8);
|
|
600
|
+
for (let i = 7; i >= 0; i--) {
|
|
601
|
+
timeChars[i] = PUSH_CHARS.charAt(now % 64);
|
|
602
|
+
now = Math.floor(now / 64);
|
|
603
|
+
}
|
|
604
|
+
let id = timeChars.join("");
|
|
605
|
+
if (!duplicateTime) {
|
|
606
|
+
lastRandChars = [];
|
|
607
|
+
for (let i = 0; i < 12; i++) {
|
|
608
|
+
lastRandChars.push(Math.floor(Math.random() * 64));
|
|
609
|
+
}
|
|
610
|
+
} else {
|
|
611
|
+
let i = 11;
|
|
612
|
+
while (i >= 0 && lastRandChars[i] === 63) {
|
|
613
|
+
lastRandChars[i] = 0;
|
|
614
|
+
i--;
|
|
615
|
+
}
|
|
616
|
+
if (i >= 0) {
|
|
617
|
+
lastRandChars[i]++;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
for (let i = 0; i < 12; i++) {
|
|
621
|
+
id += PUSH_CHARS.charAt(lastRandChars[i]);
|
|
622
|
+
}
|
|
623
|
+
return id;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// src/DatabaseReference.ts
|
|
627
|
+
var DatabaseReference = class _DatabaseReference {
|
|
628
|
+
constructor(db, path, query = {}) {
|
|
629
|
+
this._db = db;
|
|
630
|
+
this._path = normalizePath(path);
|
|
631
|
+
this._query = query;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* The path of this reference.
|
|
635
|
+
*/
|
|
636
|
+
get path() {
|
|
637
|
+
return this._path;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* The last segment of the path (the "key"), or null for root.
|
|
641
|
+
*/
|
|
642
|
+
get key() {
|
|
643
|
+
return getKey(this._path);
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Get a reference to the parent node, or null if this is root.
|
|
647
|
+
*/
|
|
648
|
+
get parent() {
|
|
649
|
+
if (this._path === "/") return null;
|
|
650
|
+
const parentPath = getParentPath(this._path);
|
|
651
|
+
return new _DatabaseReference(this._db, parentPath);
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Get a reference to the root of the database.
|
|
655
|
+
*/
|
|
656
|
+
get root() {
|
|
657
|
+
return new _DatabaseReference(this._db, "");
|
|
658
|
+
}
|
|
659
|
+
// ============================================
|
|
660
|
+
// Navigation
|
|
661
|
+
// ============================================
|
|
662
|
+
/**
|
|
663
|
+
* Get a reference to a child path.
|
|
664
|
+
*/
|
|
665
|
+
child(path) {
|
|
666
|
+
const childPath = joinPath(this._path, path);
|
|
667
|
+
return new _DatabaseReference(this._db, childPath);
|
|
668
|
+
}
|
|
669
|
+
// ============================================
|
|
670
|
+
// Write Operations
|
|
671
|
+
// ============================================
|
|
672
|
+
/**
|
|
673
|
+
* Set the data at this location, overwriting any existing data.
|
|
674
|
+
*/
|
|
675
|
+
async set(value) {
|
|
676
|
+
await this._db._sendSet(this._path, value);
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Update specific children at this location without overwriting other children.
|
|
680
|
+
*/
|
|
681
|
+
async update(values) {
|
|
682
|
+
await this._db._sendUpdate(this._path, values);
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Remove the data at this location.
|
|
686
|
+
*/
|
|
687
|
+
async remove() {
|
|
688
|
+
await this._db._sendDelete(this._path);
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Generate a new child location with a unique key and optionally set its value.
|
|
692
|
+
*
|
|
693
|
+
* If value is provided, sets the value and returns a Promise that resolves
|
|
694
|
+
* to the new reference.
|
|
695
|
+
*
|
|
696
|
+
* If no value is provided, returns a reference immediately with a client-generated
|
|
697
|
+
* push key (you can then call set() on it).
|
|
698
|
+
*/
|
|
699
|
+
push(value) {
|
|
700
|
+
if (value === void 0) {
|
|
701
|
+
const key = generatePushId();
|
|
702
|
+
return this.child(key);
|
|
703
|
+
}
|
|
704
|
+
return this._db._sendPush(this._path, value).then((key) => {
|
|
705
|
+
return this.child(key);
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Set the data with a priority value for ordering.
|
|
710
|
+
*/
|
|
711
|
+
async setWithPriority(value, priority) {
|
|
712
|
+
await this._db._sendSet(this._path, value, priority);
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Set the priority of the data at this location.
|
|
716
|
+
*/
|
|
717
|
+
async setPriority(priority) {
|
|
718
|
+
console.warn("setPriority: fetching current value to preserve it");
|
|
719
|
+
const snapshot = await this.once();
|
|
720
|
+
await this.setWithPriority(snapshot.val(), priority);
|
|
721
|
+
}
|
|
722
|
+
// ============================================
|
|
723
|
+
// Read Operations
|
|
724
|
+
// ============================================
|
|
725
|
+
/**
|
|
726
|
+
* Read the data at this location once.
|
|
727
|
+
*/
|
|
728
|
+
async once(eventType = "value") {
|
|
729
|
+
if (eventType !== "value") {
|
|
730
|
+
throw new Error('once() only supports "value" event type');
|
|
731
|
+
}
|
|
732
|
+
return this._db._sendOnce(this._path, this._buildQueryParams());
|
|
733
|
+
}
|
|
734
|
+
// ============================================
|
|
735
|
+
// Subscriptions
|
|
736
|
+
// ============================================
|
|
737
|
+
/**
|
|
738
|
+
* Subscribe to events at this location.
|
|
739
|
+
* Returns an unsubscribe function.
|
|
740
|
+
*/
|
|
741
|
+
on(eventType, callback) {
|
|
742
|
+
return this._db._subscribe(this._path, eventType, callback);
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Unsubscribe from events.
|
|
746
|
+
* If eventType is specified, removes all listeners of that type.
|
|
747
|
+
* If no eventType, removes ALL listeners at this path.
|
|
748
|
+
*/
|
|
749
|
+
off(eventType) {
|
|
750
|
+
if (eventType) {
|
|
751
|
+
this._db._unsubscribeEventType(this._path, eventType);
|
|
752
|
+
} else {
|
|
753
|
+
this._db._unsubscribeAll(this._path);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
// ============================================
|
|
757
|
+
// OnDisconnect
|
|
758
|
+
// ============================================
|
|
759
|
+
/**
|
|
760
|
+
* Get an OnDisconnect handler for this location.
|
|
761
|
+
*/
|
|
762
|
+
onDisconnect() {
|
|
763
|
+
return new OnDisconnect(this._db, this._path);
|
|
764
|
+
}
|
|
765
|
+
// ============================================
|
|
766
|
+
// Query Modifiers
|
|
767
|
+
// ============================================
|
|
768
|
+
/**
|
|
769
|
+
* Order results by key.
|
|
770
|
+
*/
|
|
771
|
+
orderByKey() {
|
|
772
|
+
return new _DatabaseReference(this._db, this._path, {
|
|
773
|
+
...this._query,
|
|
774
|
+
orderBy: "key"
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Order results by priority.
|
|
779
|
+
*/
|
|
780
|
+
orderByPriority() {
|
|
781
|
+
return new _DatabaseReference(this._db, this._path, {
|
|
782
|
+
...this._query,
|
|
783
|
+
orderBy: "priority"
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Order results by a child key.
|
|
788
|
+
* NOTE: Phase 2 - not yet implemented on server.
|
|
789
|
+
*/
|
|
790
|
+
orderByChild(path) {
|
|
791
|
+
console.warn("orderByChild() is not yet implemented");
|
|
792
|
+
return new _DatabaseReference(this._db, this._path, {
|
|
793
|
+
...this._query,
|
|
794
|
+
orderBy: "child",
|
|
795
|
+
orderByChildPath: path
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Order results by value.
|
|
800
|
+
* NOTE: Phase 2 - not yet implemented on server.
|
|
801
|
+
*/
|
|
802
|
+
orderByValue() {
|
|
803
|
+
console.warn("orderByValue() is not yet implemented");
|
|
804
|
+
return new _DatabaseReference(this._db, this._path, {
|
|
805
|
+
...this._query,
|
|
806
|
+
orderBy: "value"
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Limit to the first N results.
|
|
811
|
+
*/
|
|
812
|
+
limitToFirst(limit) {
|
|
813
|
+
return new _DatabaseReference(this._db, this._path, {
|
|
814
|
+
...this._query,
|
|
815
|
+
limitToFirst: limit
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Limit to the last N results.
|
|
820
|
+
*/
|
|
821
|
+
limitToLast(limit) {
|
|
822
|
+
return new _DatabaseReference(this._db, this._path, {
|
|
823
|
+
...this._query,
|
|
824
|
+
limitToLast: limit
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Start at a specific value/key.
|
|
829
|
+
* NOTE: Phase 2 - not yet implemented on server.
|
|
830
|
+
*/
|
|
831
|
+
startAt(value, key) {
|
|
832
|
+
console.warn("startAt() is not yet implemented");
|
|
833
|
+
return new _DatabaseReference(this._db, this._path, {
|
|
834
|
+
...this._query,
|
|
835
|
+
startAt: { value, key }
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* End at a specific value/key.
|
|
840
|
+
* NOTE: Phase 2 - not yet implemented on server.
|
|
841
|
+
*/
|
|
842
|
+
endAt(value, key) {
|
|
843
|
+
console.warn("endAt() is not yet implemented");
|
|
844
|
+
return new _DatabaseReference(this._db, this._path, {
|
|
845
|
+
...this._query,
|
|
846
|
+
endAt: { value, key }
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Filter to items equal to a specific value.
|
|
851
|
+
* NOTE: Phase 2 - not yet implemented on server.
|
|
852
|
+
*/
|
|
853
|
+
equalTo(value, key) {
|
|
854
|
+
console.warn("equalTo() is not yet implemented");
|
|
855
|
+
return new _DatabaseReference(this._db, this._path, {
|
|
856
|
+
...this._query,
|
|
857
|
+
equalTo: { value, key }
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
// ============================================
|
|
861
|
+
// Internal Helpers
|
|
862
|
+
// ============================================
|
|
863
|
+
/**
|
|
864
|
+
* Build query parameters for wire protocol.
|
|
865
|
+
*/
|
|
866
|
+
_buildQueryParams() {
|
|
867
|
+
const params = {};
|
|
868
|
+
let hasParams = false;
|
|
869
|
+
if (this._query.orderBy === "key") {
|
|
870
|
+
params.ob = "k";
|
|
871
|
+
hasParams = true;
|
|
872
|
+
} else if (this._query.orderBy === "priority") {
|
|
873
|
+
params.ob = "p";
|
|
874
|
+
hasParams = true;
|
|
875
|
+
}
|
|
876
|
+
if (this._query.limitToFirst !== void 0) {
|
|
877
|
+
params.lf = this._query.limitToFirst;
|
|
878
|
+
hasParams = true;
|
|
879
|
+
}
|
|
880
|
+
if (this._query.limitToLast !== void 0) {
|
|
881
|
+
params.ll = this._query.limitToLast;
|
|
882
|
+
hasParams = true;
|
|
883
|
+
}
|
|
884
|
+
return hasParams ? params : void 0;
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Returns the absolute URL for this database location.
|
|
888
|
+
* Format: https://db.lark.sh/project/database/path/to/data
|
|
889
|
+
*/
|
|
890
|
+
toString() {
|
|
891
|
+
const baseUrl = this._db._getBaseUrl();
|
|
892
|
+
if (this._path === "/") {
|
|
893
|
+
return baseUrl;
|
|
894
|
+
}
|
|
895
|
+
return `${baseUrl}${this._path}`;
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
// src/DataSnapshot.ts
|
|
900
|
+
var DataSnapshot = class _DataSnapshot {
|
|
901
|
+
constructor(data, path, db, options = {}) {
|
|
902
|
+
this._data = data;
|
|
903
|
+
this._path = normalizePath(path);
|
|
904
|
+
this._db = db;
|
|
905
|
+
this._volatile = options.volatile ?? false;
|
|
906
|
+
this._priority = options.priority ?? null;
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Get a DatabaseReference for the location of this snapshot.
|
|
910
|
+
*/
|
|
911
|
+
get ref() {
|
|
912
|
+
return this._db.ref(this._path);
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Get the last segment of the path (the "key"), or null for root.
|
|
916
|
+
*/
|
|
917
|
+
get key() {
|
|
918
|
+
return getKey(this._path);
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Get the raw data value.
|
|
922
|
+
*/
|
|
923
|
+
val() {
|
|
924
|
+
return this._data;
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Check if data exists at this location (is not null/undefined).
|
|
928
|
+
*/
|
|
929
|
+
exists() {
|
|
930
|
+
return this._data !== null && this._data !== void 0;
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Get a child snapshot at the specified path.
|
|
934
|
+
*/
|
|
935
|
+
child(path) {
|
|
936
|
+
const childPath = joinPath(this._path, path);
|
|
937
|
+
const childData = getValueAtPath(this._data, path);
|
|
938
|
+
return new _DataSnapshot(childData, childPath, this._db, {
|
|
939
|
+
volatile: this._volatile
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Check if this snapshot has any children.
|
|
944
|
+
*/
|
|
945
|
+
hasChildren() {
|
|
946
|
+
if (typeof this._data !== "object" || this._data === null) {
|
|
947
|
+
return false;
|
|
948
|
+
}
|
|
949
|
+
return Object.keys(this._data).length > 0;
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Check if this snapshot has a specific child.
|
|
953
|
+
*/
|
|
954
|
+
hasChild(path) {
|
|
955
|
+
const childData = getValueAtPath(this._data, path);
|
|
956
|
+
return childData !== void 0 && childData !== null;
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Get the number of children.
|
|
960
|
+
*/
|
|
961
|
+
numChildren() {
|
|
962
|
+
if (typeof this._data !== "object" || this._data === null) {
|
|
963
|
+
return 0;
|
|
964
|
+
}
|
|
965
|
+
return Object.keys(this._data).length;
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Iterate over children. Return true from callback to stop iteration.
|
|
969
|
+
*/
|
|
970
|
+
forEach(callback) {
|
|
971
|
+
if (typeof this._data !== "object" || this._data === null) {
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
const keys = Object.keys(this._data);
|
|
975
|
+
for (const key of keys) {
|
|
976
|
+
const childSnap = this.child(key);
|
|
977
|
+
if (callback(childSnap) === true) {
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Get the priority of the data at this location.
|
|
984
|
+
*/
|
|
985
|
+
getPriority() {
|
|
986
|
+
return this._priority;
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Check if this snapshot was from a volatile (high-frequency) update.
|
|
990
|
+
* This is a Lark extension not present in Firebase.
|
|
991
|
+
*/
|
|
992
|
+
isVolatile() {
|
|
993
|
+
return this._volatile;
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Export the snapshot data as JSON (alias for val()).
|
|
997
|
+
*/
|
|
998
|
+
toJSON() {
|
|
999
|
+
return this._data;
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
// src/utils/jwt.ts
|
|
1004
|
+
function decodeJwtPayload(token) {
|
|
1005
|
+
const parts = token.split(".");
|
|
1006
|
+
if (parts.length !== 3) {
|
|
1007
|
+
throw new Error("Invalid JWT format");
|
|
1008
|
+
}
|
|
1009
|
+
const payload = parts[1];
|
|
1010
|
+
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
1011
|
+
const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
|
|
1012
|
+
let decoded;
|
|
1013
|
+
if (typeof atob === "function") {
|
|
1014
|
+
decoded = atob(padded);
|
|
1015
|
+
} else {
|
|
1016
|
+
decoded = Buffer.from(padded, "base64").toString("utf-8");
|
|
1017
|
+
}
|
|
1018
|
+
return JSON.parse(decoded);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// src/LarkDatabase.ts
|
|
1022
|
+
var LarkDatabase = class {
|
|
1023
|
+
constructor() {
|
|
1024
|
+
this._state = "disconnected";
|
|
1025
|
+
this._auth = null;
|
|
1026
|
+
this._databaseId = null;
|
|
1027
|
+
this._coordinatorUrl = null;
|
|
1028
|
+
this.ws = null;
|
|
1029
|
+
// Event callbacks
|
|
1030
|
+
this.connectCallbacks = /* @__PURE__ */ new Set();
|
|
1031
|
+
this.disconnectCallbacks = /* @__PURE__ */ new Set();
|
|
1032
|
+
this.errorCallbacks = /* @__PURE__ */ new Set();
|
|
1033
|
+
this.messageQueue = new MessageQueue();
|
|
1034
|
+
this.subscriptionManager = new SubscriptionManager();
|
|
1035
|
+
}
|
|
1036
|
+
// ============================================
|
|
1037
|
+
// Connection State
|
|
1038
|
+
// ============================================
|
|
1039
|
+
/**
|
|
1040
|
+
* Whether the database is currently connected.
|
|
1041
|
+
*/
|
|
1042
|
+
get connected() {
|
|
1043
|
+
return this._state === "connected";
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Current auth info, or null if not connected.
|
|
1047
|
+
*/
|
|
1048
|
+
get auth() {
|
|
1049
|
+
return this._auth;
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* @internal Get the base URL for reference toString().
|
|
1053
|
+
*/
|
|
1054
|
+
_getBaseUrl() {
|
|
1055
|
+
if (this._coordinatorUrl && this._databaseId) {
|
|
1056
|
+
return `${this._coordinatorUrl}/${this._databaseId}`;
|
|
1057
|
+
}
|
|
1058
|
+
return "lark://";
|
|
1059
|
+
}
|
|
1060
|
+
// ============================================
|
|
1061
|
+
// Connection Management
|
|
1062
|
+
// ============================================
|
|
1063
|
+
/**
|
|
1064
|
+
* Connect to a database.
|
|
1065
|
+
*
|
|
1066
|
+
* @param databaseId - Database ID in format "project/database"
|
|
1067
|
+
* @param options - Connection options (token, anonymous, coordinator URL)
|
|
1068
|
+
*/
|
|
1069
|
+
async connect(databaseId, options = {}) {
|
|
1070
|
+
if (this._state !== "disconnected") {
|
|
1071
|
+
throw new Error("Already connected or connecting");
|
|
1072
|
+
}
|
|
1073
|
+
this._state = "connecting";
|
|
1074
|
+
this._databaseId = databaseId;
|
|
1075
|
+
this._coordinatorUrl = options.coordinator || DEFAULT_COORDINATOR_URL;
|
|
1076
|
+
try {
|
|
1077
|
+
const coordinatorUrl = this._coordinatorUrl;
|
|
1078
|
+
const coordinator = new Coordinator(coordinatorUrl);
|
|
1079
|
+
const connectResponse = await coordinator.connect(databaseId, {
|
|
1080
|
+
token: options.token,
|
|
1081
|
+
anonymous: options.anonymous
|
|
1082
|
+
});
|
|
1083
|
+
const wsUrl = connectResponse.ws_url;
|
|
1084
|
+
this.ws = new WebSocketClient({
|
|
1085
|
+
onMessage: this.handleMessage.bind(this),
|
|
1086
|
+
onOpen: () => {
|
|
1087
|
+
},
|
|
1088
|
+
// Handled in connect flow
|
|
1089
|
+
onClose: this.handleClose.bind(this),
|
|
1090
|
+
onError: this.handleError.bind(this)
|
|
1091
|
+
});
|
|
1092
|
+
await this.ws.connect(wsUrl);
|
|
1093
|
+
const requestId = this.messageQueue.nextRequestId();
|
|
1094
|
+
const joinMessage = {
|
|
1095
|
+
o: "j",
|
|
1096
|
+
t: connectResponse.token,
|
|
1097
|
+
r: requestId
|
|
1098
|
+
};
|
|
1099
|
+
this.send(joinMessage);
|
|
1100
|
+
await this.messageQueue.registerRequest(requestId);
|
|
1101
|
+
const jwtPayload = decodeJwtPayload(connectResponse.token);
|
|
1102
|
+
this._auth = {
|
|
1103
|
+
uid: jwtPayload.sub,
|
|
1104
|
+
provider: jwtPayload.provider,
|
|
1105
|
+
token: jwtPayload.claims || {}
|
|
1106
|
+
};
|
|
1107
|
+
this._state = "connected";
|
|
1108
|
+
this.subscriptionManager.initialize({
|
|
1109
|
+
sendSubscribe: this.sendSubscribeMessage.bind(this),
|
|
1110
|
+
sendUnsubscribe: this.sendUnsubscribeMessage.bind(this),
|
|
1111
|
+
createSnapshot: this.createSnapshot.bind(this)
|
|
1112
|
+
});
|
|
1113
|
+
this.connectCallbacks.forEach((cb) => cb());
|
|
1114
|
+
} catch (error) {
|
|
1115
|
+
this._state = "disconnected";
|
|
1116
|
+
this._auth = null;
|
|
1117
|
+
this._databaseId = null;
|
|
1118
|
+
this.ws?.close();
|
|
1119
|
+
this.ws = null;
|
|
1120
|
+
throw error;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Disconnect from the database.
|
|
1125
|
+
* This triggers onDisconnect hooks on the server.
|
|
1126
|
+
*/
|
|
1127
|
+
async disconnect() {
|
|
1128
|
+
if (this._state === "disconnected") {
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
if (this._state === "connected" && this.ws) {
|
|
1132
|
+
try {
|
|
1133
|
+
const requestId = this.messageQueue.nextRequestId();
|
|
1134
|
+
this.send({ o: "l", r: requestId });
|
|
1135
|
+
await Promise.race([
|
|
1136
|
+
this.messageQueue.registerRequest(requestId),
|
|
1137
|
+
new Promise((resolve) => setTimeout(resolve, 1e3))
|
|
1138
|
+
]);
|
|
1139
|
+
} catch {
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
this.cleanup();
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Clean up connection state.
|
|
1146
|
+
*/
|
|
1147
|
+
cleanup() {
|
|
1148
|
+
this.ws?.close();
|
|
1149
|
+
this.ws = null;
|
|
1150
|
+
this._state = "disconnected";
|
|
1151
|
+
this._auth = null;
|
|
1152
|
+
this._databaseId = null;
|
|
1153
|
+
this._coordinatorUrl = null;
|
|
1154
|
+
this.subscriptionManager.clear();
|
|
1155
|
+
this.messageQueue.rejectAll(new Error("Connection closed"));
|
|
1156
|
+
}
|
|
1157
|
+
// ============================================
|
|
1158
|
+
// Reference Access
|
|
1159
|
+
// ============================================
|
|
1160
|
+
/**
|
|
1161
|
+
* Get a reference to a path in the database.
|
|
1162
|
+
*/
|
|
1163
|
+
ref(path = "") {
|
|
1164
|
+
return new DatabaseReference(this, path);
|
|
1165
|
+
}
|
|
1166
|
+
// ============================================
|
|
1167
|
+
// Connection Events
|
|
1168
|
+
// ============================================
|
|
1169
|
+
/**
|
|
1170
|
+
* Register a callback for when connection is established.
|
|
1171
|
+
* Returns an unsubscribe function.
|
|
1172
|
+
*/
|
|
1173
|
+
onConnect(callback) {
|
|
1174
|
+
this.connectCallbacks.add(callback);
|
|
1175
|
+
return () => this.connectCallbacks.delete(callback);
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Register a callback for when connection is lost.
|
|
1179
|
+
* Returns an unsubscribe function.
|
|
1180
|
+
*/
|
|
1181
|
+
onDisconnect(callback) {
|
|
1182
|
+
this.disconnectCallbacks.add(callback);
|
|
1183
|
+
return () => this.disconnectCallbacks.delete(callback);
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Register a callback for connection errors.
|
|
1187
|
+
* Returns an unsubscribe function.
|
|
1188
|
+
*/
|
|
1189
|
+
onError(callback) {
|
|
1190
|
+
this.errorCallbacks.add(callback);
|
|
1191
|
+
return () => this.errorCallbacks.delete(callback);
|
|
1192
|
+
}
|
|
1193
|
+
// ============================================
|
|
1194
|
+
// Internal: Message Handling
|
|
1195
|
+
// ============================================
|
|
1196
|
+
handleMessage(data) {
|
|
1197
|
+
let message;
|
|
1198
|
+
try {
|
|
1199
|
+
message = JSON.parse(data);
|
|
1200
|
+
} catch {
|
|
1201
|
+
console.error("Failed to parse message:", data);
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
if (this.messageQueue.handleMessage(message)) {
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
if (isEventMessage(message)) {
|
|
1208
|
+
this.subscriptionManager.handleEvent(message);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
handleClose(code, reason) {
|
|
1212
|
+
const wasConnected = this._state === "connected";
|
|
1213
|
+
this.cleanup();
|
|
1214
|
+
if (wasConnected) {
|
|
1215
|
+
this.disconnectCallbacks.forEach((cb) => cb());
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
handleError(event) {
|
|
1219
|
+
const error = new Error("WebSocket error");
|
|
1220
|
+
this.errorCallbacks.forEach((cb) => cb(error));
|
|
1221
|
+
}
|
|
1222
|
+
// ============================================
|
|
1223
|
+
// Internal: Sending Messages
|
|
1224
|
+
// ============================================
|
|
1225
|
+
send(message) {
|
|
1226
|
+
if (!this.ws || !this.ws.connected) {
|
|
1227
|
+
throw new LarkError("not_connected", "Not connected to database");
|
|
1228
|
+
}
|
|
1229
|
+
this.ws.send(JSON.stringify(message));
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* @internal Send a set operation.
|
|
1233
|
+
*/
|
|
1234
|
+
async _sendSet(path, value, priority) {
|
|
1235
|
+
const requestId = this.messageQueue.nextRequestId();
|
|
1236
|
+
const message = {
|
|
1237
|
+
o: "s",
|
|
1238
|
+
p: normalizePath(path) || "/",
|
|
1239
|
+
v: value,
|
|
1240
|
+
r: requestId
|
|
1241
|
+
};
|
|
1242
|
+
if (priority !== void 0) {
|
|
1243
|
+
message.y = priority;
|
|
1244
|
+
}
|
|
1245
|
+
this.send(message);
|
|
1246
|
+
await this.messageQueue.registerRequest(requestId);
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* @internal Send an update operation.
|
|
1250
|
+
*/
|
|
1251
|
+
async _sendUpdate(path, values) {
|
|
1252
|
+
const requestId = this.messageQueue.nextRequestId();
|
|
1253
|
+
const message = {
|
|
1254
|
+
o: "u",
|
|
1255
|
+
p: normalizePath(path) || "/",
|
|
1256
|
+
v: values,
|
|
1257
|
+
r: requestId
|
|
1258
|
+
};
|
|
1259
|
+
this.send(message);
|
|
1260
|
+
await this.messageQueue.registerRequest(requestId);
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* @internal Send a delete operation.
|
|
1264
|
+
*/
|
|
1265
|
+
async _sendDelete(path) {
|
|
1266
|
+
const requestId = this.messageQueue.nextRequestId();
|
|
1267
|
+
const message = {
|
|
1268
|
+
o: "d",
|
|
1269
|
+
p: normalizePath(path) || "/",
|
|
1270
|
+
r: requestId
|
|
1271
|
+
};
|
|
1272
|
+
this.send(message);
|
|
1273
|
+
await this.messageQueue.registerRequest(requestId);
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* @internal Send a push operation. Returns the generated key.
|
|
1277
|
+
*/
|
|
1278
|
+
async _sendPush(path, value) {
|
|
1279
|
+
const requestId = this.messageQueue.nextRequestId();
|
|
1280
|
+
const message = {
|
|
1281
|
+
o: "p",
|
|
1282
|
+
p: normalizePath(path) || "/",
|
|
1283
|
+
v: value,
|
|
1284
|
+
r: requestId
|
|
1285
|
+
};
|
|
1286
|
+
this.send(message);
|
|
1287
|
+
const key = await this.messageQueue.registerRequest(requestId);
|
|
1288
|
+
return key;
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* @internal Send a once (read) operation.
|
|
1292
|
+
*/
|
|
1293
|
+
async _sendOnce(path, query) {
|
|
1294
|
+
const requestId = this.messageQueue.nextRequestId();
|
|
1295
|
+
const message = {
|
|
1296
|
+
o: "o",
|
|
1297
|
+
p: normalizePath(path) || "/",
|
|
1298
|
+
r: requestId
|
|
1299
|
+
};
|
|
1300
|
+
if (query) {
|
|
1301
|
+
message.q = query;
|
|
1302
|
+
}
|
|
1303
|
+
this.send(message);
|
|
1304
|
+
const value = await this.messageQueue.registerRequest(requestId);
|
|
1305
|
+
return new DataSnapshot(value, path, this);
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* @internal Send an onDisconnect operation.
|
|
1309
|
+
*/
|
|
1310
|
+
async _sendOnDisconnect(path, action, value, priority) {
|
|
1311
|
+
const requestId = this.messageQueue.nextRequestId();
|
|
1312
|
+
const message = {
|
|
1313
|
+
o: "od",
|
|
1314
|
+
p: normalizePath(path) || "/",
|
|
1315
|
+
a: action,
|
|
1316
|
+
r: requestId
|
|
1317
|
+
};
|
|
1318
|
+
if (value !== void 0) {
|
|
1319
|
+
message.v = value;
|
|
1320
|
+
}
|
|
1321
|
+
if (priority !== void 0) {
|
|
1322
|
+
message.y = priority;
|
|
1323
|
+
}
|
|
1324
|
+
this.send(message);
|
|
1325
|
+
await this.messageQueue.registerRequest(requestId);
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* @internal Send a subscribe message to server.
|
|
1329
|
+
*/
|
|
1330
|
+
async sendSubscribeMessage(path, eventTypes) {
|
|
1331
|
+
const requestId = this.messageQueue.nextRequestId();
|
|
1332
|
+
const message = {
|
|
1333
|
+
o: "sb",
|
|
1334
|
+
p: normalizePath(path) || "/",
|
|
1335
|
+
e: eventTypes,
|
|
1336
|
+
r: requestId
|
|
1337
|
+
};
|
|
1338
|
+
this.send(message);
|
|
1339
|
+
await this.messageQueue.registerRequest(requestId);
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* @internal Send an unsubscribe message to server.
|
|
1343
|
+
*/
|
|
1344
|
+
async sendUnsubscribeMessage(path) {
|
|
1345
|
+
const requestId = this.messageQueue.nextRequestId();
|
|
1346
|
+
const message = {
|
|
1347
|
+
o: "us",
|
|
1348
|
+
p: normalizePath(path) || "/",
|
|
1349
|
+
r: requestId
|
|
1350
|
+
};
|
|
1351
|
+
this.send(message);
|
|
1352
|
+
await this.messageQueue.registerRequest(requestId);
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* @internal Create a DataSnapshot from event data.
|
|
1356
|
+
*/
|
|
1357
|
+
createSnapshot(path, value, volatile) {
|
|
1358
|
+
return new DataSnapshot(value, path, this, { volatile });
|
|
1359
|
+
}
|
|
1360
|
+
// ============================================
|
|
1361
|
+
// Internal: Subscription Management
|
|
1362
|
+
// ============================================
|
|
1363
|
+
/**
|
|
1364
|
+
* @internal Subscribe to events at a path.
|
|
1365
|
+
*/
|
|
1366
|
+
_subscribe(path, eventType, callback) {
|
|
1367
|
+
return this.subscriptionManager.subscribe(path, eventType, callback);
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* @internal Unsubscribe from a specific event type at a path.
|
|
1371
|
+
*/
|
|
1372
|
+
_unsubscribeEventType(path, eventType) {
|
|
1373
|
+
this.subscriptionManager.unsubscribeEventType(path, eventType);
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* @internal Unsubscribe from all events at a path.
|
|
1377
|
+
*/
|
|
1378
|
+
_unsubscribeAll(path) {
|
|
1379
|
+
this.subscriptionManager.unsubscribeAll(path);
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1383
|
+
0 && (module.exports = {
|
|
1384
|
+
DataSnapshot,
|
|
1385
|
+
DatabaseReference,
|
|
1386
|
+
LarkDatabase,
|
|
1387
|
+
LarkError,
|
|
1388
|
+
OnDisconnect,
|
|
1389
|
+
generatePushId
|
|
1390
|
+
});
|
|
1391
|
+
//# sourceMappingURL=index.js.map
|