@lunora/client 1.0.0-alpha.20 → 1.0.0-alpha.22
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/auth/index.d.mts +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/index.d.mts +212 -4
- package/dist/index.d.ts +212 -4
- package/dist/index.mjs +12 -7
- package/dist/packem_shared/ClientServiceWorker-C3PAFwy0.mjs +100 -0
- package/dist/packem_shared/{LunoraClient-hrmLXEHd.mjs → LunoraClient-Clb118SU.mjs} +339 -33
- package/dist/packem_shared/{OfflineQueue-GGYJRmhF.mjs → OfflineQueue-B4HUF7rt.mjs} +1 -1
- package/dist/packem_shared/SubscriptionRegistry-D4jfIzZu.mjs +31 -0
- package/dist/packem_shared/TabCoordinator-BwRR8H06.mjs +222 -0
- package/dist/packem_shared/{applyDelta-4jFGTPA3.mjs → applyDelta-CRKZ1PBt.mjs} +21 -1
- package/dist/packem_shared/createClientQuery-CQ51bWAE.mjs +71 -0
- package/dist/packem_shared/{createInMemoryPersistence-Ds7z8n8d.mjs → createInMemoryPersistence-DZ2VHWgm.mjs} +7 -47
- package/dist/packem_shared/{createInMemoryQueryCache-iWtKPrid.mjs → createInMemoryQueryCache-DiaGZkA2.mjs} +7 -47
- package/dist/packem_shared/createLocalStore-BDbbkoXw.mjs +2 -0
- package/dist/packem_shared/createReply-lI4tVS2w.mjs +36 -0
- package/dist/packem_shared/{createServerClient-CKKZrXLc.mjs → createServerClient-Dxemst5C.mjs} +1 -1
- package/dist/packem_shared/createSnapshotPrecondition-CBwnVz6r.mjs +18 -0
- package/dist/packem_shared/idb-utility-DrSVX43Q.mjs +48 -0
- package/dist/packem_shared/{local-store-BNgN3Dw3.mjs → local-store-DtcIW4c0.mjs} +7 -12
- package/dist/packem_shared/{lunora-client.d-DVdVtJV8.d.mts → lunora-client.d-pw-9sLl0.d.mts} +212 -2
- package/dist/packem_shared/{lunora-client.d-DVdVtJV8.d.ts → lunora-client.d-pw-9sLl0.d.ts} +212 -2
- package/dist/packem_shared/{offline-queue-B9vfdSqp.mjs → offline-queue-CF4_Co5k.mjs} +29 -0
- package/dist/packem_shared/{preload.d-BXFvxpiv.d.mts → preload.d-6ME5ubgq.d.mts} +1 -1
- package/dist/packem_shared/{preload.d-Bb69VRgE.d.ts → preload.d-BkQr-3Vh.d.ts} +1 -1
- package/dist/packem_shared/{subscription-DoyO04-2.mjs → stable-key-wv6eP48B.mjs} +1 -26
- package/dist/query/index.d.mts +2 -2
- package/dist/query/index.d.ts +2 -2
- package/dist/ssr/index.d.mts +3 -3
- package/dist/ssr/index.d.ts +3 -3
- package/dist/ssr/index.mjs +1 -1
- package/package.json +2 -2
- package/dist/packem_shared/SubscriptionRegistry-Cxr70og-.mjs +0 -1
- package/dist/packem_shared/createLocalStore-IOur0jHF.mjs +0 -1
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
let nextTabId = 0;
|
|
2
|
+
const DEFAULT_CHANNEL = "lunora-bridge";
|
|
3
|
+
const DEFAULT_HEARTBEAT_MS = 1e3;
|
|
4
|
+
const DEFAULT_LEADER_TIMEOUT_MS = 3e3;
|
|
5
|
+
class TabCoordinator {
|
|
6
|
+
bc;
|
|
7
|
+
tabId;
|
|
8
|
+
heartbeatInterval;
|
|
9
|
+
leaderTimeout;
|
|
10
|
+
/** The tab id of the current known leader, or `undefined` if no leader. */
|
|
11
|
+
knownLeader = void 0;
|
|
12
|
+
/** `true` when this tab believes it is the leader. */
|
|
13
|
+
leader = false;
|
|
14
|
+
/** `true` once `start()` has been called. */
|
|
15
|
+
running = false;
|
|
16
|
+
/** Timestamp of the most recent leader heartbeat. */
|
|
17
|
+
lastHeartbeat = 0;
|
|
18
|
+
heartbeatTimer = void 0;
|
|
19
|
+
leaderCheckTimer = void 0;
|
|
20
|
+
/** Callbacks set via constructor options. */
|
|
21
|
+
onBecomeLeader;
|
|
22
|
+
onStopBeingLeader;
|
|
23
|
+
onSubscriptionData;
|
|
24
|
+
onSubscriptionError;
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
nextTabId += 1;
|
|
27
|
+
this.tabId = `tab_${String(nextTabId)}_${crypto.randomUUID()}`;
|
|
28
|
+
this.heartbeatInterval = options.heartbeatInterval ?? DEFAULT_HEARTBEAT_MS;
|
|
29
|
+
this.leaderTimeout = options.leaderTimeout ?? DEFAULT_LEADER_TIMEOUT_MS;
|
|
30
|
+
this.onBecomeLeader = options.onBecomeLeader;
|
|
31
|
+
this.onStopBeingLeader = options.onStopBeingLeader;
|
|
32
|
+
this.onSubscriptionData = options.onSubscriptionData;
|
|
33
|
+
this.onSubscriptionError = options.onSubscriptionError;
|
|
34
|
+
if (typeof BroadcastChannel === "undefined") {
|
|
35
|
+
this.bc = void 0;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
this.bc = new BroadcastChannel(options.channelName ?? DEFAULT_CHANNEL);
|
|
39
|
+
this.bc.addEventListener("message", (event) => {
|
|
40
|
+
this.handleMessage(event.data);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
// -----------------------------------------------------------------------
|
|
44
|
+
// Public API
|
|
45
|
+
// -----------------------------------------------------------------------
|
|
46
|
+
/**
|
|
47
|
+
* Start the coordinator: attempt to claim leadership and begin the
|
|
48
|
+
* heartbeat/leader-check cycle. Safe to call multiple times.
|
|
49
|
+
*/
|
|
50
|
+
start() {
|
|
51
|
+
if (this.running) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
this.running = true;
|
|
55
|
+
if (this.bc === void 0) {
|
|
56
|
+
this.becomeLeader();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
this.broadcast({ type: "claim-leadership", tabId: this.tabId, ts: Date.now() });
|
|
60
|
+
setTimeout(() => {
|
|
61
|
+
if (!this.running) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (this.knownLeader === void 0) {
|
|
65
|
+
this.becomeLeader();
|
|
66
|
+
}
|
|
67
|
+
}, this.leaderTimeout);
|
|
68
|
+
this.leaderCheckTimer = setInterval(() => {
|
|
69
|
+
this.checkLeaderHealth();
|
|
70
|
+
}, this.leaderTimeout);
|
|
71
|
+
this.heartbeatTimer = setInterval(() => {
|
|
72
|
+
this.sendHeartbeat();
|
|
73
|
+
}, this.heartbeatInterval);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Stop the coordinator: yield leadership (if held), close the channel, and
|
|
77
|
+
* clear all timers. Safe to call multiple times.
|
|
78
|
+
*/
|
|
79
|
+
stop() {
|
|
80
|
+
this.running = false;
|
|
81
|
+
if (this.leader) {
|
|
82
|
+
this.broadcast({ type: "yield-leadership", tabId: this.tabId });
|
|
83
|
+
this.leader = false;
|
|
84
|
+
this.knownLeader = void 0;
|
|
85
|
+
this.onStopBeingLeader?.();
|
|
86
|
+
}
|
|
87
|
+
if (this.heartbeatTimer !== void 0) {
|
|
88
|
+
clearInterval(this.heartbeatTimer);
|
|
89
|
+
this.heartbeatTimer = void 0;
|
|
90
|
+
}
|
|
91
|
+
if (this.leaderCheckTimer !== void 0) {
|
|
92
|
+
clearInterval(this.leaderCheckTimer);
|
|
93
|
+
this.leaderCheckTimer = void 0;
|
|
94
|
+
}
|
|
95
|
+
this.bc?.close();
|
|
96
|
+
}
|
|
97
|
+
/** `true` when this tab is the current WebSocket leader. */
|
|
98
|
+
isLeader() {
|
|
99
|
+
return this.leader;
|
|
100
|
+
}
|
|
101
|
+
/** The tab id of the current leader, or `undefined` if unknown / no leader. */
|
|
102
|
+
get leaderTabId() {
|
|
103
|
+
return this.knownLeader;
|
|
104
|
+
}
|
|
105
|
+
/** The id of this tab. */
|
|
106
|
+
get id() {
|
|
107
|
+
return this.tabId;
|
|
108
|
+
}
|
|
109
|
+
/** `true` when the coordinator has been started and is not yet stopped. */
|
|
110
|
+
get isRunning() {
|
|
111
|
+
return this.running;
|
|
112
|
+
}
|
|
113
|
+
// -----------------------------------------------------------------------
|
|
114
|
+
// Broadcasting
|
|
115
|
+
// -----------------------------------------------------------------------
|
|
116
|
+
/**
|
|
117
|
+
* Broadcast subscription data to all follower tabs. Only the leader should
|
|
118
|
+
* call this.
|
|
119
|
+
*/
|
|
120
|
+
broadcastSubscriptionData(key, data) {
|
|
121
|
+
if (!this.leader) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
this.broadcast({ type: "subscription-data", tabId: this.tabId, key, data });
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Broadcast a subscription error to all follower tabs. Only the leader
|
|
128
|
+
* should call this.
|
|
129
|
+
*/
|
|
130
|
+
broadcastSubscriptionError(key, error) {
|
|
131
|
+
if (!this.leader) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
this.broadcast({ type: "subscription-error", tabId: this.tabId, key, error });
|
|
135
|
+
}
|
|
136
|
+
// -----------------------------------------------------------------------
|
|
137
|
+
// Internal
|
|
138
|
+
// -----------------------------------------------------------------------
|
|
139
|
+
broadcast(message) {
|
|
140
|
+
this.bc?.postMessage(message);
|
|
141
|
+
}
|
|
142
|
+
handleMessage(message) {
|
|
143
|
+
switch (message.type) {
|
|
144
|
+
case "claim-leadership": {
|
|
145
|
+
if (message.tabId === this.tabId) {
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
if (this.leader) {
|
|
149
|
+
this.broadcast({ type: "heartbeat", tabId: this.tabId, ts: Date.now() });
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
if (this.knownLeader === void 0 && message.tabId < this.tabId) {
|
|
153
|
+
this.knownLeader = message.tabId;
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
case "heartbeat": {
|
|
158
|
+
this.lastHeartbeat = message.ts;
|
|
159
|
+
this.knownLeader = message.tabId;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case "subscription-data": {
|
|
163
|
+
if (this.leader || message.tabId === this.tabId) {
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
this.onSubscriptionData?.(message.key, message.data);
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
case "subscription-error": {
|
|
170
|
+
if (this.leader || message.tabId === this.tabId) {
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
this.onSubscriptionError?.(message.key, message.error);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
case "yield-leadership": {
|
|
177
|
+
if (this.knownLeader === message.tabId) {
|
|
178
|
+
this.knownLeader = void 0;
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
becomeLeader() {
|
|
185
|
+
if (this.leader) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
this.leader = true;
|
|
189
|
+
this.knownLeader = this.tabId;
|
|
190
|
+
this.lastHeartbeat = Date.now();
|
|
191
|
+
this.onBecomeLeader?.();
|
|
192
|
+
this.broadcast({ type: "heartbeat", tabId: this.tabId, ts: Date.now() });
|
|
193
|
+
}
|
|
194
|
+
sendHeartbeat() {
|
|
195
|
+
if (!this.leader) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
this.broadcast({ type: "heartbeat", tabId: this.tabId, ts: Date.now() });
|
|
199
|
+
}
|
|
200
|
+
checkLeaderHealth() {
|
|
201
|
+
if (!this.running) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (this.leader) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (this.knownLeader !== void 0) {
|
|
208
|
+
const elapsed = Date.now() - this.lastHeartbeat;
|
|
209
|
+
if (elapsed > this.leaderTimeout) {
|
|
210
|
+
this.knownLeader = void 0;
|
|
211
|
+
this.broadcast({ type: "claim-leadership", tabId: this.tabId, ts: Date.now() });
|
|
212
|
+
setTimeout(() => {
|
|
213
|
+
if (this.running && this.knownLeader === void 0) {
|
|
214
|
+
this.becomeLeader();
|
|
215
|
+
}
|
|
216
|
+
}, this.leaderTimeout);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export { TabCoordinator };
|
|
@@ -7,14 +7,34 @@ const rowId = (row) => {
|
|
|
7
7
|
const id = row[ID_FIELD];
|
|
8
8
|
return typeof id === "string" ? id : void 0;
|
|
9
9
|
};
|
|
10
|
+
const isDescending = (list) => {
|
|
11
|
+
let previous;
|
|
12
|
+
for (const existingRow of list) {
|
|
13
|
+
const existing = existingRow[CREATION_FIELD];
|
|
14
|
+
if (typeof existing !== "number") {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (previous !== void 0) {
|
|
18
|
+
if (existing < previous) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if (existing > previous) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
previous = existing;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
};
|
|
10
29
|
const insertionIndex = (list, row) => {
|
|
11
30
|
const creation = row[CREATION_FIELD];
|
|
12
31
|
if (typeof creation !== "number") {
|
|
13
32
|
return list.length;
|
|
14
33
|
}
|
|
34
|
+
const descending = isDescending(list);
|
|
15
35
|
for (const [index, existingRow] of list.entries()) {
|
|
16
36
|
const existing = existingRow[CREATION_FIELD];
|
|
17
|
-
if (typeof existing === "number" && existing > creation) {
|
|
37
|
+
if (typeof existing === "number" && (descending ? existing < creation : existing > creation)) {
|
|
18
38
|
return index;
|
|
19
39
|
}
|
|
20
40
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
class ClientQueryStore {
|
|
2
|
+
/** Current values, keyed by the ref's stable key. Absent = never set. */
|
|
3
|
+
values = /* @__PURE__ */ new Map();
|
|
4
|
+
/** Subscribers keyed by ref key — notified on every set. */
|
|
5
|
+
subscribers = /* @__PURE__ */ new Map();
|
|
6
|
+
/**
|
|
7
|
+
* Return the current value for `ref`, or `ref.defaultValue` if none has
|
|
8
|
+
* been set explicitly. Returns `ref.defaultValue` when the slot has been
|
|
9
|
+
* set to `undefined` (which is distinct from "never set").
|
|
10
|
+
*/
|
|
11
|
+
get(ref) {
|
|
12
|
+
if (this.values.has(ref.key)) {
|
|
13
|
+
return this.values.get(ref.key);
|
|
14
|
+
}
|
|
15
|
+
return ref.defaultValue;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Set a new value for `ref` and notify every subscriber. Pass `undefined`
|
|
19
|
+
* to reset the slot to `ref.defaultValue`.
|
|
20
|
+
*/
|
|
21
|
+
set(ref, value) {
|
|
22
|
+
this.values.set(ref.key, value);
|
|
23
|
+
this.notify(ref.key);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Delete the stored value for `ref`, resetting to `ref.defaultValue` and
|
|
27
|
+
* notifying subscribers.
|
|
28
|
+
*/
|
|
29
|
+
reset(ref) {
|
|
30
|
+
this.values.delete(ref.key);
|
|
31
|
+
this.notify(ref.key);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Subscribe to changes for `ref`. The callback is NOT invoked on
|
|
35
|
+
* registration — callers should read the current value via
|
|
36
|
+
* {@link get} first. Returns an unsubscribe function.
|
|
37
|
+
*/
|
|
38
|
+
subscribe(ref, callback) {
|
|
39
|
+
let subs = this.subscribers.get(ref.key);
|
|
40
|
+
if (!subs) {
|
|
41
|
+
subs = /* @__PURE__ */ new Set();
|
|
42
|
+
this.subscribers.set(ref.key, subs);
|
|
43
|
+
}
|
|
44
|
+
subs.add(callback);
|
|
45
|
+
return () => {
|
|
46
|
+
subs.delete(callback);
|
|
47
|
+
if (subs.size === 0) {
|
|
48
|
+
this.subscribers.delete(ref.key);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Notify every subscriber of a value change for the given key. */
|
|
53
|
+
notify(key) {
|
|
54
|
+
const subs = this.subscribers.get(key);
|
|
55
|
+
if (!subs) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const value = this.values.get(key);
|
|
59
|
+
for (const callback of subs) {
|
|
60
|
+
try {
|
|
61
|
+
callback(value);
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const createClientQuery = (key, defaultValue) => {
|
|
68
|
+
return { defaultValue, key };
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export { ClientQueryStore, createClientQuery };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { LunoraError } from '@lunora/errors';
|
|
2
|
+
import { p as promisifyRequest, c as createWithStore, a as createDatabaseOpener } from './idb-utility-DrSVX43Q.mjs';
|
|
2
3
|
|
|
3
4
|
const createInMemoryPersistence = () => {
|
|
4
5
|
const entries = /* @__PURE__ */ new Map();
|
|
@@ -30,14 +31,6 @@ const createInMemoryPersistence = () => {
|
|
|
30
31
|
const DEFAULT_DATABASE = "lunora-outbox";
|
|
31
32
|
const DEFAULT_STORE = "offline-mutations";
|
|
32
33
|
const ID_INDEX = "by_id";
|
|
33
|
-
const promisifyRequest = (request) => new Promise((resolve, reject) => {
|
|
34
|
-
request.addEventListener("success", () => {
|
|
35
|
-
resolve(request.result);
|
|
36
|
-
});
|
|
37
|
-
request.addEventListener("error", () => {
|
|
38
|
-
reject(request.error ?? new Error("IndexedDB request failed"));
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
34
|
const createIndexedDbPersistence = (options = {}) => {
|
|
42
35
|
const factory = options.indexedDB ?? (typeof indexedDB === "undefined" ? void 0 : indexedDB);
|
|
43
36
|
if (!factory) {
|
|
@@ -45,46 +38,13 @@ const createIndexedDbPersistence = (options = {}) => {
|
|
|
45
38
|
}
|
|
46
39
|
const databaseName = options.databaseName ?? DEFAULT_DATABASE;
|
|
47
40
|
const storeName = options.storeName ?? DEFAULT_STORE;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
41
|
+
const openDatabase = createDatabaseOpener(factory, databaseName, 1, (database) => {
|
|
42
|
+
if (!database.objectStoreNames.contains(storeName)) {
|
|
43
|
+
const store = database.createObjectStore(storeName, { autoIncrement: true });
|
|
44
|
+
store.createIndex(ID_INDEX, "id", { unique: true });
|
|
52
45
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
request.addEventListener("upgradeneeded", () => {
|
|
56
|
-
const database = request.result;
|
|
57
|
-
if (!database.objectStoreNames.contains(storeName)) {
|
|
58
|
-
const store = database.createObjectStore(storeName, { autoIncrement: true });
|
|
59
|
-
store.createIndex(ID_INDEX, "id", { unique: true });
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
request.addEventListener("success", () => {
|
|
63
|
-
resolve(request.result);
|
|
64
|
-
});
|
|
65
|
-
request.addEventListener("error", () => {
|
|
66
|
-
reject(request.error ?? new Error("IndexedDB open failed"));
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
return databasePromise;
|
|
70
|
-
};
|
|
71
|
-
const withStore = async (mode, run) => {
|
|
72
|
-
const database = await openDatabase();
|
|
73
|
-
const transaction = database.transaction(storeName, mode);
|
|
74
|
-
const result = await run(transaction.objectStore(storeName));
|
|
75
|
-
await new Promise((resolve, reject) => {
|
|
76
|
-
transaction.addEventListener("complete", () => {
|
|
77
|
-
resolve();
|
|
78
|
-
});
|
|
79
|
-
transaction.addEventListener("error", () => {
|
|
80
|
-
reject(transaction.error ?? new Error("IndexedDB transaction failed"));
|
|
81
|
-
});
|
|
82
|
-
transaction.addEventListener("abort", () => {
|
|
83
|
-
reject(transaction.error ?? new Error("IndexedDB transaction aborted"));
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
return result;
|
|
87
|
-
};
|
|
46
|
+
});
|
|
47
|
+
const withStore = createWithStore(openDatabase, storeName);
|
|
88
48
|
return {
|
|
89
49
|
append: async (mutation) => {
|
|
90
50
|
await withStore("readwrite", (store) => promisifyRequest(store.add(mutation)));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { LunoraError } from '@lunora/errors';
|
|
2
|
+
import { p as promisifyRequest, c as createWithStore, a as createDatabaseOpener } from './idb-utility-DrSVX43Q.mjs';
|
|
2
3
|
|
|
3
4
|
const queryCacheKey = (functionPath, argsKey, shardKey) => `${functionPath}::${argsKey}::${shardKey ?? ""}`;
|
|
4
5
|
const DEFAULT_MAX_ENTRIES = 500;
|
|
@@ -41,14 +42,6 @@ const DEFAULT_DATABASE = "lunora-query-cache";
|
|
|
41
42
|
const DEFAULT_STORE = "query-cache";
|
|
42
43
|
const TS_INDEX = "by_ts";
|
|
43
44
|
const DATABASE_VERSION = 1;
|
|
44
|
-
const promisifyRequest = (request) => new Promise((resolve, reject) => {
|
|
45
|
-
request.addEventListener("success", () => {
|
|
46
|
-
resolve(request.result);
|
|
47
|
-
});
|
|
48
|
-
request.addEventListener("error", () => {
|
|
49
|
-
reject(request.error ?? new Error("IndexedDB request failed"));
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
45
|
const createIndexedDbQueryCache = (options = {}) => {
|
|
53
46
|
const factory = options.indexedDB ?? (typeof indexedDB === "undefined" ? void 0 : indexedDB);
|
|
54
47
|
if (!factory) {
|
|
@@ -57,46 +50,13 @@ const createIndexedDbQueryCache = (options = {}) => {
|
|
|
57
50
|
const databaseName = options.databaseName ?? DEFAULT_DATABASE;
|
|
58
51
|
const storeName = options.storeName ?? DEFAULT_STORE;
|
|
59
52
|
const maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
53
|
+
const openDatabase = createDatabaseOpener(factory, databaseName, DATABASE_VERSION, (database) => {
|
|
54
|
+
if (!database.objectStoreNames.contains(storeName)) {
|
|
55
|
+
const store = database.createObjectStore(storeName, { keyPath: "key" });
|
|
56
|
+
store.createIndex(TS_INDEX, "ts", { unique: false });
|
|
64
57
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
request.addEventListener("upgradeneeded", () => {
|
|
68
|
-
const database = request.result;
|
|
69
|
-
if (!database.objectStoreNames.contains(storeName)) {
|
|
70
|
-
const store = database.createObjectStore(storeName, { keyPath: "key" });
|
|
71
|
-
store.createIndex(TS_INDEX, "ts", { unique: false });
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
request.addEventListener("success", () => {
|
|
75
|
-
resolve(request.result);
|
|
76
|
-
});
|
|
77
|
-
request.addEventListener("error", () => {
|
|
78
|
-
reject(request.error ?? new Error("IndexedDB open failed"));
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
return databasePromise;
|
|
82
|
-
};
|
|
83
|
-
const withStore = async (mode, run) => {
|
|
84
|
-
const database = await openDatabase();
|
|
85
|
-
const transaction = database.transaction(storeName, mode);
|
|
86
|
-
const result = await run(transaction.objectStore(storeName));
|
|
87
|
-
await new Promise((resolve, reject) => {
|
|
88
|
-
transaction.addEventListener("complete", () => {
|
|
89
|
-
resolve();
|
|
90
|
-
});
|
|
91
|
-
transaction.addEventListener("error", () => {
|
|
92
|
-
reject(transaction.error ?? new Error("IndexedDB transaction failed"));
|
|
93
|
-
});
|
|
94
|
-
transaction.addEventListener("abort", () => {
|
|
95
|
-
reject(transaction.error ?? new Error("IndexedDB transaction aborted"));
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
return result;
|
|
99
|
-
};
|
|
58
|
+
});
|
|
59
|
+
const withStore = createWithStore(openDatabase, storeName);
|
|
100
60
|
const evict = async (store) => {
|
|
101
61
|
const count = await promisifyRequest(store.count());
|
|
102
62
|
let overflow = count - maxEntries;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const sendToSw = (sw, message, expectResponse = false) => new Promise((resolve, reject) => {
|
|
2
|
+
if (!sw) {
|
|
3
|
+
reject(new Error("No active service worker"));
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
const id = message.correlationId ?? crypto.randomUUID();
|
|
7
|
+
const outgoingMessage = { ...message, correlationId: id };
|
|
8
|
+
if (expectResponse) {
|
|
9
|
+
let timer;
|
|
10
|
+
const handler = (event) => {
|
|
11
|
+
if (event.data.correlationId === id) {
|
|
12
|
+
clearTimeout(timer);
|
|
13
|
+
navigator.serviceWorker.removeEventListener("message", handler);
|
|
14
|
+
resolve(event.data.payload);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
timer = setTimeout(() => {
|
|
18
|
+
navigator.serviceWorker.removeEventListener("message", handler);
|
|
19
|
+
reject(new Error(`SW message ${id} timed out`));
|
|
20
|
+
}, 3e4);
|
|
21
|
+
navigator.serviceWorker.addEventListener("message", handler);
|
|
22
|
+
sw.postMessage(outgoingMessage);
|
|
23
|
+
} else {
|
|
24
|
+
sw.postMessage(outgoingMessage);
|
|
25
|
+
resolve(void 0);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
const createReply = (original, payload) => {
|
|
29
|
+
return {
|
|
30
|
+
type: `${original.type}:reply`,
|
|
31
|
+
payload,
|
|
32
|
+
correlationId: original.correlationId
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export { createReply, sendToSw };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { s as stableStringify } from './stable-key-wv6eP48B.mjs';
|
|
2
|
+
|
|
3
|
+
const createSnapshotPrecondition = (client, functionRef, args, shardKey) => {
|
|
4
|
+
const snapshot = client.peekActiveQueryValue(functionRef.__lunoraRef, args, shardKey);
|
|
5
|
+
const snapshotKey = snapshot === void 0 ? void 0 : stableStringify(snapshot);
|
|
6
|
+
return () => {
|
|
7
|
+
const current = client.peekActiveQueryValue(functionRef.__lunoraRef, args, shardKey);
|
|
8
|
+
if (snapshotKey === void 0 && current === void 0) {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
if (snapshotKey === void 0 || current === void 0) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
return stableStringify(current) === snapshotKey;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export { createSnapshotPrecondition as default };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const promisifyRequest = (request) => new Promise((resolve, reject) => {
|
|
2
|
+
request.addEventListener("success", () => {
|
|
3
|
+
resolve(request.result);
|
|
4
|
+
});
|
|
5
|
+
request.addEventListener("error", () => {
|
|
6
|
+
reject(request.error ?? new Error("IndexedDB request failed"));
|
|
7
|
+
});
|
|
8
|
+
});
|
|
9
|
+
const createDatabaseOpener = (factory, databaseName, version, upgrade) => {
|
|
10
|
+
let databasePromise;
|
|
11
|
+
return () => {
|
|
12
|
+
if (databasePromise) {
|
|
13
|
+
return databasePromise;
|
|
14
|
+
}
|
|
15
|
+
databasePromise = new Promise((resolve, reject) => {
|
|
16
|
+
const request = factory.open(databaseName, version);
|
|
17
|
+
request.addEventListener("upgradeneeded", () => {
|
|
18
|
+
upgrade(request.result);
|
|
19
|
+
});
|
|
20
|
+
request.addEventListener("success", () => {
|
|
21
|
+
resolve(request.result);
|
|
22
|
+
});
|
|
23
|
+
request.addEventListener("error", () => {
|
|
24
|
+
reject(request.error ?? new Error("IndexedDB open failed"));
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
return databasePromise;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
const createWithStore = (open, storeName) => async (mode, run) => {
|
|
31
|
+
const database = await open();
|
|
32
|
+
const transaction = database.transaction(storeName, mode);
|
|
33
|
+
const result = await run(transaction.objectStore(storeName));
|
|
34
|
+
await new Promise((resolve, reject) => {
|
|
35
|
+
transaction.addEventListener("complete", () => {
|
|
36
|
+
resolve();
|
|
37
|
+
});
|
|
38
|
+
transaction.addEventListener("error", () => {
|
|
39
|
+
reject(transaction.error ?? new Error("IndexedDB transaction failed"));
|
|
40
|
+
});
|
|
41
|
+
transaction.addEventListener("abort", () => {
|
|
42
|
+
reject(transaction.error ?? new Error("IndexedDB transaction aborted"));
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export { createDatabaseOpener as a, createWithStore as c, promisifyRequest as p };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { SubscriptionRegistry } from './SubscriptionRegistry-D4jfIzZu.mjs';
|
|
2
|
+
|
|
1
3
|
const foldOptimistic = (base, layers) => {
|
|
2
4
|
let value = base;
|
|
3
5
|
for (const layer of layers) {
|
|
@@ -68,33 +70,26 @@ const dropConfirmedLayers = (state, cursor) => {
|
|
|
68
70
|
return state.optimisticLayers.length !== before;
|
|
69
71
|
};
|
|
70
72
|
|
|
71
|
-
const createLocalStore = (subscriptions, shardKey
|
|
73
|
+
const createLocalStore = (subscriptions, shardKey) => {
|
|
72
74
|
const confirms = [];
|
|
73
75
|
const rollbacks = [];
|
|
74
|
-
const findState = (functionRef,
|
|
75
|
-
for (const state of subscriptions.all()) {
|
|
76
|
-
if (state.fn.__lunoraRef === functionRef && state.shardKey === shardKey && state.argsKey === argsKey) {
|
|
77
|
-
return state;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return void 0;
|
|
81
|
-
};
|
|
76
|
+
const findState = (functionRef, args) => subscriptions.get(SubscriptionRegistry.key(functionRef, args, shardKey));
|
|
82
77
|
const store = {
|
|
83
78
|
getAllQueries: (function_) => {
|
|
84
79
|
const matches = [];
|
|
85
80
|
for (const state of subscriptions.all()) {
|
|
86
|
-
if (state.fn.__lunoraRef === function_.__lunoraRef && state.shardKey === shardKey) {
|
|
81
|
+
if (state.fn.__lunoraRef === function_.__lunoraRef && (state.shardKey ?? "") === (shardKey ?? "")) {
|
|
87
82
|
matches.push({ args: state.args, value: state.lastValue });
|
|
88
83
|
}
|
|
89
84
|
}
|
|
90
85
|
return matches;
|
|
91
86
|
},
|
|
92
87
|
getQuery: (function_, args) => {
|
|
93
|
-
const state = findState(function_.__lunoraRef,
|
|
88
|
+
const state = findState(function_.__lunoraRef, args ?? {});
|
|
94
89
|
return state?.lastValue;
|
|
95
90
|
},
|
|
96
91
|
setQuery: (function_, args, value) => {
|
|
97
|
-
const state = findState(function_.__lunoraRef,
|
|
92
|
+
const state = findState(function_.__lunoraRef, args ?? {});
|
|
98
93
|
if (!state) {
|
|
99
94
|
return;
|
|
100
95
|
}
|