@drakkar.software/starfish-client 1.19.0 → 1.19.2
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/bindings/legend.js +60 -60
- package/dist/bindings/legend.js.map +7 -0
- package/dist/bindings/zustand.js +785 -250
- package/dist/bindings/zustand.js.map +7 -0
- package/dist/broadcast.js +69 -79
- package/dist/broadcast.js.map +7 -0
- package/dist/fetch.js +131 -156
- package/dist/fetch.js.map +7 -0
- package/dist/group-crypto.js +188 -213
- package/dist/group-crypto.js.map +7 -0
- package/dist/identity.js +346 -154
- package/dist/identity.js.map +7 -0
- package/dist/index.js +1344 -25
- package/dist/index.js.map +7 -0
- package/dist/testing.js +64 -83
- package/dist/testing.js.map +7 -0
- package/package.json +3 -2
package/dist/bindings/zustand.js
CHANGED
|
@@ -1,266 +1,801 @@
|
|
|
1
|
+
// src/bindings/zustand.ts
|
|
1
2
|
import { createStore } from "zustand/vanilla";
|
|
2
3
|
import { useStore } from "zustand";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
4
|
+
|
|
5
|
+
// ../../../node_modules/.pnpm/zustand@5.0.11_@types+react@19.2.14_immer@11.1.4_react@19.2.4_use-sync-external-store@1.6.0_react@19.2.4_/node_modules/zustand/esm/middleware.mjs
|
|
6
|
+
var subscribeWithSelectorImpl = (fn) => (set, get, api) => {
|
|
7
|
+
const origSubscribe = api.subscribe;
|
|
8
|
+
api.subscribe = ((selector, optListener, options) => {
|
|
9
|
+
let listener = selector;
|
|
10
|
+
if (optListener) {
|
|
11
|
+
const equalityFn = (options == null ? void 0 : options.equalityFn) || Object.is;
|
|
12
|
+
let currentSlice = selector(api.getState());
|
|
13
|
+
listener = (state) => {
|
|
14
|
+
const nextSlice = selector(state);
|
|
15
|
+
if (!equalityFn(currentSlice, nextSlice)) {
|
|
16
|
+
const previousSlice = currentSlice;
|
|
17
|
+
optListener(currentSlice = nextSlice, previousSlice);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
if (options == null ? void 0 : options.fireImmediately) {
|
|
21
|
+
optListener(currentSlice, currentSlice);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return origSubscribe(listener);
|
|
25
|
+
});
|
|
26
|
+
const initialState = fn(set, get, api);
|
|
27
|
+
return initialState;
|
|
28
|
+
};
|
|
29
|
+
var subscribeWithSelector = subscribeWithSelectorImpl;
|
|
30
|
+
function createJSONStorage(getStorage, options) {
|
|
31
|
+
let storage;
|
|
32
|
+
try {
|
|
33
|
+
storage = getStorage();
|
|
34
|
+
} catch (e) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const persistStorage = {
|
|
38
|
+
getItem: (name) => {
|
|
39
|
+
var _a;
|
|
40
|
+
const parse = (str2) => {
|
|
41
|
+
if (str2 === null) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return JSON.parse(str2, options == null ? void 0 : options.reviver);
|
|
45
|
+
};
|
|
46
|
+
const str = (_a = storage.getItem(name)) != null ? _a : null;
|
|
47
|
+
if (str instanceof Promise) {
|
|
48
|
+
return str.then(parse);
|
|
49
|
+
}
|
|
50
|
+
return parse(str);
|
|
51
|
+
},
|
|
52
|
+
setItem: (name, newValue) => storage.setItem(name, JSON.stringify(newValue, options == null ? void 0 : options.replacer)),
|
|
53
|
+
removeItem: (name) => storage.removeItem(name)
|
|
54
|
+
};
|
|
55
|
+
return persistStorage;
|
|
56
|
+
}
|
|
57
|
+
var toThenable = (fn) => (input) => {
|
|
58
|
+
try {
|
|
59
|
+
const result = fn(input);
|
|
60
|
+
if (result instanceof Promise) {
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
then(onFulfilled) {
|
|
65
|
+
return toThenable(onFulfilled)(result);
|
|
66
|
+
},
|
|
67
|
+
catch(_onRejected) {
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return {
|
|
73
|
+
then(_onFulfilled) {
|
|
74
|
+
return this;
|
|
75
|
+
},
|
|
76
|
+
catch(onRejected) {
|
|
77
|
+
return toThenable(onRejected)(e);
|
|
78
|
+
}
|
|
66
79
|
};
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
var persistImpl = (config, baseOptions) => (set, get, api) => {
|
|
83
|
+
let options = {
|
|
84
|
+
storage: createJSONStorage(() => window.localStorage),
|
|
85
|
+
partialize: (state) => state,
|
|
86
|
+
version: 0,
|
|
87
|
+
merge: (persistedState, currentState) => ({
|
|
88
|
+
...currentState,
|
|
89
|
+
...persistedState
|
|
90
|
+
}),
|
|
91
|
+
...baseOptions
|
|
92
|
+
};
|
|
93
|
+
let hasHydrated = false;
|
|
94
|
+
let hydrationVersion = 0;
|
|
95
|
+
const hydrationListeners = /* @__PURE__ */ new Set();
|
|
96
|
+
const finishHydrationListeners = /* @__PURE__ */ new Set();
|
|
97
|
+
let storage = options.storage;
|
|
98
|
+
if (!storage) {
|
|
99
|
+
return config(
|
|
100
|
+
(...args) => {
|
|
101
|
+
console.warn(
|
|
102
|
+
`[zustand persist middleware] Unable to update item '${options.name}', the given storage is currently unavailable.`
|
|
103
|
+
);
|
|
104
|
+
set(...args);
|
|
105
|
+
},
|
|
106
|
+
get,
|
|
107
|
+
api
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const setItem = () => {
|
|
111
|
+
const state = options.partialize({ ...get() });
|
|
112
|
+
return storage.setItem(options.name, {
|
|
113
|
+
state,
|
|
114
|
+
version: options.version
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
const savedSetState = api.setState;
|
|
118
|
+
api.setState = (state, replace) => {
|
|
119
|
+
savedSetState(state, replace);
|
|
120
|
+
return setItem();
|
|
121
|
+
};
|
|
122
|
+
const configResult = config(
|
|
123
|
+
(...args) => {
|
|
124
|
+
set(...args);
|
|
125
|
+
return setItem();
|
|
126
|
+
},
|
|
127
|
+
get,
|
|
128
|
+
api
|
|
129
|
+
);
|
|
130
|
+
api.getInitialState = () => configResult;
|
|
131
|
+
let stateFromStorage;
|
|
132
|
+
const hydrate = () => {
|
|
133
|
+
var _a, _b;
|
|
134
|
+
if (!storage) return;
|
|
135
|
+
const currentVersion = ++hydrationVersion;
|
|
136
|
+
hasHydrated = false;
|
|
137
|
+
hydrationListeners.forEach((cb) => {
|
|
138
|
+
var _a2;
|
|
139
|
+
return cb((_a2 = get()) != null ? _a2 : configResult);
|
|
140
|
+
});
|
|
141
|
+
const postRehydrationCallback = ((_b = options.onRehydrateStorage) == null ? void 0 : _b.call(options, (_a = get()) != null ? _a : configResult)) || void 0;
|
|
142
|
+
return toThenable(storage.getItem.bind(storage))(options.name).then((deserializedStorageValue) => {
|
|
143
|
+
if (deserializedStorageValue) {
|
|
144
|
+
if (typeof deserializedStorageValue.version === "number" && deserializedStorageValue.version !== options.version) {
|
|
145
|
+
if (options.migrate) {
|
|
146
|
+
const migration = options.migrate(
|
|
147
|
+
deserializedStorageValue.state,
|
|
148
|
+
deserializedStorageValue.version
|
|
149
|
+
);
|
|
150
|
+
if (migration instanceof Promise) {
|
|
151
|
+
return migration.then((result) => [true, result]);
|
|
152
|
+
}
|
|
153
|
+
return [true, migration];
|
|
154
|
+
}
|
|
155
|
+
console.error(
|
|
156
|
+
`State loaded from storage couldn't be migrated since no migrate function was provided`
|
|
157
|
+
);
|
|
158
|
+
} else {
|
|
159
|
+
return [false, deserializedStorageValue.state];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return [false, void 0];
|
|
163
|
+
}).then((migrationResult) => {
|
|
164
|
+
var _a2;
|
|
165
|
+
if (currentVersion !== hydrationVersion) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const [migrated, migratedState] = migrationResult;
|
|
169
|
+
stateFromStorage = options.merge(
|
|
170
|
+
migratedState,
|
|
171
|
+
(_a2 = get()) != null ? _a2 : configResult
|
|
172
|
+
);
|
|
173
|
+
set(stateFromStorage, true);
|
|
174
|
+
if (migrated) {
|
|
175
|
+
return setItem();
|
|
176
|
+
}
|
|
177
|
+
}).then(() => {
|
|
178
|
+
if (currentVersion !== hydrationVersion) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
postRehydrationCallback == null ? void 0 : postRehydrationCallback(stateFromStorage, void 0);
|
|
182
|
+
stateFromStorage = get();
|
|
183
|
+
hasHydrated = true;
|
|
184
|
+
finishHydrationListeners.forEach((cb) => cb(stateFromStorage));
|
|
185
|
+
}).catch((e) => {
|
|
186
|
+
if (currentVersion !== hydrationVersion) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
postRehydrationCallback == null ? void 0 : postRehydrationCallback(void 0, e);
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
api.persist = {
|
|
193
|
+
setOptions: (newOptions) => {
|
|
194
|
+
options = {
|
|
195
|
+
...options,
|
|
196
|
+
...newOptions
|
|
197
|
+
};
|
|
198
|
+
if (newOptions.storage) {
|
|
199
|
+
storage = newOptions.storage;
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
clearStorage: () => {
|
|
203
|
+
storage == null ? void 0 : storage.removeItem(options.name);
|
|
204
|
+
},
|
|
205
|
+
getOptions: () => options,
|
|
206
|
+
rehydrate: () => hydrate(),
|
|
207
|
+
hasHydrated: () => hasHydrated,
|
|
208
|
+
onHydrate: (cb) => {
|
|
209
|
+
hydrationListeners.add(cb);
|
|
210
|
+
return () => {
|
|
211
|
+
hydrationListeners.delete(cb);
|
|
212
|
+
};
|
|
213
|
+
},
|
|
214
|
+
onFinishHydration: (cb) => {
|
|
215
|
+
finishHydrationListeners.add(cb);
|
|
216
|
+
return () => {
|
|
217
|
+
finishHydrationListeners.delete(cb);
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
if (!options.skipHydration) {
|
|
222
|
+
hydrate();
|
|
223
|
+
}
|
|
224
|
+
return stateFromStorage || configResult;
|
|
225
|
+
};
|
|
226
|
+
var persist = persistImpl;
|
|
227
|
+
|
|
228
|
+
// src/bindings/zustand.ts
|
|
229
|
+
import { useEffect, useRef, useState, useCallback } from "react";
|
|
230
|
+
|
|
231
|
+
// src/types.ts
|
|
232
|
+
var ConflictError = class extends Error {
|
|
233
|
+
constructor() {
|
|
234
|
+
super("hash_mismatch");
|
|
235
|
+
this.name = "ConflictError";
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
var StarfishHttpError = class extends Error {
|
|
239
|
+
constructor(status, body) {
|
|
240
|
+
super(`HTTP ${status}: ${body}`);
|
|
241
|
+
this.status = status;
|
|
242
|
+
this.body = body;
|
|
243
|
+
this.name = "StarfishHttpError";
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// src/client.ts
|
|
248
|
+
var StarfishClient = class {
|
|
249
|
+
baseUrl;
|
|
250
|
+
auth;
|
|
251
|
+
fetch;
|
|
252
|
+
constructor(options) {
|
|
253
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
254
|
+
this.auth = options.auth;
|
|
255
|
+
this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Pull synced data from the server.
|
|
259
|
+
* @param path - The pull endpoint path (e.g. "/pull/users/abc/settings")
|
|
260
|
+
* @param checkpoint - Only return data updated after this timestamp (0 = full pull)
|
|
261
|
+
*/
|
|
262
|
+
async pull(path, checkpoint) {
|
|
263
|
+
const url = checkpoint ? `${this.baseUrl}${path}?checkpoint=${checkpoint}` : `${this.baseUrl}${path}`;
|
|
264
|
+
const authHeaders = this.auth ? await this.auth({ method: "GET", path, body: null }) : {};
|
|
265
|
+
const res = await this.fetch(url, {
|
|
266
|
+
method: "GET",
|
|
267
|
+
headers: { Accept: "application/json", ...authHeaders }
|
|
268
|
+
});
|
|
269
|
+
if (!res.ok) {
|
|
270
|
+
throw new StarfishHttpError(res.status, await res.text());
|
|
271
|
+
}
|
|
272
|
+
return res.json();
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Push synced data to the server.
|
|
276
|
+
* @param path - The push endpoint path (e.g. "/push/users/abc/settings")
|
|
277
|
+
* @param data - The full document data to push
|
|
278
|
+
* @param baseHash - Hash of the document this push is based on (null for first push)
|
|
279
|
+
* @param authorSignature - Optional author signature for provenance
|
|
280
|
+
* @throws {ConflictError} if the server detects a hash mismatch (409)
|
|
281
|
+
*/
|
|
282
|
+
async push(path, data, baseHash, authorSignature) {
|
|
283
|
+
const body = JSON.stringify({
|
|
284
|
+
data,
|
|
285
|
+
baseHash,
|
|
286
|
+
...authorSignature && { authorSignature }
|
|
287
|
+
});
|
|
288
|
+
const authHeaders = this.auth ? await this.auth({ method: "POST", path, body }) : {};
|
|
289
|
+
const res = await this.fetch(`${this.baseUrl}${path}`, {
|
|
290
|
+
method: "POST",
|
|
291
|
+
headers: {
|
|
292
|
+
"Content-Type": "application/json",
|
|
293
|
+
Accept: "application/json",
|
|
294
|
+
...authHeaders
|
|
295
|
+
},
|
|
296
|
+
body
|
|
297
|
+
});
|
|
298
|
+
if (res.status === 409) {
|
|
299
|
+
throw new ConflictError();
|
|
300
|
+
}
|
|
301
|
+
if (!res.ok) {
|
|
302
|
+
throw new StarfishHttpError(res.status, await res.text());
|
|
303
|
+
}
|
|
304
|
+
return res.json();
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Pull binary data from a blob collection.
|
|
308
|
+
* Returns raw bytes with the content hash from the ETag header.
|
|
309
|
+
*/
|
|
310
|
+
async pullBlob(path) {
|
|
311
|
+
const authHeaders = this.auth ? await this.auth({ method: "GET", path, body: null }) : {};
|
|
312
|
+
const res = await this.fetch(`${this.baseUrl}${path}`, {
|
|
313
|
+
method: "GET",
|
|
314
|
+
headers: { Accept: "*/*", ...authHeaders }
|
|
315
|
+
});
|
|
316
|
+
if (!res.ok) {
|
|
317
|
+
throw new StarfishHttpError(res.status, await res.text());
|
|
318
|
+
}
|
|
319
|
+
const etag = res.headers.get("ETag")?.replace(/"/g, "") ?? null;
|
|
320
|
+
const contentType = res.headers.get("Content-Type") ?? "application/octet-stream";
|
|
321
|
+
const data = await res.arrayBuffer();
|
|
322
|
+
return { data, hash: etag, contentType };
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Push binary data to a blob collection.
|
|
326
|
+
* Binary collections use last-write-wins (no conflict detection).
|
|
327
|
+
*/
|
|
328
|
+
async pushBlob(path, data, contentType) {
|
|
329
|
+
const authHeaders = this.auth ? await this.auth({ method: "POST", path, body: null }) : {};
|
|
330
|
+
const res = await this.fetch(`${this.baseUrl}${path}`, {
|
|
331
|
+
method: "POST",
|
|
332
|
+
headers: {
|
|
333
|
+
"Content-Type": contentType,
|
|
334
|
+
Accept: "application/json",
|
|
335
|
+
...authHeaders
|
|
336
|
+
},
|
|
337
|
+
body: data
|
|
338
|
+
});
|
|
339
|
+
if (!res.ok) {
|
|
340
|
+
throw new StarfishHttpError(res.status, await res.text());
|
|
341
|
+
}
|
|
342
|
+
return res.json();
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// src/sync.ts
|
|
347
|
+
import { deepMerge, stableStringify } from "@drakkar.software/starfish-protocol";
|
|
348
|
+
|
|
349
|
+
// src/crypto.ts
|
|
350
|
+
import { getCrypto, getBase64, IV_BYTES, ENCRYPTED_KEY, deriveKey } from "@drakkar.software/starfish-protocol";
|
|
351
|
+
var ALGO = "AES-GCM";
|
|
352
|
+
function createEncryptor(secret, salt, info = "starfish-e2e") {
|
|
353
|
+
if (!secret) throw new Error("encryptionSecret must not be empty");
|
|
354
|
+
if (!salt) throw new Error("encryptionSalt must not be empty");
|
|
355
|
+
const keyPromise = deriveKey(secret, salt, info);
|
|
356
|
+
return {
|
|
357
|
+
async encrypt(data) {
|
|
358
|
+
const key = await keyPromise;
|
|
359
|
+
const c = getCrypto();
|
|
360
|
+
const b64 = getBase64();
|
|
361
|
+
const plaintext = new TextEncoder().encode(JSON.stringify(data));
|
|
362
|
+
const iv = c.getRandomValues(new Uint8Array(IV_BYTES));
|
|
363
|
+
const ciphertext = await c.subtle.encrypt({ name: ALGO, iv }, key, plaintext);
|
|
364
|
+
const combined = new Uint8Array(iv.length + ciphertext.byteLength);
|
|
365
|
+
combined.set(iv);
|
|
366
|
+
combined.set(new Uint8Array(ciphertext), iv.length);
|
|
367
|
+
return { [ENCRYPTED_KEY]: b64.encode(combined) };
|
|
368
|
+
},
|
|
369
|
+
async decrypt(wrapper) {
|
|
370
|
+
const encoded = wrapper[ENCRYPTED_KEY];
|
|
371
|
+
if (typeof encoded !== "string") {
|
|
372
|
+
throw new Error("Expected encrypted data but received unencrypted document");
|
|
373
|
+
}
|
|
374
|
+
const key = await keyPromise;
|
|
375
|
+
const c = getCrypto();
|
|
376
|
+
const b64 = getBase64();
|
|
377
|
+
const combined = b64.decode(encoded);
|
|
378
|
+
if (combined.length < IV_BYTES) {
|
|
379
|
+
throw new Error("Encrypted data is too short");
|
|
380
|
+
}
|
|
381
|
+
const iv = combined.slice(0, IV_BYTES);
|
|
382
|
+
const ciphertext = combined.slice(IV_BYTES);
|
|
383
|
+
try {
|
|
384
|
+
const plaintext = await c.subtle.decrypt({ name: ALGO, iv }, key, ciphertext);
|
|
385
|
+
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
386
|
+
} catch (err) {
|
|
387
|
+
throw new Error("Decryption failed: data may be tampered or key is incorrect", { cause: err });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// src/validate.ts
|
|
394
|
+
var ValidationError = class extends Error {
|
|
395
|
+
constructor(errors) {
|
|
396
|
+
super(`Validation failed: ${errors.join("; ")}`);
|
|
397
|
+
this.errors = errors;
|
|
398
|
+
this.name = "ValidationError";
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// src/sync.ts
|
|
403
|
+
var SyncManager = class {
|
|
404
|
+
client;
|
|
405
|
+
pullPath;
|
|
406
|
+
pushPath;
|
|
407
|
+
onConflict;
|
|
408
|
+
maxRetries;
|
|
409
|
+
encryptor;
|
|
410
|
+
signData;
|
|
411
|
+
logger;
|
|
412
|
+
loggerName;
|
|
413
|
+
validate;
|
|
414
|
+
lastHash = null;
|
|
415
|
+
lastCheckpoint = 0;
|
|
416
|
+
localData = {};
|
|
417
|
+
constructor(options) {
|
|
418
|
+
this.client = options.client;
|
|
419
|
+
this.pullPath = options.pullPath;
|
|
420
|
+
this.pushPath = options.pushPath;
|
|
421
|
+
this.onConflict = options.onConflict ?? deepMerge;
|
|
422
|
+
this.maxRetries = options.maxRetries ?? 3;
|
|
423
|
+
this.signData = options.signData;
|
|
424
|
+
this.logger = options.logger;
|
|
425
|
+
this.loggerName = options.loggerName ?? options.pullPath.split("/").filter(Boolean).pop() ?? options.pullPath;
|
|
426
|
+
this.validate = options.validate;
|
|
427
|
+
this.encryptor = options.encryptor ?? (options.encryptionSecret && options.encryptionSalt ? createEncryptor(options.encryptionSecret, options.encryptionSalt, options.encryptionInfo) : null);
|
|
428
|
+
}
|
|
429
|
+
getData() {
|
|
430
|
+
return { ...this.localData };
|
|
431
|
+
}
|
|
432
|
+
getHash() {
|
|
433
|
+
return this.lastHash;
|
|
434
|
+
}
|
|
435
|
+
getCheckpoint() {
|
|
436
|
+
return this.lastCheckpoint;
|
|
437
|
+
}
|
|
438
|
+
async pull() {
|
|
439
|
+
this.logger?.pullStart(this.loggerName);
|
|
440
|
+
const start = performance.now();
|
|
441
|
+
try {
|
|
442
|
+
const result = await this.client.pull(this.pullPath, this.lastCheckpoint);
|
|
443
|
+
if (this.encryptor) {
|
|
444
|
+
const decrypted = await this.encryptor.decrypt(result.data);
|
|
445
|
+
this.localData = decrypted;
|
|
446
|
+
result.data = decrypted;
|
|
447
|
+
} else if (this.lastCheckpoint > 0) {
|
|
448
|
+
this.localData = deepMerge(this.localData, result.data);
|
|
449
|
+
result.data = this.localData;
|
|
450
|
+
} else {
|
|
451
|
+
this.localData = result.data;
|
|
452
|
+
}
|
|
453
|
+
this.lastHash = result.hash;
|
|
454
|
+
this.lastCheckpoint = result.timestamp;
|
|
455
|
+
this.logger?.pullSuccess(this.loggerName, Math.round(performance.now() - start));
|
|
456
|
+
return result;
|
|
457
|
+
} catch (err) {
|
|
458
|
+
this.logger?.pullError(this.loggerName, err instanceof Error ? err.message : String(err));
|
|
459
|
+
throw err;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
async push(data) {
|
|
463
|
+
if (this.validate) {
|
|
464
|
+
const result = this.validate(data);
|
|
465
|
+
if (result !== true) throw new ValidationError(result);
|
|
466
|
+
}
|
|
467
|
+
this.logger?.pushStart(this.loggerName);
|
|
468
|
+
const start = performance.now();
|
|
469
|
+
let attempt = 0;
|
|
470
|
+
let pendingData = data;
|
|
471
|
+
while (attempt <= this.maxRetries) {
|
|
472
|
+
try {
|
|
473
|
+
const payload = this.encryptor ? await this.encryptor.encrypt(pendingData) : pendingData;
|
|
474
|
+
const sig = this.signData ? await this.signData(stableStringify(payload)) : void 0;
|
|
475
|
+
const result = await this.client.push(
|
|
476
|
+
this.pushPath,
|
|
477
|
+
payload,
|
|
478
|
+
this.lastHash,
|
|
479
|
+
sig
|
|
480
|
+
);
|
|
481
|
+
this.lastHash = result.hash;
|
|
482
|
+
this.lastCheckpoint = result.timestamp;
|
|
483
|
+
this.localData = pendingData;
|
|
484
|
+
this.logger?.pushSuccess(this.loggerName, Math.round(performance.now() - start));
|
|
485
|
+
return result;
|
|
486
|
+
} catch (err) {
|
|
487
|
+
if (!(err instanceof ConflictError) || attempt >= this.maxRetries) {
|
|
488
|
+
this.logger?.pushError(this.loggerName, err instanceof Error ? err.message : String(err));
|
|
489
|
+
throw err;
|
|
490
|
+
}
|
|
491
|
+
this.logger?.conflict(this.loggerName, attempt + 1);
|
|
492
|
+
try {
|
|
493
|
+
const remote = await this.client.pull(this.pullPath);
|
|
494
|
+
const remoteData = this.encryptor ? await this.encryptor.decrypt(remote.data) : remote.data;
|
|
495
|
+
this.lastHash = remote.hash;
|
|
496
|
+
this.lastCheckpoint = remote.timestamp;
|
|
497
|
+
pendingData = this.onConflict(pendingData, remoteData);
|
|
498
|
+
} catch (resolveErr) {
|
|
499
|
+
const msg = resolveErr instanceof Error ? resolveErr.message : String(resolveErr);
|
|
500
|
+
this.logger?.pushError(this.loggerName, `Conflict resolution failed (attempt ${attempt + 1}): ${msg}`);
|
|
501
|
+
throw resolveErr;
|
|
502
|
+
}
|
|
503
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(100 * Math.pow(2, attempt), 2e3) + Math.random() * 100));
|
|
504
|
+
attempt++;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
throw new ConflictError();
|
|
508
|
+
}
|
|
509
|
+
async update(modifier) {
|
|
510
|
+
await this.pull();
|
|
511
|
+
const updated = modifier(this.localData);
|
|
512
|
+
return this.push(updated);
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// src/broadcast.ts
|
|
517
|
+
function setupBroadcastSync(store, name) {
|
|
518
|
+
const channel = new BroadcastChannel(`starfish-${name}`);
|
|
519
|
+
let lastReceivedData = null;
|
|
520
|
+
channel.onmessage = (event) => {
|
|
521
|
+
const payload = event.data;
|
|
522
|
+
if (!payload || typeof payload !== "object" || !payload.data || typeof payload.data !== "object") return;
|
|
523
|
+
lastReceivedData = payload.data;
|
|
524
|
+
store.setState({ data: payload.data, dirty: !!payload.dirty });
|
|
525
|
+
};
|
|
526
|
+
const unsub = store.subscribe((state, prev) => {
|
|
527
|
+
if (state.data === lastReceivedData) return;
|
|
528
|
+
if (state.data !== prev.data || state.dirty !== prev.dirty) {
|
|
529
|
+
try {
|
|
530
|
+
channel.postMessage({ data: state.data, dirty: state.dirty });
|
|
531
|
+
} catch {
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
return () => {
|
|
536
|
+
unsub();
|
|
537
|
+
channel.close();
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
function setupStorageFallback(store, name) {
|
|
541
|
+
const storageKey = `starfish-broadcast-${name}`;
|
|
542
|
+
let lastReceivedData = null;
|
|
543
|
+
const onStorage = (e) => {
|
|
544
|
+
if (e.key !== storageKey || !e.newValue) return;
|
|
545
|
+
let payload;
|
|
546
|
+
try {
|
|
547
|
+
payload = JSON.parse(e.newValue);
|
|
548
|
+
} catch {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (!payload || typeof payload !== "object" || !payload.data || typeof payload.data !== "object") return;
|
|
552
|
+
lastReceivedData = payload.data;
|
|
553
|
+
store.setState({ data: payload.data, dirty: !!payload.dirty });
|
|
554
|
+
};
|
|
555
|
+
globalThis.addEventListener("storage", onStorage);
|
|
556
|
+
const unsub = store.subscribe((state, prev) => {
|
|
557
|
+
if (state.data === lastReceivedData) return;
|
|
558
|
+
if (state.data !== prev.data || state.dirty !== prev.dirty) {
|
|
559
|
+
try {
|
|
560
|
+
localStorage.setItem(
|
|
561
|
+
storageKey,
|
|
562
|
+
JSON.stringify({ data: state.data, dirty: state.dirty })
|
|
563
|
+
);
|
|
564
|
+
} catch {
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
return () => {
|
|
569
|
+
unsub();
|
|
570
|
+
globalThis.removeEventListener("storage", onStorage);
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
function setupCrossTabSync(store, name) {
|
|
574
|
+
if (typeof BroadcastChannel !== "undefined") {
|
|
575
|
+
return setupBroadcastSync(store, name);
|
|
576
|
+
}
|
|
577
|
+
if (typeof globalThis.addEventListener === "function" && typeof localStorage !== "undefined") {
|
|
578
|
+
return setupStorageFallback(store, name);
|
|
579
|
+
}
|
|
580
|
+
return () => {
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// src/bindings/zustand.ts
|
|
585
|
+
function createStarfishStore(options) {
|
|
586
|
+
const { name, syncManager, storage } = options;
|
|
587
|
+
const storeCreator = (rawSet, get) => {
|
|
588
|
+
const set = rawSet;
|
|
589
|
+
return {
|
|
590
|
+
data: {},
|
|
591
|
+
syncing: false,
|
|
592
|
+
online: true,
|
|
593
|
+
dirty: false,
|
|
594
|
+
error: null,
|
|
595
|
+
pull: async () => {
|
|
596
|
+
set({ syncing: true, error: null }, false, "pull/start");
|
|
597
|
+
try {
|
|
598
|
+
await syncManager.pull();
|
|
599
|
+
const newData = syncManager.getData();
|
|
600
|
+
set({ data: newData, syncing: false }, false, "pull/success");
|
|
601
|
+
options.onRemoteUpdate?.(newData);
|
|
602
|
+
} catch (err) {
|
|
603
|
+
set({ syncing: false, error: err instanceof Error ? err.message : String(err) }, false, "pull/error");
|
|
604
|
+
}
|
|
605
|
+
},
|
|
606
|
+
set: (modifier) => {
|
|
607
|
+
try {
|
|
608
|
+
const next = options.produce ? options.produce(get().data, modifier) : modifier(get().data);
|
|
609
|
+
set({ data: next, dirty: true, error: null }, false, "set");
|
|
610
|
+
if (get().online) get().flush().catch(() => {
|
|
611
|
+
});
|
|
612
|
+
} catch (err) {
|
|
613
|
+
set({ error: err instanceof Error ? err.message : String(err) }, false, "set/error");
|
|
614
|
+
}
|
|
615
|
+
},
|
|
616
|
+
restore: (data) => {
|
|
617
|
+
set({ data }, false, "restore");
|
|
618
|
+
},
|
|
619
|
+
flush: async () => {
|
|
620
|
+
if (get().syncing || !get().dirty) return;
|
|
621
|
+
set({ syncing: true, error: null }, false, "flush/start");
|
|
622
|
+
try {
|
|
623
|
+
await syncManager.push(get().data);
|
|
624
|
+
set({ data: syncManager.getData(), syncing: false, dirty: false }, false, "flush/success");
|
|
625
|
+
} catch (err) {
|
|
626
|
+
set({ syncing: false, error: err instanceof Error ? err.message : String(err) }, false, "flush/error");
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
setOnline: (online) => {
|
|
630
|
+
set({ online }, false, "setOnline");
|
|
631
|
+
if (online && get().dirty) get().flush().catch(() => {
|
|
76
632
|
});
|
|
77
|
-
|
|
78
|
-
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
};
|
|
636
|
+
const withPersist = storage === false ? storeCreator : persist(storeCreator, {
|
|
637
|
+
name: `starfish-${name}`,
|
|
638
|
+
storage: storage ? createJSONStorage(() => storage) : void 0,
|
|
639
|
+
partialize: (state) => ({
|
|
640
|
+
data: state.data,
|
|
641
|
+
dirty: state.dirty
|
|
642
|
+
})
|
|
643
|
+
});
|
|
644
|
+
const withSelector = subscribeWithSelector(withPersist);
|
|
645
|
+
return createStore()(
|
|
646
|
+
options.devtools ? options.devtools(withSelector) : withSelector
|
|
647
|
+
);
|
|
79
648
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (state.syncing)
|
|
87
|
-
return "syncing";
|
|
88
|
-
if (state.dirty)
|
|
89
|
-
return "pending";
|
|
90
|
-
return "synced";
|
|
649
|
+
function deriveSyncStatus(state) {
|
|
650
|
+
if (!state.online) return "offline";
|
|
651
|
+
if (state.error) return "error";
|
|
652
|
+
if (state.syncing) return "syncing";
|
|
653
|
+
if (state.dirty) return "pending";
|
|
654
|
+
return "synced";
|
|
91
655
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return "error";
|
|
99
|
-
if (statuses.includes("syncing"))
|
|
100
|
-
return "syncing";
|
|
101
|
-
if (statuses.includes("pending"))
|
|
102
|
-
return "pending";
|
|
103
|
-
if (statuses.includes("offline"))
|
|
104
|
-
return "offline";
|
|
105
|
-
return "synced";
|
|
656
|
+
function aggregateSyncStatus(statuses) {
|
|
657
|
+
if (statuses.includes("error")) return "error";
|
|
658
|
+
if (statuses.includes("syncing")) return "syncing";
|
|
659
|
+
if (statuses.includes("pending")) return "pending";
|
|
660
|
+
if (statuses.includes("offline")) return "offline";
|
|
661
|
+
return "synced";
|
|
106
662
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return useStore(store);
|
|
663
|
+
function useStarfish(store) {
|
|
664
|
+
return useStore(store);
|
|
110
665
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
666
|
+
function useStarfishData(store, selector) {
|
|
667
|
+
return useStore(
|
|
668
|
+
store,
|
|
669
|
+
(state) => selector ? selector(state.data) : state.data
|
|
670
|
+
);
|
|
114
671
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return useStore(store, deriveSyncStatus);
|
|
672
|
+
function useSyncStatus(store) {
|
|
673
|
+
return useStore(store, deriveSyncStatus);
|
|
118
674
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
*
|
|
130
|
-
* // Later, to stop listening:
|
|
131
|
-
* unsub()
|
|
132
|
-
* ```
|
|
133
|
-
*/
|
|
134
|
-
export function subscribeSyncStatus(store, callback) {
|
|
135
|
-
let prev = deriveSyncStatus(store.getState());
|
|
136
|
-
callback(prev);
|
|
137
|
-
return store.subscribe((state) => {
|
|
138
|
-
const next = deriveSyncStatus(state);
|
|
139
|
-
if (next !== prev) {
|
|
140
|
-
prev = next;
|
|
141
|
-
callback(next);
|
|
142
|
-
}
|
|
143
|
-
});
|
|
675
|
+
function subscribeSyncStatus(store, callback) {
|
|
676
|
+
let prev = deriveSyncStatus(store.getState());
|
|
677
|
+
callback(prev);
|
|
678
|
+
return store.subscribe((state) => {
|
|
679
|
+
const next = deriveSyncStatus(state);
|
|
680
|
+
if (next !== prev) {
|
|
681
|
+
prev = next;
|
|
682
|
+
callback(next);
|
|
683
|
+
}
|
|
684
|
+
});
|
|
144
685
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}, [store, name]);
|
|
686
|
+
function useCrossTabSync(store, name) {
|
|
687
|
+
useEffect(() => {
|
|
688
|
+
return setupCrossTabSync(store, name);
|
|
689
|
+
}, [store, name]);
|
|
150
690
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}, [store]);
|
|
691
|
+
function useConnectivity(store) {
|
|
692
|
+
useEffect(() => {
|
|
693
|
+
const handleOnline = () => store.getState().setOnline(true);
|
|
694
|
+
const handleOffline = () => store.getState().setOnline(false);
|
|
695
|
+
window.addEventListener("online", handleOnline);
|
|
696
|
+
window.addEventListener("offline", handleOffline);
|
|
697
|
+
return () => {
|
|
698
|
+
window.removeEventListener("online", handleOnline);
|
|
699
|
+
window.removeEventListener("offline", handleOffline);
|
|
700
|
+
};
|
|
701
|
+
}, [store]);
|
|
163
702
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const timer = setInterval(() => {
|
|
193
|
-
setLabel(computeLabel());
|
|
194
|
-
}, 5000);
|
|
195
|
-
return () => clearInterval(timer);
|
|
196
|
-
}, [computeLabel]);
|
|
197
|
-
return label;
|
|
703
|
+
function useLastSynced(store) {
|
|
704
|
+
const lastSyncedAt = useRef(null);
|
|
705
|
+
const [label, setLabel] = useState("Never synced");
|
|
706
|
+
const computeLabel = useCallback(() => {
|
|
707
|
+
if (lastSyncedAt.current === null) return "Never synced";
|
|
708
|
+
const seconds = Math.floor((Date.now() - lastSyncedAt.current) / 1e3);
|
|
709
|
+
if (seconds < 10) return "Just now";
|
|
710
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
711
|
+
return `${Math.floor(seconds / 60)}m ago`;
|
|
712
|
+
}, []);
|
|
713
|
+
useEffect(() => {
|
|
714
|
+
let prevSyncing = store.getState().syncing;
|
|
715
|
+
const unsub = store.subscribe((state) => {
|
|
716
|
+
if (prevSyncing && !state.syncing && !state.error) {
|
|
717
|
+
lastSyncedAt.current = Date.now();
|
|
718
|
+
setLabel(computeLabel());
|
|
719
|
+
}
|
|
720
|
+
prevSyncing = state.syncing;
|
|
721
|
+
});
|
|
722
|
+
return unsub;
|
|
723
|
+
}, [store, computeLabel]);
|
|
724
|
+
useEffect(() => {
|
|
725
|
+
const timer = setInterval(() => {
|
|
726
|
+
setLabel(computeLabel());
|
|
727
|
+
}, 5e3);
|
|
728
|
+
return () => clearInterval(timer);
|
|
729
|
+
}, [computeLabel]);
|
|
730
|
+
return label;
|
|
198
731
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
732
|
+
function useSyncInit(config) {
|
|
733
|
+
const [store, setStore] = useState(null);
|
|
734
|
+
const onDataRef = useRef(config?.onData);
|
|
735
|
+
onDataRef.current = config?.onData;
|
|
736
|
+
useEffect(() => {
|
|
737
|
+
if (!config) {
|
|
738
|
+
setStore(null);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const client = new StarfishClient({
|
|
742
|
+
baseUrl: config.serverUrl,
|
|
743
|
+
auth: config.auth,
|
|
744
|
+
fetch: config.fetch
|
|
745
|
+
});
|
|
746
|
+
const syncManager = new SyncManager({
|
|
747
|
+
client,
|
|
748
|
+
pullPath: config.pullPath,
|
|
749
|
+
pushPath: config.pushPath,
|
|
750
|
+
encryptionSecret: config.encryptionSecret,
|
|
751
|
+
encryptionSalt: config.encryptionSalt,
|
|
752
|
+
onConflict: config.onConflict,
|
|
753
|
+
logger: config.logger,
|
|
754
|
+
validate: config.validate
|
|
755
|
+
});
|
|
756
|
+
const newStore = createStarfishStore({
|
|
757
|
+
name: config.storeName ?? "sync",
|
|
758
|
+
syncManager,
|
|
759
|
+
storage: config.storage,
|
|
760
|
+
// onRemoteUpdate fires only for pull() results, never for local set() writes —
|
|
761
|
+
// so no isRestoring flag is needed.
|
|
762
|
+
onRemoteUpdate: (data) => {
|
|
763
|
+
try {
|
|
764
|
+
onDataRef.current?.(data);
|
|
765
|
+
} catch (err) {
|
|
766
|
+
newStore.setState({
|
|
767
|
+
error: `onData failed: ${err instanceof Error ? err.message : String(err)}`
|
|
768
|
+
});
|
|
216
769
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
syncManager,
|
|
235
|
-
storage: config.storage,
|
|
236
|
-
// onRemoteUpdate fires only for pull() results, never for local set() writes —
|
|
237
|
-
// so no isRestoring flag is needed.
|
|
238
|
-
onRemoteUpdate: (data) => {
|
|
239
|
-
try {
|
|
240
|
-
onDataRef.current?.(data);
|
|
241
|
-
}
|
|
242
|
-
catch (err) {
|
|
243
|
-
newStore.setState({
|
|
244
|
-
error: `onData failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
},
|
|
248
|
-
});
|
|
249
|
-
setStore(newStore);
|
|
250
|
-
// Initial pull — errors are stored in state.error by the pull() action
|
|
251
|
-
newStore.getState().pull().catch(() => { });
|
|
252
|
-
return () => {
|
|
253
|
-
setStore(null);
|
|
254
|
-
};
|
|
255
|
-
// Intentionally depend on serializable config values, not the object reference
|
|
256
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
257
|
-
}, [
|
|
258
|
-
config?.serverUrl,
|
|
259
|
-
config?.pullPath,
|
|
260
|
-
config?.pushPath,
|
|
261
|
-
config?.encryptionSecret,
|
|
262
|
-
config?.encryptionSalt,
|
|
263
|
-
config?.storeName,
|
|
264
|
-
]);
|
|
265
|
-
return store;
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
setStore(newStore);
|
|
773
|
+
newStore.getState().pull().catch(() => {
|
|
774
|
+
});
|
|
775
|
+
return () => {
|
|
776
|
+
setStore(null);
|
|
777
|
+
};
|
|
778
|
+
}, [
|
|
779
|
+
config?.serverUrl,
|
|
780
|
+
config?.pullPath,
|
|
781
|
+
config?.pushPath,
|
|
782
|
+
config?.encryptionSecret,
|
|
783
|
+
config?.encryptionSalt,
|
|
784
|
+
config?.storeName
|
|
785
|
+
]);
|
|
786
|
+
return store;
|
|
266
787
|
}
|
|
788
|
+
export {
|
|
789
|
+
aggregateSyncStatus,
|
|
790
|
+
createStarfishStore,
|
|
791
|
+
deriveSyncStatus,
|
|
792
|
+
subscribeSyncStatus,
|
|
793
|
+
useConnectivity,
|
|
794
|
+
useCrossTabSync,
|
|
795
|
+
useLastSynced,
|
|
796
|
+
useStarfish,
|
|
797
|
+
useStarfishData,
|
|
798
|
+
useSyncInit,
|
|
799
|
+
useSyncStatus
|
|
800
|
+
};
|
|
801
|
+
//# sourceMappingURL=zustand.js.map
|