selfstore 1.8.19 → 1.8.21
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 +107 -12
- package/dist/advanced.d.ts +23 -8
- package/dist/advanced.js +23 -12
- package/dist/advanced.js.map +1 -1
- package/dist/backups.d.ts +4 -4
- package/dist/backups.js +8 -6
- package/dist/{cache-lock-KWJUORTS.js → cache-lock-5RNG6CIK.js} +4 -3
- package/dist/cache-lock-5RNG6CIK.js.map +1 -0
- package/dist/{chunk-SKWHKM25.js → chunk-23UAUYSW.js} +40 -4
- package/dist/chunk-23UAUYSW.js.map +1 -0
- package/dist/chunk-3BWBBGFW.js +29 -0
- package/dist/chunk-3BWBBGFW.js.map +1 -0
- package/dist/{chunk-W22V5CRM.js → chunk-3KAAPYPO.js} +3 -3
- package/dist/{chunk-W22V5CRM.js.map → chunk-3KAAPYPO.js.map} +1 -1
- package/dist/chunk-757ZOSUI.js +27 -0
- package/dist/chunk-757ZOSUI.js.map +1 -0
- package/dist/{chunk-DCEXWSM6.js → chunk-AUH35AAQ.js} +3 -3
- package/dist/{chunk-DCEXWSM6.js.map → chunk-AUH35AAQ.js.map} +1 -1
- package/dist/{chunk-TQH7ZYID.js → chunk-DJZ5S3FI.js} +31 -14
- package/dist/chunk-DJZ5S3FI.js.map +1 -0
- package/dist/{chunk-HOG2IKEP.js → chunk-EBCQ7ZG7.js} +4 -4
- package/dist/{chunk-HOG2IKEP.js.map → chunk-EBCQ7ZG7.js.map} +1 -1
- package/dist/chunk-FJGWTBAL.js +6 -0
- package/dist/chunk-FJGWTBAL.js.map +1 -0
- package/dist/chunk-GW5CYP4C.js +212 -0
- package/dist/chunk-GW5CYP4C.js.map +1 -0
- package/dist/{chunk-MGDXR3RB.js → chunk-HHPJPEEG.js} +6 -5
- package/dist/chunk-HHPJPEEG.js.map +1 -0
- package/dist/{chunk-J5MFXJU6.js → chunk-JTNFZV2B.js} +78 -26
- package/dist/chunk-JTNFZV2B.js.map +1 -0
- package/dist/chunk-JZHWAX34.js +178 -0
- package/dist/chunk-JZHWAX34.js.map +1 -0
- package/dist/chunk-LUNYYH6D.js +223 -0
- package/dist/chunk-LUNYYH6D.js.map +1 -0
- package/dist/{chunk-LKIRWVXM.js → chunk-LYO36VYB.js} +12 -11
- package/dist/chunk-LYO36VYB.js.map +1 -0
- package/dist/{chunk-O3Y6FORN.js → chunk-OY2A5DCB.js} +4 -3
- package/dist/chunk-OY2A5DCB.js.map +1 -0
- package/dist/chunk-QSLHEE2L.js +13 -0
- package/dist/chunk-QSLHEE2L.js.map +1 -0
- package/dist/{chunk-3OBJOMQ2.js → chunk-RREJ2MFN.js} +4 -28
- package/dist/chunk-RREJ2MFN.js.map +1 -0
- package/dist/chunk-XMC67RNM.js +29 -0
- package/dist/chunk-XMC67RNM.js.map +1 -0
- package/dist/{connect-BTVzRJY8.d.ts → connect-Bob2mofK.d.ts} +3 -3
- package/dist/{drive-CY_Vsj7V.d.ts → drive-86CHuTij.d.ts} +63 -2
- package/dist/flows.d.ts +5 -5
- package/dist/flows.js +17 -15
- package/dist/flows.js.map +1 -1
- package/dist/groups.js +4 -3
- package/dist/groups.js.map +1 -1
- package/dist/households.d.ts +1 -1
- package/dist/index.d.ts +66 -10
- package/dist/index.js +73 -18
- package/dist/index.js.map +1 -1
- package/dist/kdf-worker.js +7 -2
- package/dist/kdf-worker.js.map +1 -1
- package/dist/{local-Dh6eGiK9.d.ts → local-8H61XxbL.d.ts} +2 -2
- package/dist/local-XET4G32B.js +7 -0
- package/dist/{local-DPS35F5C.js.map → local-XET4G32B.js.map} +1 -1
- package/dist/{manager-DhoIbJyg.d.ts → manager-Di6lNE4E.d.ts} +1 -1
- package/dist/manager-drive-YLSLQ63O.js +10 -0
- package/dist/{manager-drive-4WLKOOXO.js.map → manager-drive-YLSLQ63O.js.map} +1 -1
- package/dist/s3-43RWNCV3.js +11 -0
- package/dist/s3-43RWNCV3.js.map +1 -0
- package/dist/{s3-0qqIyphl.d.ts → s3-BsbVn3fw.d.ts} +1 -1
- package/dist/{store-Cs6QIzzG.d.ts → store-CIbiNoiE.d.ts} +11 -3
- package/dist/sync.d.ts +7 -6
- package/dist/sync.js +3 -1
- package/dist/webdav-EEQKX5TZ.js +9 -0
- package/dist/webdav-EEQKX5TZ.js.map +1 -0
- package/dist/widgets.d.ts +51 -10
- package/dist/widgets.js +134 -34
- package/dist/widgets.js.map +1 -1
- package/llms.txt +51 -4
- package/package.json +12 -5
- package/skills/selfstore/SKILL.md +115 -0
- package/dist/cache-lock-KWJUORTS.js.map +0 -1
- package/dist/chunk-3OBJOMQ2.js.map +0 -1
- package/dist/chunk-J5MFXJU6.js.map +0 -1
- package/dist/chunk-LKIRWVXM.js.map +0 -1
- package/dist/chunk-MGDXR3RB.js.map +0 -1
- package/dist/chunk-O3Y6FORN.js.map +0 -1
- package/dist/chunk-SKWHKM25.js.map +0 -1
- package/dist/chunk-TKWU52LK.js +0 -607
- package/dist/chunk-TKWU52LK.js.map +0 -1
- package/dist/chunk-TQH7ZYID.js.map +0 -1
- package/dist/local-DPS35F5C.js +0 -6
- package/dist/manager-drive-4WLKOOXO.js +0 -8
package/README.md
CHANGED
|
@@ -90,6 +90,35 @@ const store = await selfstore('crm', { sync: { ids: { contacts: 'uuid' } } });
|
|
|
90
90
|
Everything else about your records is your business: plain JSON in, plain JSON
|
|
91
91
|
out, no proxies, no injected fields, no ORM to fight.
|
|
92
92
|
|
|
93
|
+
## Files
|
|
94
|
+
|
|
95
|
+
Bytes ride in the same store, the same backup and the same merge:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
const id = await store.putFile({ bytes, name: 'scan.pdf', mime: 'application/pdf' });
|
|
99
|
+
store.getFile(id); // { id, name, mime, bytes }
|
|
100
|
+
store.allFiles(); // every file
|
|
101
|
+
await store.removeFile(id);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The id defaults to the **SHA-256 of the bytes**, and that default is load
|
|
105
|
+
bearing. Files are merged by a union on their id, with no clock to order two
|
|
106
|
+
bodies: when two devices hold DIFFERENT bytes under the SAME id, one is kept
|
|
107
|
+
and the other is dropped - silently, because at that level there is nothing to
|
|
108
|
+
compare and nothing to report. A content id makes that impossible, since
|
|
109
|
+
different bytes are a different file and the union keeps both.
|
|
110
|
+
|
|
111
|
+
So `putFile` refuses different bytes under an id you named (identical bytes are
|
|
112
|
+
a no-op), and `{ replace: true }` says you meant it - correct for a body only
|
|
113
|
+
ever written on one device, a silent loser as soon as two devices write it.
|
|
114
|
+
|
|
115
|
+
That union is also what makes a CRDT safe to carry here. Yjs and Automerge
|
|
116
|
+
updates are commutative and idempotent, so storing each update under its
|
|
117
|
+
content id turns the union INTO the CRDT merge: no device's update is lost when
|
|
118
|
+
the copies meet, and folding them is the entire read path.
|
|
119
|
+
`examples/yjs-document.ts` is a complete, typechecked integration - real
|
|
120
|
+
concurrent-edit merging, still no server.
|
|
121
|
+
|
|
93
122
|
## Status and errors are headless
|
|
94
123
|
|
|
95
124
|
The store never ships UI copy. It exposes a status descriptor
|
|
@@ -162,9 +191,9 @@ too, as subpaths of the same install:
|
|
|
162
191
|
|---|---|---|
|
|
163
192
|
| `selfstore` | the simple store + backup files | almost always: start here |
|
|
164
193
|
| `selfstore/flows` | the user journeys (connect a destination, share panel, join an invitation) as headless, tested state machines | you are building the connect/share/join UI and want the ordering and failure rules - one popup per gesture, password proven before attach, merge by default - already right |
|
|
165
|
-
| `selfstore/widgets` | the same journeys as drop-in web components (`<selfstore-storage>` for the whole journey in one tag
|
|
194
|
+
| `selfstore/widgets` | the same journeys as drop-in web components (`<selfstore-storage>` for the whole journey in one tag - it opens on a **full-screen first-run gate**, so reach for `<selfstore-destination>` or `<selfstore-status>` instead on a page that must work before the user answers; also `<selfstore-connect>`, `<selfstore-gate>`, `<selfstore-account>`, `<selfstore-share>`, `<selfstore-join>` on their own) - framework-free, themable via CSS custom properties and `::part()`, reworded/localized via a `labels` map | you want the screens ready-made and your app's look on top |
|
|
166
195
|
| `selfstore/advanced` | the pull-model store (`createLocalStore`), custom `BackupTarget`s, caches, headless status derivation, functional codec | your state lives in its own reactive model (Svelte runes, Redux), or you are writing a destination (S3, your KV...) |
|
|
167
|
-
| `selfstore/groups` | passwordless group encryption: per-member identities, sealed envelopes, signed membership manifests | several PEOPLE share one encrypted store without a shared password ([PEERS.md](./PEERS.md)) |
|
|
196
|
+
| `selfstore/groups` **(experimental)** | passwordless group encryption: per-member identities, sealed envelopes, signed membership manifests | several PEOPLE share one encrypted store without a shared password ([PEERS.md](./PEERS.md)) - see [Stability](#stability) for what experimental costs you |
|
|
168
197
|
| `selfstore/sync` | the bare merge engine (HLC + per-collection strategies, no CRDT runtime) | you only want the algorithm inside your own persistence ([SYNC docs in SPEC.md](./SPEC.md)) |
|
|
169
198
|
|
|
170
199
|
`store.advanced` on the simple store IS the `selfstore/advanced` store - same
|
|
@@ -228,8 +257,9 @@ losing value is surfaced in the sync journal's conflicts, not merged. In
|
|
|
228
257
|
`lww-map`, a record deleted on one replica while another edits one of its
|
|
229
258
|
fields resurrects with the surviving side's fields only. Change detection
|
|
230
259
|
hashes content with 32-bit FNV-1a: a collision (~2^-32 per edit) would miss an
|
|
231
|
-
edit. If concurrent-edit losslessness is your requirement,
|
|
232
|
-
document (Yjs/Automerge)
|
|
260
|
+
edit. If concurrent-edit losslessness is your requirement, keep a real CRDT
|
|
261
|
+
document (Yjs/Automerge) and let the store carry it - see "Files" below;
|
|
262
|
+
`examples/yjs-document.ts` is the whole integration.
|
|
233
263
|
|
|
234
264
|
## Sharing between people: peers
|
|
235
265
|
|
|
@@ -243,13 +273,35 @@ backup of the group), gossip is transitive (a star around one member is
|
|
|
243
273
|
enough). Peer problems are recorded per peer on `state.peers` and never gate
|
|
244
274
|
your own saves.
|
|
245
275
|
|
|
276
|
+
On Google Drive, the file work that topology needs is in `driveTarget`
|
|
277
|
+
(`selfstore/advanced`): `createCompanion` mints a copy file next to the backup,
|
|
278
|
+
`share` / `unshare` publish it on a link and take it back, `owner` says whose
|
|
279
|
+
Drive a copy lives on, and `secondary({ auth, kv }, fileId)` is a read-write target over
|
|
280
|
+
it whose lifecycle is its own - dropping a shared copy leaves the user's real
|
|
281
|
+
backup and their Drive session alone, where `preview`'s `disconnect` would end
|
|
282
|
+
both. Every failure carries a code, so a refused grant and a lost session read
|
|
283
|
+
differently.
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
const { fileId } = await driveTarget.createCompanion({ auth, fileName: 'my copy.zip' });
|
|
287
|
+
await driveTarget.share({ auth, fileId }); // link-readable, ciphertext only
|
|
288
|
+
await store.advanced.attachMirror(driveTarget.secondary({ auth, kv }, fileId),
|
|
289
|
+
{ password: linkKey }); // republished on every save
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
What stays yours: READING another member's copy. The `drive.file` scope only
|
|
293
|
+
sees files this app created or the user picked, so fetching someone else's
|
|
294
|
+
link-shared file needs either the Google Picker (a gesture per file, plus a
|
|
295
|
+
third-party script) or a relay you host - both app decisions, one of them a
|
|
296
|
+
server. `attachPeer` takes whatever `load()` you can provide.
|
|
297
|
+
|
|
246
298
|
Group crypto, two options: ONE shared passphrase (exchanged once out of band),
|
|
247
|
-
or **passwordless groups** (`selfstore/groups
|
|
248
|
-
copy Ed25519-signed by its
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
[PEERS.md](./PEERS.md); the trust
|
|
252
|
-
[THREAT-MODEL.md](./THREAT-MODEL.md).
|
|
299
|
+
or **passwordless groups** (`selfstore/groups`, experimental - see
|
|
300
|
+
[Stability](#stability)): a keypair per member, every copy Ed25519-signed by its
|
|
301
|
+
author and sealed per recipient (X25519 envelopes), membership governed by an
|
|
302
|
+
admin-signed manifest with rollback protection. The full model, the failure
|
|
303
|
+
table and the operational notes live in [PEERS.md](./PEERS.md); the trust
|
|
304
|
+
analysis in [THREAT-MODEL.md](./THREAT-MODEL.md).
|
|
253
305
|
|
|
254
306
|
## Destinations
|
|
255
307
|
|
|
@@ -273,6 +325,12 @@ and retried. `examples/custom-target.ts` is a complete one in ~30 lines.
|
|
|
273
325
|
> Drive connection needs a small refresh-token broker. WebDAV and S3 need only
|
|
274
326
|
> a CORS rule on the server/bucket for your app origin.
|
|
275
327
|
|
|
328
|
+
Opt-in is about the bytes too: WebDAV and S3 arrive on the gesture that opens
|
|
329
|
+
them, so an app offering only Drive and a disk file never ships the SigV4
|
|
330
|
+
signer or the WebDAV client (4.4 KB gzipped it would otherwise carry). Drive and
|
|
331
|
+
the disk file stay eager on purpose - both open a picker inside the transient
|
|
332
|
+
activation of the user's click, which an `await import()` can spend.
|
|
333
|
+
|
|
276
334
|
For resilience, `store.addReplica(target)` keeps a SECOND destination in sync
|
|
277
335
|
with the same backup - Drive as the primary, an S3 bucket as the synced copy -
|
|
278
336
|
so losing one home never loses the data. For locked-down deployments,
|
|
@@ -348,6 +406,11 @@ const persistence = {
|
|
|
348
406
|
the app cannot forget to check.
|
|
349
407
|
- The full analysis - assets, trust boundaries, threats and explicit non-goals
|
|
350
408
|
(XSS, rollback replay, metadata) - is in [THREAT-MODEL.md](./THREAT-MODEL.md).
|
|
409
|
+
- WHY each of those choices, with the alternative it rejected and where its
|
|
410
|
+
author would attack it first, is in
|
|
411
|
+
[CRYPTO-RATIONALE.md](./CRYPTO-RATIONALE.md). Written to be argued with:
|
|
412
|
+
review is wanted, and a case that a decision is wrong is worth more here than
|
|
413
|
+
a bug report.
|
|
351
414
|
|
|
352
415
|
## When not to use selfstore
|
|
353
416
|
|
|
@@ -364,8 +427,10 @@ data-only (connections and mode switches apply to other tabs on reload;
|
|
|
364
427
|
`multiTab: false` opts out); deletion tombstones grow unless you opt in to
|
|
365
428
|
compaction (`tombstoneHorizonMs` on the advanced store - safe only if the
|
|
366
429
|
horizon exceeds the longest a device stays offline); binary files merge by id
|
|
367
|
-
union
|
|
368
|
-
|
|
430
|
+
union and their DELETIONS do not propagate (no tombstones), so a device that
|
|
431
|
+
was offline re-contributes files another device removed - tie a file's lifetime
|
|
432
|
+
to a record and let the record's deletion drive the cleanup. In Vite DEV, if
|
|
433
|
+
the KDF worker 404s under dependency pre-bundling,
|
|
369
434
|
add `optimizeDeps: { exclude: ['selfstore'] }`.
|
|
370
435
|
|
|
371
436
|
## Testing your integration
|
|
@@ -380,6 +445,25 @@ selfstore tests itself.
|
|
|
380
445
|
|
|
381
446
|
## For AI agents
|
|
382
447
|
|
|
448
|
+
A **skill** ships in the package, so it is on disk at the version installed and
|
|
449
|
+
fires when an assistant writes selfstore code rather than waiting to be pointed
|
|
450
|
+
at a URL:
|
|
451
|
+
|
|
452
|
+
```sh
|
|
453
|
+
cp -r node_modules/selfstore/skills/selfstore ~/.claude/skills/
|
|
454
|
+
|
|
455
|
+
# or, to follow the repository
|
|
456
|
+
claude plugin marketplace add selfstoredev/selfstore
|
|
457
|
+
claude plugin install selfstore@selfstore
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
[`skills/selfstore/SKILL.md`](./skills/selfstore/SKILL.md) is plain markdown
|
|
461
|
+
with no assistant-specific instruction in the body, so it doubles as an
|
|
462
|
+
`AGENTS.md` for anything else that reads one. It carries the decision (is this
|
|
463
|
+
the right tool at all) and the five things a first integration gets wrong, and
|
|
464
|
+
hands over to the reference below for the rest. `npm run skill` fails when it
|
|
465
|
+
names something this package no longer has.
|
|
466
|
+
|
|
383
467
|
`llms.txt` at the package root summarizes the API with canonical snippets, and
|
|
384
468
|
[RECIPES.md](./RECIPES.md) has copy-paste tasks (the `examples/` folder holds
|
|
385
469
|
the same mini-apps, typechecked). Rules of thumb: every record needs a
|
|
@@ -403,6 +487,17 @@ an app compiling or alters what it already does - a new default counts. Releases
|
|
|
403
487
|
are deliberate and batched, not one per merge. The rules, and what the version
|
|
404
488
|
history reads like, are in [RELEASING.md](./RELEASING.md).
|
|
405
489
|
|
|
490
|
+
**One exception, named rather than hidden: `selfstore/groups` is
|
|
491
|
+
experimental.** Its exports may change shape, or be withdrawn, in a MINOR
|
|
492
|
+
release; every other entry waits for a major. The reason is evidence: no
|
|
493
|
+
application has shipped that API, so it has never been tested by a second pair
|
|
494
|
+
of hands, and it is the most security-sensitive surface here - a contract
|
|
495
|
+
nobody has exercised is a promise, not a fact. What it does NOT put at risk is
|
|
496
|
+
anyone's data: group mode is format generation 2, specified in
|
|
497
|
+
[SPEC.md](./SPEC.md) section 12 with a canonical test vector, and it keeps the
|
|
498
|
+
guarantee above. An app that adopts it today may have to recompile; its users'
|
|
499
|
+
files stay readable forever either way.
|
|
500
|
+
|
|
406
501
|
Published versions are never unpublished. A superseded one stays installable;
|
|
407
502
|
the tool for a release that should not be used is `npm deprecate`, and the tool
|
|
408
503
|
for a broken one is the next release.
|
package/dist/advanced.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { B as BackupTarget } from './store-
|
|
2
|
-
export { A as AuthExpiredError, a as BuiltinTargetKind, E as EncodeOptions, H as Header, K as KdfParams, L as LocalStore, b as LocalStoreOptions, c as LocalStoreState, M as MirrorState, d as Mode, e as PasswordCheck, f as PasswordPolicy, g as PasswordRequirement, P as PeerSource, h as PeerState, R as RESERVED_COLLECTION_PREFIX, i as RESERVED_STORE_MODES, j as RecipientStanza, k as ReplicaState, l as SelfstoreError, m as SelfstoreErrorCode, n as Severity, o as Snapshot, p as SnapshotFile, q as StatusAction, r as StatusDescriptor, s as StatusInput, t as StorageState, S as StoreError, u as StoreGroupConfig, v as SyncJournalEntry, w as SyncSource, T as TargetKind, x as checkPasswordPolicy, y as createLocalStore, z as deriveStatus, C as errorLabelKey, D as isAuthExpired, F as isReservedStoreMode, G as isSelfstoreError } from './store-
|
|
1
|
+
import { B as BackupTarget } from './store-CIbiNoiE.js';
|
|
2
|
+
export { A as AuthExpiredError, a as BuiltinTargetKind, E as EncodeOptions, H as Header, K as KdfParams, L as LocalStore, b as LocalStoreOptions, c as LocalStoreState, M as MirrorState, d as Mode, e as PasswordCheck, f as PasswordPolicy, g as PasswordRequirement, P as PeerSource, h as PeerState, R as RESERVED_COLLECTION_PREFIX, i as RESERVED_STORE_MODES, j as RecipientStanza, k as ReplicaState, l as SelfstoreError, m as SelfstoreErrorCode, n as Severity, o as Snapshot, p as SnapshotFile, q as StatusAction, r as StatusDescriptor, s as StatusInput, t as StorageState, S as StoreError, u as StoreGroupConfig, v as SyncJournalEntry, w as SyncSource, T as TargetKind, x as checkPasswordPolicy, y as createLocalStore, z as deriveStatus, C as errorLabelKey, D as isAuthExpired, F as isReservedStoreMode, G as isSelfstoreError } from './store-CIbiNoiE.js';
|
|
3
3
|
import { K as KV } from './cache-ilsK3Tki.js';
|
|
4
4
|
export { C as CachedFile, L as LocalCache, e as LockableCache, i as indexedDbCache, f as isLockableCache, m as memoryCache } from './cache-ilsK3Tki.js';
|
|
5
|
-
export { G as GisDriveAuthOptions, a as GisTokenPersistence, d as datedName, e as exportSnapshot, g as gisDriveAuth, i as importSnapshot, b as inspect, c as isEncrypted, p as pickFromDisk, s as saveToDisk } from './local-
|
|
6
|
-
import { c as connect$2, f as fromSession$2, a as connect$3, b as fromSession$3, p as peer } from './s3-
|
|
7
|
-
export { S as S3Config, d as S3ConnectOptions, W as WebdavConfig, e as WebdavConnectOptions, g as WebdavPeerOptions } from './s3-
|
|
8
|
-
import { c as connect$1, f as fromSession$1, p as preview, b as adopt, d as findOrCreateOwnFile, l as listBackups, e as createBackup, g as deleteBackup, r as renameBackup, h as account } from './drive-
|
|
9
|
-
export {
|
|
5
|
+
export { G as GisDriveAuthOptions, a as GisTokenPersistence, d as datedName, e as exportSnapshot, g as gisDriveAuth, i as importSnapshot, b as inspect, c as isEncrypted, p as pickFromDisk, s as saveToDisk } from './local-8H61XxbL.js';
|
|
6
|
+
import { c as connect$2, f as fromSession$2, a as connect$3, b as fromSession$3, p as peer } from './s3-BsbVn3fw.js';
|
|
7
|
+
export { S as S3Config, d as S3ConnectOptions, W as WebdavConfig, e as WebdavConnectOptions, g as WebdavPeerOptions } from './s3-BsbVn3fw.js';
|
|
8
|
+
import { c as connect$1, f as fromSession$1, p as preview, b as adopt, d as findOrCreateOwnFile, l as listBackups, e as createBackup, g as deleteBackup, r as renameBackup, h as account, i as createCompanion, s as share, u as unshare, o as owner, j as secondary } from './drive-86CHuTij.js';
|
|
9
|
+
export { k as DriveAccount, a as DriveAuth, m as DriveBackupInfo, D as DriveOptions } from './drive-86CHuTij.js';
|
|
10
10
|
import './sync.js';
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -146,7 +146,17 @@ declare const s3Target: {
|
|
|
146
146
|
* and the backup-management ops (list the app's files with dates, create a
|
|
147
147
|
* named empty backup, rename one, delete one for good) - the raw pieces a "my
|
|
148
148
|
* backups" panel builds on. `account()` names WHICH Google account holds them,
|
|
149
|
-
* because a brand name is not an address.
|
|
149
|
+
* because a brand name is not an address.
|
|
150
|
+
*
|
|
151
|
+
* The last five are the Drive side of SHARING, on companion files the app
|
|
152
|
+
* creates next to the backup: `createCompanion` mints one, `share` publishes it
|
|
153
|
+
* on a link, `unshare` makes it private again, `owner` says whose Drive it
|
|
154
|
+
* lives on, and `secondary` is a read-write target over it whose lifecycle is
|
|
155
|
+
* its own - detaching a shared copy leaves the user's real backup and their
|
|
156
|
+
* Drive session untouched. That is what a ShareBackend needs from Drive and
|
|
157
|
+
* what every app was rewriting; what stays yours is reading ANOTHER account's
|
|
158
|
+
* file, which the drive.file scope cannot do without a Picker gesture or a
|
|
159
|
+
* relay you host. */
|
|
150
160
|
declare const driveTarget: {
|
|
151
161
|
connect: typeof connect$1;
|
|
152
162
|
fromSession: typeof fromSession$1;
|
|
@@ -158,6 +168,11 @@ declare const driveTarget: {
|
|
|
158
168
|
deleteBackup: typeof deleteBackup;
|
|
159
169
|
renameBackup: typeof renameBackup;
|
|
160
170
|
account: typeof account;
|
|
171
|
+
createCompanion: typeof createCompanion;
|
|
172
|
+
share: typeof share;
|
|
173
|
+
unshare: typeof unshare;
|
|
174
|
+
owner: typeof owner;
|
|
175
|
+
secondary: typeof secondary;
|
|
161
176
|
FILE_ID_KEY: string;
|
|
162
177
|
};
|
|
163
178
|
|
package/dist/advanced.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { peer, fromSession as fromSession$3, connect as connect$3 } from './chunk-JZHWAX34.js';
|
|
2
|
+
import { fromSession as fromSession$2, connect as connect$2 } from './chunk-LUNYYH6D.js';
|
|
3
|
+
export { RESERVED_STORE_MODES, createLocalStore, deriveStatus, gisDriveAuth, indexedDbCache, isLockableCache, isReservedStoreMode, memoryCache } from './chunk-DJZ5S3FI.js';
|
|
4
|
+
export { datedName, pickFromDisk, saveToDisk } from './chunk-AUH35AAQ.js';
|
|
5
|
+
import './chunk-QSLHEE2L.js';
|
|
6
|
+
import { openExisting, isOpenSupported, isSupported, fromSession as fromSession$1, connect as connect$1 } from './chunk-GW5CYP4C.js';
|
|
7
|
+
import { FILE_ID_KEY, secondary, owner, unshare, share, createCompanion, account, renameBackup, deleteBackup, createBackup, listBackups, findOrCreateOwnFile, adopt, preview, fromSession, connect } from './chunk-JTNFZV2B.js';
|
|
8
|
+
import './chunk-757ZOSUI.js';
|
|
9
|
+
export { RESERVED_COLLECTION_PREFIX, checkPasswordPolicy, exportSnapshot, importSnapshot, inspect, isEncrypted } from './chunk-HHPJPEEG.js';
|
|
6
10
|
import './chunk-QBCU7OOP.js';
|
|
7
|
-
import './chunk-
|
|
8
|
-
|
|
9
|
-
import './chunk-
|
|
11
|
+
import './chunk-OY2A5DCB.js';
|
|
12
|
+
import './chunk-RREJ2MFN.js';
|
|
13
|
+
import './chunk-23UAUYSW.js';
|
|
14
|
+
import './chunk-FJGWTBAL.js';
|
|
15
|
+
export { AuthExpiredError, SelfstoreError, errorLabelKey, isAuthExpired, isSelfstoreError } from './chunk-XMC67RNM.js';
|
|
10
16
|
|
|
11
17
|
// src/persistence/installation.ts
|
|
12
18
|
function isSafari(ua) {
|
|
@@ -46,13 +52,13 @@ var fileTarget = {
|
|
|
46
52
|
openExisting
|
|
47
53
|
};
|
|
48
54
|
var webdavTarget = {
|
|
49
|
-
connect:
|
|
50
|
-
fromSession:
|
|
55
|
+
connect: connect$3,
|
|
56
|
+
fromSession: fromSession$3,
|
|
51
57
|
peer
|
|
52
58
|
};
|
|
53
59
|
var s3Target = {
|
|
54
|
-
connect:
|
|
55
|
-
fromSession:
|
|
60
|
+
connect: connect$2,
|
|
61
|
+
fromSession: fromSession$2
|
|
56
62
|
};
|
|
57
63
|
var driveTarget = {
|
|
58
64
|
connect,
|
|
@@ -65,6 +71,11 @@ var driveTarget = {
|
|
|
65
71
|
deleteBackup,
|
|
66
72
|
renameBackup,
|
|
67
73
|
account,
|
|
74
|
+
createCompanion,
|
|
75
|
+
share,
|
|
76
|
+
unshare,
|
|
77
|
+
owner,
|
|
78
|
+
secondary,
|
|
68
79
|
FILE_ID_KEY
|
|
69
80
|
};
|
|
70
81
|
|
package/dist/advanced.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/persistence/installation.ts","../src/entries/advanced.ts"],"names":["connect","fromSession"],"mappings":";;;;;;;;;;;AAuCA,SAAS,SAAS,EAAA,EAAqB;AACtC,EAAA,OAAO,UAAA,CAAW,IAAA,CAAK,EAAE,CAAA,IAAK,CAAC,uBAAA,CAAwB,IAAA,CAAK,EAAE,CAAA,IAAK,CAAC,eAAA,CAAgB,IAAA,CAAK,EAAE,CAAA;AAC5F;AAEA,SAAS,YAAA,GAAwB;AAChC,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,EAAa,OAAO,KAAA;AAC1C,EAAA,MAAM,WAAA,GAAc,MAAA,CAAO,UAAA,GAAa,4BAA4B,GAAG,OAAA,KAAY,IAAA;AAEnF,EAAA,MAAM,GAAA,GAAO,UAAmD,UAAA,KAAe,IAAA;AAC/E,EAAA,OAAO,WAAA,IAAe,GAAA;AACvB;AAOO,SAAS,aAAA,GAA+B;AAC9C,EAAA,IAAI,OAAO,SAAA,KAAc,WAAA,IAAe,OAAO,WAAW,WAAA,EAAa;AACtE,IAAA,OAAO,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAA,EAAW,KAAA,EAAM;AAAA,EACzC;AACA,EAAA,MAAM,YAAY,YAAA,EAAa;AAC/B,EAAA,IAAI,CAAC,QAAA,CAAS,SAAA,CAAU,SAAS,KAAK,SAAA,EAAW;AAChD,IAAA,OAAO,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAA,EAAU;AAAA,EAClC;AACA,EAAA,MAAM,QAAQ,kBAAA,CAAmB,IAAA,CAAK,UAAU,SAAS,CAAA,IAAK,UAAU,cAAA,GAAiB,CAAA;AACzF,EAAA,OAAO;AAAA,IACN,IAAA,EAAM,mBAAA;AAAA,IACN,MAAA,EAAQ,QAAQ,wBAAA,GAA2B,iBAAA;AAAA,IAC3C,SAAA,EAAW;AAAA,GACZ;AACD;;;AC8DO,IAAM,UAAA,GAAa;AAAA,EACzB,OAAA,EAASA,SAAAA;AAAA,EACT,WAAA,EAAaC,aAAAA;AAAA,EACb,WAAA;AAAA,EACA,eAAA;AAAA;AAAA;AAAA;AAAA,EAIA;AACD;AAKO,IAAM,YAAA,GAAe;AAAA,EAC3B,OAAA,EAASD,QAAAA;AAAA,EACT,WAAA,EAAaC,YAAAA;AAAA,EACb;AACD;AAMO,IAAM,QAAA,GAAW;AAAA,EACvB,OAAA,EAASD,QAAAA;AAAA,EACT,WAAA,EAAaC;AACd;AAQO,IAAM,WAAA,GAAc;AAAA,EAC1B,OAAA;AAAA,EACA,WAAA;AAAA,EACA,OAAA;AAAA,EACA,KAAA;AAAA,EACA,mBAAA;AAAA,EACA,WAAA;AAAA,EACA,YAAA;AAAA,EACA,YAAA;AAAA,EACA,YAAA;AAAA,EACA,OAAA;AAAA,EACA;AACD","file":"advanced.js","sourcesContent":["/**\n * Whether this browser will keep the local store, and what the user can do\n * about it.\n *\n * One browser does not merely inconvenience a local-first app, it DELETES its\n * data: Safari erases script-writable storage after seven days of browser use\n * without interaction with the site. Someone who closes for three weeks comes\n * back to an empty app, and nothing warned them - which is also the moment they\n * notice it last.\n *\n * Adding the app to the Dock or the Home Screen switches its storage to\n * persistent mode, which WebKit exempts from its eviction triggers. WebKit does\n * not write anywhere that this neutralises the inactivity rule by name, so this\n * module reports an advisory, never a guarantee.\n *\n * The library ships no user-facing copy: it names the situation, the host writes\n * the sentence.\n */\n\n/** What stands between this browser and keeping the data. */\nexport type StorageRisk =\n\t/** Nothing known against it: the store survives until the user clears it. */\n\t| 'none'\n\t/** Data is erased when the tab closes (private browsing). Refuse to set up here. */\n\t| 'ephemeral'\n\t/** Erased after a stretch of not opening the app, unless it is installed. */\n\t| 'evicted-when-idle';\n\nexport interface StorageAdvice {\n\trisk: StorageRisk;\n\t/** The gesture that removes the risk, when there is one. Stable key; the\n\t * host owns the wording and the platform-specific instructions. */\n\tremedy?: 'install-to-dock' | 'install-to-home-screen';\n\t/** Already running as an installed app, so the remedy is done. */\n\tinstalled: boolean;\n}\n\n/** Real Safari, not Chrome or Edge on macOS - all three carry \"Safari\" in the\n * user agent, and only Safari carries none of the others' tokens. */\nfunction isSafari(ua: string): boolean {\n\treturn /Safari\\//.test(ua) && !/Chrome|Chromium|Edg\\//.test(ua) && !/OPR\\/|Android/.test(ua);\n}\n\nfunction isStandalone(): boolean {\n\tif (typeof window === 'undefined') return false;\n\tconst displayMode = window.matchMedia?.('(display-mode: standalone)')?.matches === true;\n\t// iOS and iPadOS kept their own flag, older than display-mode.\n\tconst ios = (navigator as Navigator & { standalone?: boolean }).standalone === true;\n\treturn displayMode || ios;\n}\n\n/**\n * What this browser will do with the local store, and the one gesture that\n * changes it. Call it before setting up, and again in whatever screen owns the\n * data: the advice disappears on its own once the remedy is applied.\n */\nexport function storageAdvice(): StorageAdvice {\n\tif (typeof navigator === 'undefined' || typeof window === 'undefined') {\n\t\treturn { risk: 'none', installed: false };\n\t}\n\tconst installed = isStandalone();\n\tif (!isSafari(navigator.userAgent) || installed) {\n\t\treturn { risk: 'none', installed };\n\t}\n\tconst touch = /iPhone|iPad|iPod/.test(navigator.userAgent) || navigator.maxTouchPoints > 1;\n\treturn {\n\t\trisk: 'evicted-when-idle',\n\t\tremedy: touch ? 'install-to-home-screen' : 'install-to-dock',\n\t\tinstalled: false\n\t};\n}\n","/**\n * selfstore/advanced - the pull-model store and the machinery under it.\n *\n * The simple `selfstore()` facade (package root) owns its data. This entry\n * inverts that: with createLocalStore your app owns the data and hands the\n * store a gather/apply pair - use it when state lives in your own reactive\n * model (Svelte runes, Redux, signals) and the facade's collections would be\n * a second copy. Everything here also backs `store.advanced` on the simple\n * store: same machinery, same instance, so the two styles compose.\n *\n * Also here: the durable destinations (disk file, Google Drive, WebDAV) as\n * connectable targets, the BackupTarget contract for writing your own, the\n * caches, the headless status derivation, and the functional codec for the\n * backup file format (the fluent one lives at the package root).\n */\n\n// --- The pull-model store: your app owns the data ---\nexport {\n\tcreateLocalStore,\n\ttype LocalStore,\n\ttype LocalStoreOptions,\n\ttype LocalStoreState,\n\ttype StoreError,\n\ttype Mode,\n\ttype TargetKind,\n\ttype SyncSource,\n\ttype SyncJournalEntry,\n\ttype PeerState,\n\ttype MirrorState,\n\ttype ReplicaState,\n\ttype StoreGroupConfig\n} from '../persistence/store';\n\n// --- On-device caches (the store's working copy) ---\nexport {\n\tindexedDbCache,\n\tmemoryCache,\n\tisLockableCache,\n\ttype LocalCache,\n\ttype LockableCache,\n\ttype CachedFile,\n\ttype KV\n} from '../persistence/cache';\n\n// --- Headless status: map store state to your UI copy ---\nexport {\n\tderiveStatus,\n\ttype StatusDescriptor,\n\ttype StatusInput,\n\ttype Severity,\n\ttype StorageState,\n\ttype StatusAction\n} from '../persistence/status';\n\n// --- Will this browser keep the data, and what can the user do about it ---\nexport { storageAdvice, type StorageAdvice, type StorageRisk } from '../persistence/installation';\n\n// --- The target contract (write your own destination) ---\nexport {\n\tRESERVED_STORE_MODES,\n\tisReservedStoreMode,\n\ttype BackupTarget,\n\ttype BuiltinTargetKind,\n\ttype PeerSource\n} from '../persistence/target';\n\n// --- The backup file format, functional style (fluent lives at the root) ---\nexport {\n\texportSnapshot,\n\timportSnapshot,\n\tinspect,\n\tisEncrypted,\n\tRESERVED_COLLECTION_PREFIX,\n\ttype Snapshot,\n\ttype SnapshotFile,\n\ttype Header,\n\ttype KdfParams,\n\ttype RecipientStanza,\n\ttype EncodeOptions\n} from '../selfstore';\n\n// --- Password strength policy (the store enforces it; the UI previews it) ---\nexport {\n\tcheckPasswordPolicy,\n\ttype PasswordPolicy,\n\ttype PasswordCheck,\n\ttype PasswordRequirement\n} from '../selfstore';\n\n// --- The full error contract (AuthExpiredError is what a custom target or\n// DriveAuth THROWS to signal genuine access loss; everything else it\n// throws is treated as transient and retried) ---\nexport {\n\tSelfstoreError,\n\tAuthExpiredError,\n\tisSelfstoreError,\n\tisAuthExpired,\n\terrorLabelKey,\n\ttype SelfstoreErrorCode\n} from '../selfstore';\n\n// --- Durable destinations (opt-in), each implementing BackupTarget ---\n// Explicit namespace objects: every member is deliberately public, so a future\n// internal helper in a target module can never leak into the contract.\nimport {\n\tconnect as fileConnect,\n\tfromSession as fileFromSession,\n\tisSupported as fileIsSupported,\n\tisOpenSupported as fileIsOpenSupported,\n\topenExisting as fileOpenExisting\n} from '../persistence/targets/file';\nimport {\n\tconnect as webdavConnect,\n\tfromSession as webdavFromSession,\n\tpeer as webdavPeer\n} from '../persistence/targets/webdav';\nimport { connect as s3Connect, fromSession as s3FromSession } from '../persistence/targets/s3';\nimport {\n\tconnect as driveConnect,\n\tfromSession as driveFromSession,\n\tpreview as drivePreview,\n\tadopt as driveAdopt,\n\tfindOrCreateOwnFile as driveFindOrCreateOwnFile,\n\tlistBackups as driveListBackups,\n\tcreateBackup as driveCreateBackup,\n\tdeleteBackup as driveDeleteBackup,\n\trenameBackup as driveRenameBackup,\n\taccount as driveAccount,\n\tFILE_ID_KEY as DRIVE_FILE_ID_KEY\n} from '../persistence/targets/drive';\n\n/** Disk-file destination (File System Access, Chromium). */\nexport const fileTarget = {\n\tconnect: fileConnect,\n\tfromSession: fileFromSession,\n\tisSupported: fileIsSupported,\n\tisOpenSupported: fileIsOpenSupported,\n\t/** Let the user point at an EXISTING backup file and adopt it as the\n\t * destination. Gate on isOpenSupported(): unlike creating, this has no\n\t * download fallback. */\n\topenExisting: fileOpenExisting\n};\n\n/** WebDAV destination (Nextcloud, ownCloud, any server the user controls), plus\n * `peer()`: a read-only source over another member's shared WebDAV link, so a\n * group can share read-write without Google Drive (see store.attachPeer). */\nexport const webdavTarget = {\n\tconnect: webdavConnect,\n\tfromSession: webdavFromSession,\n\tpeer: webdavPeer\n};\n\n/** S3-compatible destination (Amazon S3, Cloudflare R2, Backblaze B2, MinIO):\n * the browser signs each request with SigV4 and writes one object on a bucket\n * the user controls, no vendor broker. The bucket only ever holds the same\n * end-to-end-encrypted .selfstore bytes. */\nexport const s3Target = {\n\tconnect: s3Connect,\n\tfromSession: s3FromSession\n};\n\n/** Google Drive destination, plus the shared-file primitives (preview a specific\n * file, adopt it as this device's backup, find-or-create the user's own file)\n * and the backup-management ops (list the app's files with dates, create a\n * named empty backup, rename one, delete one for good) - the raw pieces a \"my\n * backups\" panel builds on. `account()` names WHICH Google account holds them,\n * because a brand name is not an address. */\nexport const driveTarget = {\n\tconnect: driveConnect,\n\tfromSession: driveFromSession,\n\tpreview: drivePreview,\n\tadopt: driveAdopt,\n\tfindOrCreateOwnFile: driveFindOrCreateOwnFile,\n\tlistBackups: driveListBackups,\n\tcreateBackup: driveCreateBackup,\n\tdeleteBackup: driveDeleteBackup,\n\trenameBackup: driveRenameBackup,\n\taccount: driveAccount,\n\tFILE_ID_KEY: DRIVE_FILE_ID_KEY\n};\n\nexport type { FileConnectOptions } from '../persistence/targets/file';\nexport type {\n\tDriveAuth,\n\tDriveOptions,\n\tDriveBackupInfo,\n\tDriveAccount\n} from '../persistence/targets/drive';\nexport type {\n\tWebdavConfig,\n\tWebdavConnectOptions,\n\tWebdavPeerOptions\n} from '../persistence/targets/webdav';\nexport type { S3Config, S3ConnectOptions } from '../persistence/targets/s3';\nexport {\n\tgisDriveAuth,\n\ttype GisDriveAuthOptions,\n\ttype GisTokenPersistence\n} from '../persistence/targets/drive-auth-gis';\nexport { saveToDisk, pickFromDisk, datedName } from '../selfstore/targets/local';\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/persistence/installation.ts","../src/entries/advanced.ts"],"names":["connect","fromSession"],"mappings":";;;;;;;;;;;;;;;;;AAuCA,SAAS,SAAS,EAAA,EAAqB;AACtC,EAAA,OAAO,UAAA,CAAW,IAAA,CAAK,EAAE,CAAA,IAAK,CAAC,uBAAA,CAAwB,IAAA,CAAK,EAAE,CAAA,IAAK,CAAC,eAAA,CAAgB,IAAA,CAAK,EAAE,CAAA;AAC5F;AAEA,SAAS,YAAA,GAAwB;AAChC,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,EAAa,OAAO,KAAA;AAC1C,EAAA,MAAM,WAAA,GAAc,MAAA,CAAO,UAAA,GAAa,4BAA4B,GAAG,OAAA,KAAY,IAAA;AAEnF,EAAA,MAAM,GAAA,GAAO,UAAmD,UAAA,KAAe,IAAA;AAC/E,EAAA,OAAO,WAAA,IAAe,GAAA;AACvB;AAOO,SAAS,aAAA,GAA+B;AAC9C,EAAA,IAAI,OAAO,SAAA,KAAc,WAAA,IAAe,OAAO,WAAW,WAAA,EAAa;AACtE,IAAA,OAAO,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAA,EAAW,KAAA,EAAM;AAAA,EACzC;AACA,EAAA,MAAM,YAAY,YAAA,EAAa;AAC/B,EAAA,IAAI,CAAC,QAAA,CAAS,SAAA,CAAU,SAAS,KAAK,SAAA,EAAW;AAChD,IAAA,OAAO,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAA,EAAU;AAAA,EAClC;AACA,EAAA,MAAM,QAAQ,kBAAA,CAAmB,IAAA,CAAK,UAAU,SAAS,CAAA,IAAK,UAAU,cAAA,GAAiB,CAAA;AACzF,EAAA,OAAO;AAAA,IACN,IAAA,EAAM,mBAAA;AAAA,IACN,MAAA,EAAQ,QAAQ,wBAAA,GAA2B,iBAAA;AAAA,IAC3C,SAAA,EAAW;AAAA,GACZ;AACD;;;ACmEO,IAAM,UAAA,GAAa;AAAA,EACzB,OAAA,EAASA,SAAAA;AAAA,EACT,WAAA,EAAaC,aAAAA;AAAA,EACb,WAAA;AAAA,EACA,eAAA;AAAA;AAAA;AAAA;AAAA,EAIA;AACD;AAKO,IAAM,YAAA,GAAe;AAAA,EAC3B,OAAA,EAASD,SAAAA;AAAA,EACT,WAAA,EAAaC,aAAAA;AAAA,EACb;AACD;AAMO,IAAM,QAAA,GAAW;AAAA,EACvB,OAAA,EAASD,SAAAA;AAAA,EACT,WAAA,EAAaC;AACd;AAkBO,IAAM,WAAA,GAAc;AAAA,EAC1B,OAAA;AAAA,EACA,WAAA;AAAA,EACA,OAAA;AAAA,EACA,KAAA;AAAA,EACA,mBAAA;AAAA,EACA,WAAA;AAAA,EACA,YAAA;AAAA,EACA,YAAA;AAAA,EACA,YAAA;AAAA,EACA,OAAA;AAAA,EACA,eAAA;AAAA,EACA,KAAA;AAAA,EACA,OAAA;AAAA,EACA,KAAA;AAAA,EACA,SAAA;AAAA,EACA;AACD","file":"advanced.js","sourcesContent":["/**\n * Whether this browser will keep the local store, and what the user can do\n * about it.\n *\n * One browser does not merely inconvenience a local-first app, it DELETES its\n * data: Safari erases script-writable storage after seven days of browser use\n * without interaction with the site. Someone who closes for three weeks comes\n * back to an empty app, and nothing warned them - which is also the moment they\n * notice it last.\n *\n * Adding the app to the Dock or the Home Screen switches its storage to\n * persistent mode, which WebKit exempts from its eviction triggers. WebKit does\n * not write anywhere that this neutralises the inactivity rule by name, so this\n * module reports an advisory, never a guarantee.\n *\n * The library ships no user-facing copy: it names the situation, the host writes\n * the sentence.\n */\n\n/** What stands between this browser and keeping the data. */\nexport type StorageRisk =\n\t/** Nothing known against it: the store survives until the user clears it. */\n\t| 'none'\n\t/** Data is erased when the tab closes (private browsing). Refuse to set up here. */\n\t| 'ephemeral'\n\t/** Erased after a stretch of not opening the app, unless it is installed. */\n\t| 'evicted-when-idle';\n\nexport interface StorageAdvice {\n\trisk: StorageRisk;\n\t/** The gesture that removes the risk, when there is one. Stable key; the\n\t * host owns the wording and the platform-specific instructions. */\n\tremedy?: 'install-to-dock' | 'install-to-home-screen';\n\t/** Already running as an installed app, so the remedy is done. */\n\tinstalled: boolean;\n}\n\n/** Real Safari, not Chrome or Edge on macOS - all three carry \"Safari\" in the\n * user agent, and only Safari carries none of the others' tokens. */\nfunction isSafari(ua: string): boolean {\n\treturn /Safari\\//.test(ua) && !/Chrome|Chromium|Edg\\//.test(ua) && !/OPR\\/|Android/.test(ua);\n}\n\nfunction isStandalone(): boolean {\n\tif (typeof window === 'undefined') return false;\n\tconst displayMode = window.matchMedia?.('(display-mode: standalone)')?.matches === true;\n\t// iOS and iPadOS kept their own flag, older than display-mode.\n\tconst ios = (navigator as Navigator & { standalone?: boolean }).standalone === true;\n\treturn displayMode || ios;\n}\n\n/**\n * What this browser will do with the local store, and the one gesture that\n * changes it. Call it before setting up, and again in whatever screen owns the\n * data: the advice disappears on its own once the remedy is applied.\n */\nexport function storageAdvice(): StorageAdvice {\n\tif (typeof navigator === 'undefined' || typeof window === 'undefined') {\n\t\treturn { risk: 'none', installed: false };\n\t}\n\tconst installed = isStandalone();\n\tif (!isSafari(navigator.userAgent) || installed) {\n\t\treturn { risk: 'none', installed };\n\t}\n\tconst touch = /iPhone|iPad|iPod/.test(navigator.userAgent) || navigator.maxTouchPoints > 1;\n\treturn {\n\t\trisk: 'evicted-when-idle',\n\t\tremedy: touch ? 'install-to-home-screen' : 'install-to-dock',\n\t\tinstalled: false\n\t};\n}\n","/**\n * selfstore/advanced - the pull-model store and the machinery under it.\n *\n * The simple `selfstore()` facade (package root) owns its data. This entry\n * inverts that: with createLocalStore your app owns the data and hands the\n * store a gather/apply pair - use it when state lives in your own reactive\n * model (Svelte runes, Redux, signals) and the facade's collections would be\n * a second copy. Everything here also backs `store.advanced` on the simple\n * store: same machinery, same instance, so the two styles compose.\n *\n * Also here: the durable destinations (disk file, Google Drive, WebDAV) as\n * connectable targets, the BackupTarget contract for writing your own, the\n * caches, the headless status derivation, and the functional codec for the\n * backup file format (the fluent one lives at the package root).\n */\n\n// --- The pull-model store: your app owns the data ---\nexport {\n\tcreateLocalStore,\n\ttype LocalStore,\n\ttype LocalStoreOptions,\n\ttype LocalStoreState,\n\ttype StoreError,\n\ttype Mode,\n\ttype TargetKind,\n\ttype SyncSource,\n\ttype SyncJournalEntry,\n\ttype PeerState,\n\ttype MirrorState,\n\ttype ReplicaState,\n\ttype StoreGroupConfig\n} from '../persistence/store';\n\n// --- On-device caches (the store's working copy) ---\nexport {\n\tindexedDbCache,\n\tmemoryCache,\n\tisLockableCache,\n\ttype LocalCache,\n\ttype LockableCache,\n\ttype CachedFile,\n\ttype KV\n} from '../persistence/cache';\n\n// --- Headless status: map store state to your UI copy ---\nexport {\n\tderiveStatus,\n\ttype StatusDescriptor,\n\ttype StatusInput,\n\ttype Severity,\n\ttype StorageState,\n\ttype StatusAction\n} from '../persistence/status';\n\n// --- Will this browser keep the data, and what can the user do about it ---\nexport { storageAdvice, type StorageAdvice, type StorageRisk } from '../persistence/installation';\n\n// --- The target contract (write your own destination) ---\nexport {\n\tRESERVED_STORE_MODES,\n\tisReservedStoreMode,\n\ttype BackupTarget,\n\ttype BuiltinTargetKind,\n\ttype PeerSource\n} from '../persistence/target';\n\n// --- The backup file format, functional style (fluent lives at the root) ---\nexport {\n\texportSnapshot,\n\timportSnapshot,\n\tinspect,\n\tisEncrypted,\n\tRESERVED_COLLECTION_PREFIX,\n\ttype Snapshot,\n\ttype SnapshotFile,\n\ttype Header,\n\ttype KdfParams,\n\ttype RecipientStanza,\n\ttype EncodeOptions\n} from '../selfstore';\n\n// --- Password strength policy (the store enforces it; the UI previews it) ---\nexport {\n\tcheckPasswordPolicy,\n\ttype PasswordPolicy,\n\ttype PasswordCheck,\n\ttype PasswordRequirement\n} from '../selfstore';\n\n// --- The full error contract (AuthExpiredError is what a custom target or\n// DriveAuth THROWS to signal genuine access loss; everything else it\n// throws is treated as transient and retried) ---\nexport {\n\tSelfstoreError,\n\tAuthExpiredError,\n\tisSelfstoreError,\n\tisAuthExpired,\n\terrorLabelKey,\n\ttype SelfstoreErrorCode\n} from '../selfstore';\n\n// --- Durable destinations (opt-in), each implementing BackupTarget ---\n// Explicit namespace objects: every member is deliberately public, so a future\n// internal helper in a target module can never leak into the contract.\nimport {\n\tconnect as fileConnect,\n\tfromSession as fileFromSession,\n\tisSupported as fileIsSupported,\n\tisOpenSupported as fileIsOpenSupported,\n\topenExisting as fileOpenExisting\n} from '../persistence/targets/file';\nimport {\n\tconnect as webdavConnect,\n\tfromSession as webdavFromSession,\n\tpeer as webdavPeer\n} from '../persistence/targets/webdav';\nimport { connect as s3Connect, fromSession as s3FromSession } from '../persistence/targets/s3';\nimport {\n\tconnect as driveConnect,\n\tfromSession as driveFromSession,\n\tpreview as drivePreview,\n\tadopt as driveAdopt,\n\tfindOrCreateOwnFile as driveFindOrCreateOwnFile,\n\tlistBackups as driveListBackups,\n\tcreateBackup as driveCreateBackup,\n\tdeleteBackup as driveDeleteBackup,\n\trenameBackup as driveRenameBackup,\n\taccount as driveAccount,\n\tcreateCompanion as driveCreateCompanion,\n\tshare as driveShare,\n\tunshare as driveUnshare,\n\towner as driveOwner,\n\tsecondary as driveSecondary,\n\tFILE_ID_KEY as DRIVE_FILE_ID_KEY\n} from '../persistence/targets/drive';\n\n/** Disk-file destination (File System Access, Chromium). */\nexport const fileTarget = {\n\tconnect: fileConnect,\n\tfromSession: fileFromSession,\n\tisSupported: fileIsSupported,\n\tisOpenSupported: fileIsOpenSupported,\n\t/** Let the user point at an EXISTING backup file and adopt it as the\n\t * destination. Gate on isOpenSupported(): unlike creating, this has no\n\t * download fallback. */\n\topenExisting: fileOpenExisting\n};\n\n/** WebDAV destination (Nextcloud, ownCloud, any server the user controls), plus\n * `peer()`: a read-only source over another member's shared WebDAV link, so a\n * group can share read-write without Google Drive (see store.attachPeer). */\nexport const webdavTarget = {\n\tconnect: webdavConnect,\n\tfromSession: webdavFromSession,\n\tpeer: webdavPeer\n};\n\n/** S3-compatible destination (Amazon S3, Cloudflare R2, Backblaze B2, MinIO):\n * the browser signs each request with SigV4 and writes one object on a bucket\n * the user controls, no vendor broker. The bucket only ever holds the same\n * end-to-end-encrypted .selfstore bytes. */\nexport const s3Target = {\n\tconnect: s3Connect,\n\tfromSession: s3FromSession\n};\n\n/** Google Drive destination, plus the shared-file primitives (preview a specific\n * file, adopt it as this device's backup, find-or-create the user's own file)\n * and the backup-management ops (list the app's files with dates, create a\n * named empty backup, rename one, delete one for good) - the raw pieces a \"my\n * backups\" panel builds on. `account()` names WHICH Google account holds them,\n * because a brand name is not an address.\n *\n * The last five are the Drive side of SHARING, on companion files the app\n * creates next to the backup: `createCompanion` mints one, `share` publishes it\n * on a link, `unshare` makes it private again, `owner` says whose Drive it\n * lives on, and `secondary` is a read-write target over it whose lifecycle is\n * its own - detaching a shared copy leaves the user's real backup and their\n * Drive session untouched. That is what a ShareBackend needs from Drive and\n * what every app was rewriting; what stays yours is reading ANOTHER account's\n * file, which the drive.file scope cannot do without a Picker gesture or a\n * relay you host. */\nexport const driveTarget = {\n\tconnect: driveConnect,\n\tfromSession: driveFromSession,\n\tpreview: drivePreview,\n\tadopt: driveAdopt,\n\tfindOrCreateOwnFile: driveFindOrCreateOwnFile,\n\tlistBackups: driveListBackups,\n\tcreateBackup: driveCreateBackup,\n\tdeleteBackup: driveDeleteBackup,\n\trenameBackup: driveRenameBackup,\n\taccount: driveAccount,\n\tcreateCompanion: driveCreateCompanion,\n\tshare: driveShare,\n\tunshare: driveUnshare,\n\towner: driveOwner,\n\tsecondary: driveSecondary,\n\tFILE_ID_KEY: DRIVE_FILE_ID_KEY\n};\n\nexport type { FileConnectOptions } from '../persistence/targets/file';\nexport type {\n\tDriveAuth,\n\tDriveOptions,\n\tDriveBackupInfo,\n\tDriveAccount\n} from '../persistence/targets/drive';\nexport type {\n\tWebdavConfig,\n\tWebdavConnectOptions,\n\tWebdavPeerOptions\n} from '../persistence/targets/webdav';\nexport type { S3Config, S3ConnectOptions } from '../persistence/targets/s3';\nexport {\n\tgisDriveAuth,\n\ttype GisDriveAuthOptions,\n\ttype GisTokenPersistence\n} from '../persistence/targets/drive-auth-gis';\nexport { saveToDisk, pickFromDisk, datedName } from '../selfstore/targets/local';\n"]}
|
package/dist/backups.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { B as BackupsHost } from './manager-
|
|
2
|
-
export { a as BackupFileInfo, b as BackupOwner, c as BackupRow, d as BackupsErrorCode, e as BackupsKeys, f as BackupsManager, g as BackupsNaming, h as BackupsSnapshot, K as KnownBackups, i as createBackupsManager } from './manager-
|
|
3
|
-
import { D as DriveOptions } from './drive-
|
|
1
|
+
import { B as BackupsHost } from './manager-Di6lNE4E.js';
|
|
2
|
+
export { a as BackupFileInfo, b as BackupOwner, c as BackupRow, d as BackupsErrorCode, e as BackupsKeys, f as BackupsManager, g as BackupsNaming, h as BackupsSnapshot, K as KnownBackups, i as createBackupsManager } from './manager-Di6lNE4E.js';
|
|
3
|
+
import { D as DriveOptions } from './drive-86CHuTij.js';
|
|
4
4
|
import './cache-ilsK3Tki.js';
|
|
5
|
-
import './store-
|
|
5
|
+
import './store-CIbiNoiE.js';
|
|
6
6
|
import './sync.js';
|
|
7
7
|
import './machine-ALMkT3mk.js';
|
|
8
8
|
|
package/dist/backups.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
export { createBackupsManager, driveBackupsHost } from './chunk-
|
|
2
|
-
import './chunk-
|
|
3
|
-
import './chunk-
|
|
4
|
-
import './chunk-
|
|
5
|
-
import './chunk-
|
|
6
|
-
import './chunk-
|
|
1
|
+
export { createBackupsManager, driveBackupsHost } from './chunk-EBCQ7ZG7.js';
|
|
2
|
+
import './chunk-3KAAPYPO.js';
|
|
3
|
+
import './chunk-JTNFZV2B.js';
|
|
4
|
+
import './chunk-757ZOSUI.js';
|
|
5
|
+
import './chunk-HHPJPEEG.js';
|
|
6
|
+
import './chunk-OY2A5DCB.js';
|
|
7
|
+
import './chunk-RREJ2MFN.js';
|
|
8
|
+
import './chunk-XMC67RNM.js';
|
|
7
9
|
//# sourceMappingURL=backups.js.map
|
|
8
10
|
//# sourceMappingURL=backups.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { unseal } from './chunk-QBCU7OOP.js';
|
|
2
|
-
import { freshKdfParams, deriveKey } from './chunk-
|
|
2
|
+
import { freshKdfParams, deriveKey } from './chunk-RREJ2MFN.js';
|
|
3
|
+
import './chunk-XMC67RNM.js';
|
|
3
4
|
|
|
4
5
|
// src/persistence/cache-lock.ts
|
|
5
6
|
function freshCacheKdf() {
|
|
@@ -18,5 +19,5 @@ async function opens(key, probe) {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export { freshCacheKdf, keyFromPassword, opens };
|
|
21
|
-
//# sourceMappingURL=cache-lock-
|
|
22
|
-
//# sourceMappingURL=cache-lock-
|
|
22
|
+
//# sourceMappingURL=cache-lock-5RNG6CIK.js.map
|
|
23
|
+
//# sourceMappingURL=cache-lock-5RNG6CIK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/persistence/cache-lock.ts"],"names":[],"mappings":";;;;;AAeO,SAAS,aAAA,GAA0B;AACzC,EAAA,OAAO,cAAA,EAAe;AACvB;AAGO,SAAS,eAAA,CAAgB,UAAkB,GAAA,EAAmC;AACpF,EAAA,OAAO,SAAA,CAAU,UAAU,GAAG,CAAA;AAC/B;AAIA,eAAsB,KAAA,CAAM,KAAgB,KAAA,EAAsC;AACjF,EAAA,IAAI;AACH,IAAA,MAAM,MAAA,CAAO,KAAK,KAAK,CAAA;AACvB,IAAA,OAAO,IAAA;AAAA,EACR,CAAA,CAAA,MAAQ;AACP,IAAA,OAAO,KAAA;AAAA,EACR;AACD","file":"cache-lock-5RNG6CIK.js","sourcesContent":["// Seal-key material for a lock-mode cache. The key is derived from a password\n// (the backup's Argon2id KDF) or supplied by the app (e.g. a passkey PRF key),\n// and is held in memory only - never written next to the data. That is what a\n// device-key seal (cache-crypto) cannot give: a copy of the whole browser\n// profile carries the device key, but not a secret kept in RAM. Only the KDF\n// salt is persisted, and a salt is not secret.\n\nimport { unseal, type EncEnvelope } from './cache-crypto';\nimport { deriveKey, freshKdfParams } from '../selfstore/crypto';\nimport type { KdfParams } from '../selfstore/types';\n\n/** Argon2id parameters for a password-locked cache; persisted in the clear. */\nexport type CacheKdf = KdfParams;\n\n/** Fresh Argon2id parameters (random salt, current default cost). */\nexport function freshCacheKdf(): CacheKdf {\n\treturn freshKdfParams();\n}\n\n/** Derive the AES-GCM seal key from a password and its stored parameters. */\nexport function keyFromPassword(password: string, kdf: CacheKdf): Promise<CryptoKey> {\n\treturn deriveKey(password, kdf);\n}\n\n/** True if `key` decrypts `probe`. A wrong key fails AES-GCM authentication;\n * that is a wrong-secret answer, never an error to surface. */\nexport async function opens(key: CryptoKey, probe: EncEnvelope): Promise<boolean> {\n\ttry {\n\t\tawait unseal(key, probe);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n"]}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { byCodePoint } from './chunk-FJGWTBAL.js';
|
|
2
|
+
import { SelfstoreError } from './chunk-XMC67RNM.js';
|
|
3
|
+
|
|
1
4
|
// src/sync/hlc.ts
|
|
2
5
|
var WALL_W = 15;
|
|
3
6
|
var CTR_W = 6;
|
|
@@ -10,7 +13,19 @@ function decode(h) {
|
|
|
10
13
|
return { wall: Number(h.slice(0, i)), counter: Number(h.slice(i + 1, j)), node: h.slice(j + 1) };
|
|
11
14
|
}
|
|
12
15
|
function createNode() {
|
|
13
|
-
|
|
16
|
+
if (typeof crypto === "undefined") {
|
|
17
|
+
throw new TypeError(
|
|
18
|
+
"createNode(): this platform provides no WebCrypto, so no replica id can be minted (evergreen browsers, Node 20+)."
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
if (crypto.randomUUID) return crypto.randomUUID();
|
|
22
|
+
const bytes = new Uint8Array(16);
|
|
23
|
+
crypto.getRandomValues(bytes);
|
|
24
|
+
return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
25
|
+
}
|
|
26
|
+
var MAX_DRIFT_MS = 5 * 6e4;
|
|
27
|
+
function driftedAhead(h, wallNow = Date.now(), maxDriftMs = MAX_DRIFT_MS) {
|
|
28
|
+
return hlcWall(h) > wallNow + maxDriftMs;
|
|
14
29
|
}
|
|
15
30
|
function issue(prev, node, wallNow = Date.now()) {
|
|
16
31
|
const p = prev ? decode(prev) : null;
|
|
@@ -38,7 +53,7 @@ function stableStringify(v) {
|
|
|
38
53
|
if (v === null || typeof v !== "object") return JSON.stringify(v) ?? "null";
|
|
39
54
|
if (Array.isArray(v)) return "[" + v.map(stableStringify).join(",") + "]";
|
|
40
55
|
const o = v;
|
|
41
|
-
return "{" + Object.keys(o).sort().map((k) => JSON.stringify(k) + ":" + stableStringify(o[k])).join(",") + "}";
|
|
56
|
+
return "{" + Object.keys(o).sort(byCodePoint).map((k) => JSON.stringify(k) + ":" + stableStringify(o[k])).join(",") + "}";
|
|
42
57
|
}
|
|
43
58
|
function hash(v) {
|
|
44
59
|
const s = stableStringify(v);
|
|
@@ -153,7 +168,28 @@ function stamp(meta, collections, config, wallNow = Date.now()) {
|
|
|
153
168
|
}
|
|
154
169
|
return { node: meta.node, clock, cols };
|
|
155
170
|
}
|
|
171
|
+
function* allClocks(meta) {
|
|
172
|
+
if (meta.clock) yield meta.clock;
|
|
173
|
+
for (const col of Object.values(meta.cols)) {
|
|
174
|
+
yield* Object.values(col.clocks);
|
|
175
|
+
yield* Object.values(col.deleted);
|
|
176
|
+
for (const fields of Object.values(col.fields ?? {})) {
|
|
177
|
+
for (const [clock] of Object.values(fields)) yield clock;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function assertNoDrift(meta, wallNow) {
|
|
182
|
+
for (const clock of allClocks(meta)) {
|
|
183
|
+
if (driftedAhead(clock, wallNow)) {
|
|
184
|
+
throw new SelfstoreError(
|
|
185
|
+
"CLOCK_DRIFT",
|
|
186
|
+
`This copy carries a clock ${Math.round((hlcWall(clock) - wallNow) / 6e4)} minutes ahead of this device. Check the clock of the device that wrote it: merging would let those writes win every future conflict, on every device.`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
156
191
|
function merge(a, b, config, base) {
|
|
192
|
+
assertNoDrift(b.meta, Date.now());
|
|
157
193
|
const conflicts = [];
|
|
158
194
|
const outCols = {};
|
|
159
195
|
const outMeta = {
|
|
@@ -346,5 +382,5 @@ function changes(before, after, config) {
|
|
|
346
382
|
}
|
|
347
383
|
|
|
348
384
|
export { changes, createMeta, createNode, detectConflicts, gcTombstones, idPathFor, merge, stamp, unsyncableCounts };
|
|
349
|
-
//# sourceMappingURL=chunk-
|
|
350
|
-
//# sourceMappingURL=chunk-
|
|
385
|
+
//# sourceMappingURL=chunk-23UAUYSW.js.map
|
|
386
|
+
//# sourceMappingURL=chunk-23UAUYSW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/sync/hlc.ts","../src/sync/merge.ts"],"names":[],"mappings":"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","file":"chunk-23UAUYSW.js","sourcesContent":["/**\n * Hybrid Logical Clock: wall time + a counter + a node id, giving a\n * deterministic total order that tolerates clock skew. Encoded as a fixed-width\n * string so a plain string comparison equals causal order.\n */\n\nexport type Hlc = string;\n\ninterface Parts {\n\twall: number;\n\tcounter: number;\n\tnode: string;\n}\n\nconst WALL_W = 15;\nconst CTR_W = 6;\n\nfunction encode(p: Parts): Hlc {\n\treturn `${String(p.wall).padStart(WALL_W, '0')}|${String(p.counter).padStart(CTR_W, '0')}|${p.node}`;\n}\n\nfunction decode(h: Hlc): Parts {\n\tconst i = h.indexOf('|');\n\tconst j = h.indexOf('|', i + 1);\n\treturn { wall: Number(h.slice(0, i)), counter: Number(h.slice(i + 1, j)), node: h.slice(j + 1) };\n}\n\n/**\n * A fresh random replica id (one per device). Not a secret - but two replicas\n * sharing an id lose the tiebreak that makes the order total, so it comes from\n * the same source as everything else here. `randomUUID` is exposed only in a\n * secure context; `getRandomValues` is not, which is why the fallback is that\n * one and never `Math.random`.\n */\nexport function createNode(): string {\n\tif (typeof crypto === 'undefined') {\n\t\tthrow new TypeError(\n\t\t\t'createNode(): this platform provides no WebCrypto, so no replica id can be minted ' +\n\t\t\t\t'(evergreen browsers, Node 20+).'\n\t\t);\n\t}\n\tif (crypto.randomUUID) return crypto.randomUUID();\n\tconst bytes = new Uint8Array(16);\n\tcrypto.getRandomValues(bytes);\n\treturn [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('');\n}\n\n/**\n * How far ahead of local wall time a clock minted elsewhere may sit before this\n * replica refuses it.\n *\n * A hybrid logical clock absorbs the highest value it sees, so ONE impossible\n * wall time does three things at once: it wins every last-writer-wins\n * comparison from then on, it raises the clock of every replica that merges it\n * (and of every replica those merge with, in turn), and it keeps doing so after\n * whoever sent it has been removed from the group. Five minutes leaves room for\n * a device whose clock was never synchronised; nothing legitimate needs more.\n */\nexport const MAX_DRIFT_MS = 5 * 60_000;\n\n/** Whether `h` claims a wall time further ahead than the drift bound allows. */\nexport function driftedAhead(\n\th: Hlc,\n\twallNow: number = Date.now(),\n\tmaxDriftMs: number = MAX_DRIFT_MS\n): boolean {\n\treturn hlcWall(h) > wallNow + maxDriftMs;\n}\n\n/** Issue a clock for a local event. Monotonic per node even if the wall clock moves back. */\nexport function issue(prev: Hlc | null, node: string, wallNow: number = Date.now()): Hlc {\n\tconst p = prev ? decode(prev) : null;\n\tconst prevWall = p ? p.wall : 0;\n\tconst wall = Math.max(wallNow, prevWall);\n\tconst counter = wall === prevWall && p ? p.counter + 1 : 0;\n\treturn encode({ wall, counter, node });\n}\n\n/** Fold a clock observed from another replica into the local one. */\nexport function receive(\n\tlocal: Hlc | null,\n\tremote: Hlc,\n\tnode: string,\n\twallNow: number = Date.now()\n): Hlc {\n\tconst l = local ? decode(local) : { wall: 0, counter: 0, node };\n\tconst r = decode(remote);\n\tconst wall = Math.max(wallNow, l.wall, r.wall);\n\tlet counter: number;\n\tif (wall === l.wall && wall === r.wall) counter = Math.max(l.counter, r.counter) + 1;\n\telse if (wall === l.wall) counter = l.counter + 1;\n\telse if (wall === r.wall) counter = r.counter + 1;\n\telse counter = 0;\n\treturn encode({ wall, counter, node });\n}\n\n/** The wall-time component (ms since epoch) encoded in a clock, for age-based\n * pruning (tombstone GC). Not a precise event time - HLC wall can run ahead of\n * real time under skew - but a safe lower bound on \"how old is this at least\". */\nexport function hlcWall(h: Hlc): number {\n\treturn Number(h.slice(0, h.indexOf('|')));\n}\n\n/** Total order: <0 if a precedes b, >0 if a follows b, 0 only if identical. */\nexport function compare(a: Hlc, b: Hlc): number {\n\tif (a < b) return -1;\n\tif (a > b) return 1;\n\treturn 0;\n}\n\n/** The later of two clocks. */\nexport function max(a: Hlc | null, b: Hlc | null): Hlc {\n\tif (!a) return b as Hlc;\n\tif (!b) return a;\n\treturn compare(a, b) >= 0 ? a : b;\n}\n","/**\n * Schema-agnostic convergent merge for plain JSON. It keeps a small sidecar of\n * metadata (one logical clock per record) and reconciles two replica states with\n * well-known set semantics. Documents stay plain objects: identity is a\n * configured id field, change detection is by content hash, nothing is injected.\n *\n * Per-collection strategy:\n * 'lww-set' records keyed by id; later write per id wins; deletes tombstoned.\n * 'lww-map' records keyed by id, merged field BY field: concurrent edits\n * to different fields of one record both survive; the same field\n * goes to the later clock. Deletes stay record-level.\n * 'grow-set' append-only; entries immutable; union, never a conflict.\n * 'lww-register' a single value; later write wins.\n * 'manual' surface concurrent edits to the same id instead of resolving.\n *\n * For collaborative rich text, store an Automerge/Yjs document as a binary file\n * in the snapshot instead; this engine stops short of sequence CRDTs.\n */\n\nimport { type Hlc, issue, max, compare, createNode, hlcWall, driftedAhead } from './hlc';\nimport { SelfstoreError } from '../selfstore/errors';\nimport { byCodePoint } from '../ordering';\n\nexport type { Hlc } from './hlc';\nexport type Id = string;\nexport type MergeStrategy = 'lww-set' | 'lww-map' | 'grow-set' | 'lww-register' | 'manual';\n\nexport interface SyncConfig {\n\t/** Field holding each record's stable id (default 'id'). Dot-notation allowed. */\n\tidField?: string;\n\t/** Per-collection id path override for records whose id is not at the top level. */\n\tids?: Record<string, string>;\n\t/** Strategy per collection name; anything unlisted uses `fallback`. */\n\tstrategies?: Record<string, MergeStrategy>;\n\t/** Strategy for unlisted collections (default 'lww-set'). */\n\tfallback?: MergeStrategy;\n}\n\n/** Sidecar metadata for one collection, keyed by record id. */\nexport interface ColMeta {\n\tclocks: Record<Id, Hlc>;\n\tdeleted: Record<Id, Hlc>;\n\thashes: Record<Id, number>;\n\t/** 'lww-map' only: per-field [clock, contentHash], so concurrent edits to\n\t * different fields of one record merge instead of one side losing. */\n\tfields?: Record<Id, Record<string, [Hlc, number]>>;\n}\n\n/** Everything one replica tracks. Travels with the data so the other side can merge. */\nexport interface SyncMeta {\n\tnode: string;\n\tclock: Hlc | null;\n\tcols: Record<string, ColMeta>;\n}\n\n/** A same-id concurrent edit the 'manual' strategy refused to auto-resolve.\n * `local`/`remote` carry the two full versions during the session; consumers\n * persisting conflicts (e.g. the store's journal) redact them, so they are\n * absent after a reload - never write user records to unencrypted storage. */\nexport interface Conflict {\n\tcollection: string;\n\tid: Id;\n\tlocal?: unknown;\n\tremote?: unknown;\n\t/** The side kept as the provisional value (so it still converges). */\n\tkept: 'local' | 'remote';\n}\n\nexport interface MergeResult {\n\tcollections: Record<string, unknown[]>;\n\tmeta: SyncMeta;\n\tconflicts: Conflict[];\n}\n\nfunction stableStringify(v: unknown): string {\n\tif (v === null || typeof v !== 'object') return JSON.stringify(v) ?? 'null';\n\tif (Array.isArray(v)) return '[' + v.map(stableStringify).join(',') + ']';\n\tconst o = v as Record<string, unknown>;\n\treturn (\n\t\t'{' +\n\t\tObject.keys(o)\n\t\t\t.sort(byCodePoint)\n\t\t\t.map((k) => JSON.stringify(k) + ':' + stableStringify(o[k]))\n\t\t\t.join(',') +\n\t\t'}'\n\t);\n}\n\n/** FNV-1a 32-bit hash, enough to detect that a record changed. */\nfunction hash(v: unknown): number {\n\tconst s = stableStringify(v);\n\tlet h = 0x811c9dc5;\n\tfor (let i = 0; i < s.length; i++) {\n\t\th ^= s.charCodeAt(i);\n\t\th = Math.imul(h, 0x01000193);\n\t}\n\treturn h >>> 0;\n}\n\nfunction emptyCol(): ColMeta {\n\treturn { clocks: {}, deleted: {}, hashes: {} };\n}\n\nfunction cloneCol(c: ColMeta | undefined): ColMeta {\n\tif (!c) return emptyCol();\n\tconst out: ColMeta = {\n\t\tclocks: { ...c.clocks },\n\t\tdeleted: { ...c.deleted },\n\t\thashes: { ...c.hashes }\n\t};\n\tif (c.fields) {\n\t\tout.fields = {};\n\t\tfor (const [id, f] of Object.entries(c.fields)) out.fields[id] = { ...f };\n\t}\n\treturn out;\n}\n\nfunction strategyOf(config: SyncConfig, collection: string): MergeStrategy {\n\treturn config.strategies?.[collection] ?? config.fallback ?? 'lww-set';\n}\n\nfunction idOf(config: SyncConfig, collection: string, doc: unknown): Id | null {\n\tconst path = config.ids?.[collection] ?? config.idField ?? 'id';\n\tlet v: unknown = doc;\n\tfor (const key of path.split('.')) {\n\t\tif (v === null || typeof v !== 'object') return null;\n\t\tv = (v as Record<string, unknown>)[key];\n\t}\n\treturn typeof v === 'string' ? v : null;\n}\n\n/** The id path this config resolves for a collection (for diagnostics). */\nexport function idPathFor(config: SyncConfig, collection: string): string {\n\treturn config.ids?.[collection] ?? config.idField ?? 'id';\n}\n\n/** Records the engine cannot track because they have no string id at the\n * configured path. Identity is a string id field, so such a record is invisible\n * to stamp/merge - it would silently never replicate. Returns the count per\n * collection (nonzero entries only), for a dev-time warning. A number or missing\n * id is the classic footgun: give each record a string id, or map the field via\n * `sync.ids`. */\nexport function unsyncableCounts(\n\tcollections: Record<string, unknown[]>,\n\tconfig: SyncConfig = {}\n): Record<string, number> {\n\tconst out: Record<string, number> = {};\n\tfor (const [name, docs] of Object.entries(collections)) {\n\t\tlet n = 0;\n\t\tfor (const doc of docs ?? []) if (idOf(config, name, doc) === null) n++;\n\t\tif (n > 0) out[name] = n;\n\t}\n\treturn out;\n}\n\nexport function createMeta(node: string = createNode()): SyncMeta {\n\treturn { node, clock: null, cols: {} };\n}\n\n/**\n * Prune tombstones (delete markers) whose clock is older than `beforeWall` (a\n * wall-time ms cutoff, e.g. `Date.now() - horizon`). Tombstones otherwise\n * accumulate forever, since a delete must be remembered to out-vote a stale\n * replica still holding the record.\n *\n * Safe only when the cutoff is older than the longest a replica can stay offline\n * before it next syncs: a replica that never saw a delete, and whose tombstone\n * has been GC'd everywhere, will resurrect that record on its next merge. Pick a\n * horizon comfortably beyond your worst-case offline window. Returns new meta\n * (input untouched); present-record clocks are untouched.\n */\nexport function gcTombstones(meta: SyncMeta, beforeWall: number): SyncMeta {\n\tconst cols: Record<string, ColMeta> = {};\n\tfor (const [name, col] of Object.entries(meta.cols)) {\n\t\tconst next = cloneCol(col);\n\t\tfor (const [id, clk] of Object.entries(next.deleted)) {\n\t\t\tif (hlcWall(clk) < beforeWall) delete next.deleted[id];\n\t\t}\n\t\tcols[name] = next;\n\t}\n\treturn { node: meta.node, clock: meta.clock, cols };\n}\n\n/**\n * Diff `collections` against the metadata's content hashes, assign a fresh clock\n * to every added or changed record, and tombstone records that disappeared.\n * Returns new metadata (input untouched). Call on every local save.\n */\ntype Tick = () => Hlc;\n\n/** Stamp each present record: a fresh clock for anything added or changed (by\n * content hash). Returns the set of ids seen, for the tombstone pass. */\nfunction stampPresent(\n\tcol: ColMeta,\n\tpresent: unknown[],\n\tconfig: SyncConfig,\n\tname: string,\n\ttick: Tick\n): Set<Id> {\n\tconst fieldLevel = strategyOf(config, name) === 'lww-map';\n\tconst seen = new Set<Id>();\n\tfor (const doc of present) {\n\t\tconst id = idOf(config, name, doc);\n\t\tif (id === null) continue;\n\t\tseen.add(id);\n\t\tconst h = hash(doc);\n\t\tif (col.hashes[id] !== h) {\n\t\t\tcol.clocks[id] = tick();\n\t\t\tcol.hashes[id] = h;\n\t\t\tdelete col.deleted[id];\n\t\t\tif (fieldLevel && isRecord(doc)) stampFields(col, id, doc, tick);\n\t\t}\n\t}\n\treturn seen;\n}\n\n/** Stamp each added/changed field of one record ('lww-map'): a field keeps its\n * clock until its own content changes, so field clocks stay independent and a\n * merge can tell which side last touched which field. */\nfunction stampFields(col: ColMeta, id: Id, doc: Record<string, unknown>, tick: Tick): void {\n\tconst rec = ((col.fields ??= {})[id] ??= {});\n\tfor (const key of Object.keys(doc)) {\n\t\tconst h = hash(doc[key]);\n\t\tif (rec[key]?.[1] !== h) rec[key] = [tick(), h];\n\t}\n\tfor (const key of Object.keys(rec)) if (!(key in doc)) delete rec[key];\n}\n\n/** Tombstone every previously-tracked id that is no longer present. */\nfunction tombstoneMissing(col: ColMeta, seen: Set<Id>, tick: Tick): void {\n\tfor (const id of Object.keys(col.clocks)) {\n\t\tif (!seen.has(id)) {\n\t\t\tcol.deleted[id] = tick();\n\t\t\tdelete col.clocks[id];\n\t\t\tdelete col.hashes[id];\n\t\t\tdelete col.fields?.[id];\n\t\t}\n\t}\n}\n\nexport function stamp(\n\tmeta: SyncMeta,\n\tcollections: Record<string, unknown[]>,\n\tconfig: SyncConfig,\n\twallNow: number = Date.now()\n): SyncMeta {\n\tlet clock = meta.clock;\n\tconst tick: Tick = () => (clock = issue(clock, meta.node, wallNow));\n\tconst cols: Record<string, ColMeta> = {};\n\tconst names = new Set([...Object.keys(meta.cols), ...Object.keys(collections)]);\n\n\tfor (const name of names) {\n\t\tconst strategy = strategyOf(config, name);\n\t\tconst col = cloneCol(meta.cols[name]);\n\t\tconst present = collections[name] ?? [];\n\n\t\tconst seen = stampPresent(col, present, config, name, tick);\n\t\tif (strategy !== 'grow-set') tombstoneMissing(col, seen, tick);\n\t\tcols[name] = col;\n\t}\n\treturn { node: meta.node, clock, cols };\n}\n\n/** One replica's full state: the collections plus their sync metadata. What\n * `merge` and `detectConflicts` take on each side. */\nexport interface ReplicaState {\n\tcollections: Record<string, unknown[]>;\n\tmeta: SyncMeta;\n}\ntype State = ReplicaState;\n\n/**\n * Merge replica `b` into `a` (commutative for the convergent strategies). The\n * optional `base` lets 'manual' tell a true concurrent conflict from a one-sided\n * change; without it, 'manual' errs toward surfacing.\n */\n/** Every clock a replica carries, flattened: meta, records, tombstones, fields. */\nfunction* allClocks(meta: SyncMeta): Generator<Hlc> {\n\tif (meta.clock) yield meta.clock;\n\tfor (const col of Object.values(meta.cols)) {\n\t\tyield* Object.values(col.clocks);\n\t\tyield* Object.values(col.deleted);\n\t\tfor (const fields of Object.values(col.fields ?? {})) {\n\t\t\tfor (const [clock] of Object.values(fields)) yield clock;\n\t\t}\n\t}\n}\n\n/**\n * Refuse a replica whose clocks cannot be real. `b` is the incoming side by\n * signature, and only it is checked: a local replica that already absorbed a\n * bad clock must still be able to merge, or the fix would strand exactly the\n * stores it exists to protect. Refusing rather than clamping is deliberate -\n * a silently lowered clock would leave two replicas each convinced they hold\n * the later write.\n */\nfunction assertNoDrift(meta: SyncMeta, wallNow: number): void {\n\tfor (const clock of allClocks(meta)) {\n\t\tif (driftedAhead(clock, wallNow)) {\n\t\t\tthrow new SelfstoreError(\n\t\t\t\t'CLOCK_DRIFT',\n\t\t\t\t`This copy carries a clock ${Math.round((hlcWall(clock) - wallNow) / 60_000)} minutes ` +\n\t\t\t\t\t'ahead of this device. Check the clock of the device that wrote it: merging would ' +\n\t\t\t\t\t'let those writes win every future conflict, on every device.'\n\t\t\t);\n\t\t}\n\t}\n}\n\nexport function merge(a: State, b: State, config: SyncConfig, base?: SyncMeta): MergeResult {\n\tassertNoDrift(b.meta, Date.now());\n\tconst conflicts: Conflict[] = [];\n\tconst outCols: Record<string, unknown[]> = {};\n\tconst outMeta: SyncMeta = {\n\t\tnode: a.meta.node,\n\t\tclock: max(a.meta.clock, b.meta.clock),\n\t\tcols: {}\n\t};\n\n\tconst names = new Set([\n\t\t...Object.keys(a.collections),\n\t\t...Object.keys(b.collections),\n\t\t...Object.keys(a.meta.cols),\n\t\t...Object.keys(b.meta.cols)\n\t]);\n\n\tfor (const name of names) {\n\t\tconst merged = mergeCollection(name, a, b, config, base, conflicts);\n\t\toutCols[name] = merged.docs;\n\t\toutMeta.cols[name] = merged.col;\n\t}\n\n\treturn { collections: outCols, meta: outMeta, conflicts };\n}\n\n/**\n * Concurrent same-id edits that a convergent strategy resolves silently by keeping\n * the later clock. Informational only: it never changes what `merge` keeps, it\n * reports which records both replicas changed since `base` (the loser's version is\n * the one merge dropped). Empty without a base, since two-way state alone cannot\n * tell a concurrent edit from a one-sided change.\n */\nexport function detectConflicts(\n\ta: State,\n\tb: State,\n\tconfig: SyncConfig,\n\tbase?: SyncMeta\n): Conflict[] {\n\tif (!base) return [];\n\tconst out: Conflict[] = [];\n\tconst names = new Set([...Object.keys(a.collections), ...Object.keys(b.collections)]);\n\tfor (const name of names) {\n\t\tconst strategy = strategyOf(config, name);\n\t\tconst aDocs = byId(config, name, a.collections[name]);\n\t\tconst bDocs = byId(config, name, b.collections[name]);\n\t\tconst aMeta = a.meta.cols[name] ?? emptyCol();\n\t\tconst bMeta = b.meta.cols[name] ?? emptyCol();\n\t\tconst baseMeta = base.cols[name];\n\t\tfor (const id of new Set<Id>([...aDocs.keys(), ...bDocs.keys()])) {\n\t\t\tconst aClk = aMeta.clocks[id] ?? null;\n\t\t\tconst bClk = bMeta.clocks[id] ?? null;\n\t\t\tconst winnerSide = pickWinner(aClk, bClk, aDocs.has(id));\n\t\t\t// lww-map merges different-field edits cleanly: only the same field\n\t\t\t// changed on both sides is a real (auto-resolved) conflict.\n\t\t\tconst conflict =\n\t\t\t\tstrategy === 'lww-map'\n\t\t\t\t\t? fieldConflict(\n\t\t\t\t\t\t\tname,\n\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\taDocs,\n\t\t\t\t\t\t\tbDocs,\n\t\t\t\t\t\t\taMeta.fields?.[id],\n\t\t\t\t\t\t\tbMeta.fields?.[id],\n\t\t\t\t\t\t\tbaseMeta?.fields?.[id],\n\t\t\t\t\t\t\twinnerSide\n\t\t\t\t\t\t)\n\t\t\t\t\t: manualConflict(name, id, aDocs, bDocs, aClk, bClk, baseMeta, winnerSide);\n\t\t\tif (conflict) out.push(conflict);\n\t\t}\n\t}\n\treturn out;\n}\n\n/** The 'lww-map' conflict check: a Conflict only when the same field changed on\n * both sides since base (different-field edits merge without loss). */\nfunction fieldConflict(\n\tname: string,\n\tid: Id,\n\taDocs: Map<Id, unknown>,\n\tbDocs: Map<Id, unknown>,\n\taF: Record<string, [Hlc, number]> | undefined,\n\tbF: Record<string, [Hlc, number]> | undefined,\n\tbaseF: Record<string, [Hlc, number]> | undefined,\n\twinnerSide: 'local' | 'remote'\n): Conflict | null {\n\tif (!(aDocs.has(id) && bDocs.has(id) && aF && bF)) return null;\n\tfor (const key of Object.keys(aF)) {\n\t\tconst a = aF[key];\n\t\tconst b = bF[key];\n\t\tif (!b || a[0] === b[0]) continue;\n\t\tconst baseClk = baseF?.[key]?.[0] ?? null;\n\t\tconst aChanged = !baseClk || compare(a[0], baseClk) > 0;\n\t\tconst bChanged = !baseClk || compare(b[0], baseClk) > 0;\n\t\tif (aChanged && bChanged)\n\t\t\treturn {\n\t\t\t\tcollection: name,\n\t\t\t\tid,\n\t\t\t\tlocal: aDocs.get(id),\n\t\t\t\tremote: bDocs.get(id),\n\t\t\t\tkept: winnerSide\n\t\t\t};\n\t}\n\treturn null;\n}\n\n/** The 'manual' concurrent-edit check for one record: a Conflict when both sides\n * changed the same id since `base` (or, without a base, both simply hold it and\n * their clocks differ). Pure; null when there is nothing to surface. */\nfunction manualConflict(\n\tname: string,\n\tid: Id,\n\taDocs: Map<Id, unknown>,\n\tbDocs: Map<Id, unknown>,\n\taClk: Hlc | null,\n\tbClk: Hlc | null,\n\tbaseMeta: ColMeta | undefined,\n\twinnerSide: 'local' | 'remote'\n): Conflict | null {\n\tif (!(aDocs.has(id) && bDocs.has(id) && aClk && bClk && aClk !== bClk)) return null;\n\tconst baseClk = baseMeta?.clocks[id] ?? null;\n\tconst aChanged = !baseClk || compare(aClk, baseClk) > 0;\n\tconst bChanged = !baseClk || compare(bClk, baseClk) > 0;\n\tif (!(aChanged && bChanged)) return null;\n\treturn { collection: name, id, local: aDocs.get(id), remote: bDocs.get(id), kept: winnerSide };\n}\n\n/** Reconcile every id of one collection across the two replicas. */\nfunction mergeCollection(\n\tname: string,\n\ta: State,\n\tb: State,\n\tconfig: SyncConfig,\n\tbase: SyncMeta | undefined,\n\tconflicts: Conflict[]\n): { docs: unknown[]; col: ColMeta } {\n\tconst strategy = strategyOf(config, name);\n\tconst aDocs = byId(config, name, a.collections[name]);\n\tconst bDocs = byId(config, name, b.collections[name]);\n\tconst aMeta = a.meta.cols[name] ?? emptyCol();\n\tconst bMeta = b.meta.cols[name] ?? emptyCol();\n\tconst baseMeta = base?.cols[name];\n\tconst col = emptyCol();\n\tconst docs: unknown[] = [];\n\n\tconst ids = new Set<Id>([\n\t\t...Object.keys(aMeta.clocks),\n\t\t...Object.keys(bMeta.clocks),\n\t\t...Object.keys(aMeta.deleted),\n\t\t...Object.keys(bMeta.deleted),\n\t\t...aDocs.keys(),\n\t\t...bDocs.keys()\n\t]);\n\n\tfor (const id of ids) {\n\t\tconst aClk = aMeta.clocks[id] ?? null;\n\t\tconst bClk = bMeta.clocks[id] ?? null;\n\t\tconst aliveClk = max(aClk, bClk);\n\t\tconst deadClk = max(aMeta.deleted[id] ?? null, bMeta.deleted[id] ?? null);\n\n\t\tconst deleteWins =\n\t\t\tstrategy !== 'grow-set' && deadClk && (!aliveClk || compare(deadClk, aliveClk) > 0);\n\n\t\tif (deleteWins) {\n\t\t\tcol.deleted[id] = deadClk as Hlc;\n\t\t\tcontinue;\n\t\t}\n\t\tif (!aliveClk) {\n\t\t\tif (deadClk) col.deleted[id] = deadClk;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst aHas = aDocs.has(id);\n\t\tconst winnerSide = pickWinner(aClk, bClk, aHas);\n\t\tlet doc = winnerSide === 'local' ? aDocs.get(id) : bDocs.get(id);\n\n\t\tif (strategy === 'manual') {\n\t\t\tconst conflict = manualConflict(name, id, aDocs, bDocs, aClk, bClk, baseMeta, winnerSide);\n\t\t\tif (conflict) conflicts.push(conflict);\n\t\t}\n\n\t\tif (strategy === 'lww-map') {\n\t\t\tconst merged = mergeFields(id, aDocs.get(id), bDocs.get(id), aMeta, bMeta, winnerSide);\n\t\t\tif (merged) {\n\t\t\t\tdoc = merged.doc;\n\t\t\t\t(col.fields ??= {})[id] = merged.fields;\n\t\t\t} else {\n\t\t\t\t// One side has no document: the record-level winner stands; carry its\n\t\t\t\t// field metadata forward so later merges stay field-aware.\n\t\t\t\tconst f = (winnerSide === 'local' ? aMeta : bMeta).fields?.[id];\n\t\t\t\tif (f) (col.fields ??= {})[id] = { ...f };\n\t\t\t}\n\t\t}\n\n\t\tif (doc !== undefined) {\n\t\t\tdocs.push(doc);\n\t\t\tcol.clocks[id] = aliveClk;\n\t\t\tcol.hashes[id] = hash(doc);\n\t\t}\n\t}\n\n\treturn { docs, col };\n}\n\n/** Field-by-field merge of one record ('lww-map'): each field goes to the side\n * whose field clock is later; a field only one side stamped keeps that side's\n * value; unstamped fields (written under a record-level strategy) follow the\n * record-level winner.\n * Null when either document is missing (the record-level result stands). */\nfunction mergeFields(\n\tid: Id,\n\taDoc: unknown,\n\tbDoc: unknown,\n\taMeta: ColMeta,\n\tbMeta: ColMeta,\n\twinnerSide: 'local' | 'remote'\n): { doc: Record<string, unknown>; fields: Record<string, [Hlc, number]> } | null {\n\tif (!isRecord(aDoc) || !isRecord(bDoc)) return null;\n\tconst aF = aMeta.fields?.[id] ?? {};\n\tconst bF = bMeta.fields?.[id] ?? {};\n\tconst doc: Record<string, unknown> = {};\n\tconst fields: Record<string, [Hlc, number]> = {};\n\tfor (const key of new Set([...Object.keys(aDoc), ...Object.keys(bDoc)])) {\n\t\tconst a = aF[key] ?? null;\n\t\tconst b = bF[key] ?? null;\n\t\tconst side = pickFieldSide(a, b, winnerSide);\n\t\tconst src = side === 'local' ? aDoc : bDoc;\n\t\tif (!(key in src)) continue; // the winning side removed the field: drop it\n\t\tdoc[key] = src[key];\n\t\tconst stamped = side === 'local' ? a : b;\n\t\tif (stamped) fields[key] = stamped;\n\t}\n\treturn { doc, fields };\n}\n\n/** Which side supplies one field of an 'lww-map' record: the later field clock\n * when both sides stamped it, the stamping side when only one did, else the\n * record-level winner (unstamped fields follow the record). */\nfunction pickFieldSide(\n\ta: [Hlc, number] | null,\n\tb: [Hlc, number] | null,\n\twinnerSide: 'local' | 'remote'\n): 'local' | 'remote' {\n\tif (a && b) return compare(a[0], b[0]) >= 0 ? 'local' : 'remote';\n\tif (a) return 'local';\n\tif (b) return 'remote';\n\treturn winnerSide;\n}\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n\treturn typeof v === 'object' && v !== null && !Array.isArray(v);\n}\n\n/** Which side's value to keep for a record. With both clocks present, the later\n * clock wins (ties to local); otherwise the side that actually holds the doc. */\nfunction pickWinner(aClk: Hlc | null, bClk: Hlc | null, aHas: boolean): 'local' | 'remote' {\n\tif (aClk && bClk) return compare(aClk, bClk) >= 0 ? 'local' : 'remote';\n\treturn aHas ? 'local' : 'remote';\n}\n\nfunction byId(\n\tconfig: SyncConfig,\n\tcollection: string,\n\tdocs: unknown[] | undefined\n): Map<Id, unknown> {\n\tconst m = new Map<Id, unknown>();\n\tfor (const d of docs ?? []) {\n\t\tconst id = idOf(config, collection, d);\n\t\tif (id !== null) m.set(id, d);\n\t}\n\treturn m;\n}\n\n/** What a merge did to one collection, counted by record. */\nexport interface CollectionChanges {\n\tadded: number;\n\tupdated: number;\n\tremoved: number;\n}\n\n/** Per-collection record counts of what changed between two states (same id\n * resolution as the merge). Collections with no change are omitted, so an empty\n * result means the states are equivalent. Feeds the sync journal. */\nexport function changes(\n\tbefore: Record<string, unknown[]>,\n\tafter: Record<string, unknown[]>,\n\tconfig: SyncConfig\n): Record<string, CollectionChanges> {\n\tconst out: Record<string, CollectionChanges> = {};\n\tfor (const name of new Set([...Object.keys(before), ...Object.keys(after)])) {\n\t\tconst b = byId(config, name, before[name]);\n\t\tconst a = byId(config, name, after[name]);\n\t\tlet added = 0;\n\t\tlet updated = 0;\n\t\tlet removed = 0;\n\t\tfor (const [id, doc] of a) {\n\t\t\tif (!b.has(id)) added++;\n\t\t\telse if (hash(doc) !== hash(b.get(id))) updated++;\n\t\t}\n\t\tfor (const id of b.keys()) if (!a.has(id)) removed++;\n\t\tif (added || updated || removed) out[name] = { added, updated, removed };\n\t}\n\treturn out;\n}\n"]}
|