@docstack/pouchdb-adapter-googledrive 0.1.4 → 0.1.6
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/CHANGELOG.md +180 -0
- package/LICENSE +10 -0
- package/README.md +52 -1
- package/lib/adapter.d.ts +0 -0
- package/lib/adapter.js +374 -178
- package/lib/cache.d.ts +0 -0
- package/lib/cache.js +0 -0
- package/lib/client.d.ts +2 -0
- package/lib/client.js +6 -4
- package/lib/drive.d.ts +136 -2
- package/lib/drive.js +670 -89
- package/lib/index.d.ts +0 -0
- package/lib/index.js +0 -0
- package/lib/types.d.ts +34 -4
- package/lib/types.js +0 -0
- package/package.json +7 -3
- package/.env.example +0 -9
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.6 — 2026-08-27
|
|
4
|
+
|
|
5
|
+
A data-loss fix. Every client sharing a folder should be upgraded together; see
|
|
6
|
+
[Compatibility](#compatibility).
|
|
7
|
+
|
|
8
|
+
Background and reasoning, in order:
|
|
9
|
+
[ADR-0001](docs/adr/0001-metadata-writes-without-compare-and-swap.md) (metadata writes
|
|
10
|
+
without compare-and-swap), [ADR-0002](docs/adr/0002-the-folder-is-the-index.md) (the
|
|
11
|
+
folder is the index), [ADR-0003](docs/adr/0003-sequence-numbers-carry-a-writer-slot.md)
|
|
12
|
+
(sequence numbers carry a writer slot). The reports that drove it:
|
|
13
|
+
[docstack-gdrive-adapter-0.1.5.md](docs/adr/docstack-gdrive-adapter-0.1.5.md) and
|
|
14
|
+
[0005-orphaned-change-logs.md](docs/adr/0005-orphaned-change-logs.md). The ADRs live in
|
|
15
|
+
the repository, not in this package.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **A lost metadata update no longer costs a document.** Change logs are found by
|
|
20
|
+
listing the folder, not only by reading `changeLogIds` out of `_meta.json`. The
|
|
21
|
+
file's existence is the durable record that a change log exists; the metadata is a
|
|
22
|
+
cache of that. Reported from Tokido on 2026-08-27 and reproduced locally as
|
|
23
|
+
acknowledged writes going missing in roughly one stress run in five —
|
|
24
|
+
[ADR-0002](docs/adr/0002-the-folder-is-the-index.md).
|
|
25
|
+
|
|
26
|
+
This supersedes the reachability half of the fix below, which could only repair a
|
|
27
|
+
dropped reference if the writer that lost the race ran again. At the end of a
|
|
28
|
+
session it never does.
|
|
29
|
+
|
|
30
|
+
- **Change logs replay in sequence order.** Insertion order was not deterministic
|
|
31
|
+
across clients once metadata merges were in play, so two readers of one folder
|
|
32
|
+
could disagree about which revision of a document won.
|
|
33
|
+
|
|
34
|
+
- **Concurrent writers no longer orphan each other's change logs.** Two clients could
|
|
35
|
+
each read `_meta.json`, append a change log, and write back a `changeLogIds` list
|
|
36
|
+
that did not mention the other's. The losing log stayed in the folder referenced by
|
|
37
|
+
nothing — present in Drive, invisible to every reader, and its documents with it.
|
|
38
|
+
|
|
39
|
+
The adapter believed it was guarding this with an ETag compare-and-swap. Drive API
|
|
40
|
+
v3 has no ETags, so `If-Match` was never sent and would have been ignored if it
|
|
41
|
+
had been; the emulated test server *does* enforce it, which is why the existing
|
|
42
|
+
concurrency tests stayed green. Metadata writes now read fresh, merge instead of
|
|
43
|
+
replace, read back to confirm, and restore any of this writer's logs that go
|
|
44
|
+
missing.
|
|
45
|
+
|
|
46
|
+
- **Compaction no longer deletes change logs it has not confirmed de-referencing.**
|
|
47
|
+
It deleted them straight after a metadata write that nothing had verified. With no
|
|
48
|
+
compare-and-swap, a lost update there means the snapshot never lands *and* the logs
|
|
49
|
+
— still the only copy of everything in them — are gone. A lost update became lost
|
|
50
|
+
documents.
|
|
51
|
+
|
|
52
|
+
- **Two writers can no longer mint the same sequence number.** A sequence number is
|
|
53
|
+
now `tick * 1_000_000 + writerSlot`, the slot being a hash of the writer's id. Two
|
|
54
|
+
clients reading the same counter still derive the same tick — nothing stops that
|
|
55
|
+
without a lock — but they no longer collide on the number itself.
|
|
56
|
+
|
|
57
|
+
This matters because `_changes` filters on `seq > since`: two documents sharing a
|
|
58
|
+
number means the second is never emitted to a replication target again, having
|
|
59
|
+
already been checkpointed past. Allocating from Drive at write time narrowed the
|
|
60
|
+
window and did not close it, because "read the counter, then check it has not
|
|
61
|
+
moved" is not atomic without a compare-and-swap —
|
|
62
|
+
[ADR-0003](docs/adr/0003-sequence-numbers-carry-a-writer-slot.md).
|
|
63
|
+
|
|
64
|
+
Sequence numbers are consequently large and sparse. The first write to a fresh
|
|
65
|
+
folder gets about `1000000`, not `1`.
|
|
66
|
+
|
|
67
|
+
A writer that sees a rival id hashing to its own slot in the folder listing re-rolls
|
|
68
|
+
to a free slot before minting anything, closing the hash-collision residual down to
|
|
69
|
+
the window before the rival's first log is visible.
|
|
70
|
+
|
|
71
|
+
- **Two clients opening the same empty folder converge on one `_meta.json`.** Drive
|
|
72
|
+
allows duplicate names, and clients were choosing between the rivals at random.
|
|
73
|
+
Lowest file id wins; the loser deletes its own and adopts the winner.
|
|
74
|
+
|
|
75
|
+
- **A client no longer replays its own change logs on reload.** The log lines on
|
|
76
|
+
Drive have `nextIndexEntry` stripped, so replaying one overwrote a real merged
|
|
77
|
+
revision tree with a synthesized single-node one, losing ancestry.
|
|
78
|
+
|
|
79
|
+
- **`pollingIntervalMs` works again.** The call site was lost in a refactor in April
|
|
80
|
+
(`2105386`) and the option has done nothing since. Without it a client only hears
|
|
81
|
+
about its own writes, so `db.changes({ live: true })` never fires for a remote one —
|
|
82
|
+
connect-and-read worked, continuous sync between two connected clients did not.
|
|
83
|
+
|
|
84
|
+
Change detection is now by `md5Checksum`, falling back to `modifiedTime`. The ETag
|
|
85
|
+
comparison is gone: it led the chain, so it decided the outcome, and on Drive v3 it
|
|
86
|
+
could only compare `''` against `''`.
|
|
87
|
+
|
|
88
|
+
- **`createFile` and `updateFile` request `md5Checksum`.** They asked for
|
|
89
|
+
`id,modifiedTime`, so the cached checksum was null after every write and polling
|
|
90
|
+
silently degraded to comparing a timestamp the client had set itself.
|
|
91
|
+
|
|
92
|
+
### Changed
|
|
93
|
+
|
|
94
|
+
- **`MetaData` gains `retiredLogIds`** — change logs a compaction has folded into the
|
|
95
|
+
snapshot and deleted. They stop the writer that produced a log from restoring it
|
|
96
|
+
after it is legitimately gone. Capped at 500 entries.
|
|
97
|
+
|
|
98
|
+
- **Change-log filenames carry a writer id**: `changes-<seq>-<writerId>-<random>.ndjson`,
|
|
99
|
+
previously `changes-<seq>-<random>.ndjson`. Nothing parses these names — this is so
|
|
100
|
+
two writers cannot produce the same one, and so an orphan can be traced to whoever
|
|
101
|
+
wrote it.
|
|
102
|
+
|
|
103
|
+
- **A writer catches up before it writes.** If the folder holds logs this client has
|
|
104
|
+
not replayed, it loads them first. For the low-level `appendChange()` API this also
|
|
105
|
+
restores conflict detection, which in production had never run — it was reached
|
|
106
|
+
only from a 412 that Drive never returns. `_bulkDocs` is exempt: it resolves
|
|
107
|
+
revisions through pouchdb-merge and expresses a collision as a conflict branch, so
|
|
108
|
+
failing its whole batch would be the wrong answer.
|
|
109
|
+
|
|
110
|
+
- **Appends cost more Drive calls — 4 became 9.** Reading metadata fresh before
|
|
111
|
+
allocating a sequence range, and reading it back after committing, are what the
|
|
112
|
+
missing compare-and-swap is bought with. Two of the nine are `findFile`'s ETag
|
|
113
|
+
backfill, which can never succeed on Drive v3; removing it takes an append to 7 and
|
|
114
|
+
is the cheapest remaining win.
|
|
115
|
+
|
|
116
|
+
- **`GoogleDriveClient.createFile` and `.updateFile` return `md5Checksum`.** Additive
|
|
117
|
+
— the returned object gained an optional field.
|
|
118
|
+
|
|
119
|
+
- **Loads cost one more `files.list`**, to discover change logs. A load happens on
|
|
120
|
+
connect, on a polling tick that sees a change, and on a write that has to catch up
|
|
121
|
+
with another writer.
|
|
122
|
+
|
|
123
|
+
- **An interrupted write can now become visible.** A change log uploaded by a client
|
|
124
|
+
that died before committing its metadata used to be invisible; it is now found by
|
|
125
|
+
the listing and adopted. Correct for an append-only log, but it is a change in
|
|
126
|
+
which writes survive.
|
|
127
|
+
|
|
128
|
+
### Compatibility
|
|
129
|
+
|
|
130
|
+
- **Sequence numbers jump once, harmlessly.** A folder sitting at `seq: 23` yields a
|
|
131
|
+
next sequence number of about a million — above everything already in the index and
|
|
132
|
+
above any checkpoint a replication target holds. Nothing re-replicates, nothing is
|
|
133
|
+
skipped.
|
|
134
|
+
- **No migration.** `retiredLogIds` is optional and absent metadata reads as an empty
|
|
135
|
+
list. Existing folders work untouched.
|
|
136
|
+
- **A 0.1.5 client can still orphan a 0.1.6 client's logs**, because the bug is in the
|
|
137
|
+
writer, not the format. A 0.1.6 client restores its own logs on its next load or
|
|
138
|
+
commit, so the damage is repaired rather than prevented. Upgrade every writer.
|
|
139
|
+
- **A 0.1.5 client preserves `retiredLogIds`** as it passes through — it copies the
|
|
140
|
+
metadata it downloaded rather than rebuilding it — so a mixed fleet will not strip
|
|
141
|
+
the tombstones.
|
|
142
|
+
|
|
143
|
+
### Known limitations
|
|
144
|
+
|
|
145
|
+
- **Slot collisions are detected and dodged, not impossible.** A writer that sees a
|
|
146
|
+
rival id hashing to its own slot in the folder listing re-rolls to a free slot
|
|
147
|
+
before minting anything. What remains is the window before the rival's first log
|
|
148
|
+
appears in a listing, combined with the 1-in-a-million hash collision itself.
|
|
149
|
+
- **A change that lands below a target's checkpoint is still skipped.** Uniqueness
|
|
150
|
+
stops two documents sharing a number; it does not stop a writer committing a tick it
|
|
151
|
+
allocated before the target checkpointed past it. That needs a serialization point,
|
|
152
|
+
which Drive does not offer.
|
|
153
|
+
- Concurrent writes to the *same document* resolve last-writer-wins in the index
|
|
154
|
+
rather than through pouchdb-merge. Both revisions are on Drive; the losing one does
|
|
155
|
+
not surface as a conflict branch.
|
|
156
|
+
- Two clients that each create a folder by `folderName` get two databases. Duplicate
|
|
157
|
+
folders are not reconciled, only duplicate `_meta.json` files within one.
|
|
158
|
+
|
|
159
|
+
### Tests
|
|
160
|
+
|
|
161
|
+
- `tests/meta_concurrency.test.ts` — eight cases against a fake Drive that behaves
|
|
162
|
+
like the real one: never returns an ETag, never honours one. This is the clean
|
|
163
|
+
reproduction the 0.1.5 report asked for, and it needs neither a Drive account nor
|
|
164
|
+
the client-side id fix first.
|
|
165
|
+
- `tests/writer_stress.test.ts` — three writers, eight rounds, jittered latency, and
|
|
166
|
+
a writer that vanishes mid-write. The invariant is the load-bearing one: *an append
|
|
167
|
+
whose promise resolved must be readable by whoever comes next.* This is what caught
|
|
168
|
+
the defect ADR-0002 fixes.
|
|
169
|
+
- `tests/simultaneous_writers.test.ts` — overlapping appends, as opposed to the
|
|
170
|
+
stale-writer shape the other suites cover.
|
|
171
|
+
- `tests/polling.test.ts` — five cases, `pollingIntervalMs`'s first tests.
|
|
172
|
+
- `tests/fake-drive.ts` — the shared fake: real md5 hashing, latency jitter, a
|
|
173
|
+
`sever()` for simulating a context that goes away, and a `diagnose()` that tells an
|
|
174
|
+
orphaned log apart from a dangling reference.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 0.1.5 and earlier
|
|
179
|
+
|
|
180
|
+
Not recorded. See `git log`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)
|
|
2
|
+
|
|
3
|
+
Copyright (c) Onyx <hello@onyx.ac> (https://onyx.ac)
|
|
4
|
+
|
|
5
|
+
This work is licensed under the Creative Commons Attribution-ShareAlike 4.0
|
|
6
|
+
International License. You are free to share and adapt this work, including
|
|
7
|
+
commercially, provided you give appropriate attribution and distribute any
|
|
8
|
+
derivative works under the same license.
|
|
9
|
+
|
|
10
|
+
Full legal text: https://creativecommons.org/licenses/by-sa/4.0/legalcode
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ A persistent, serverless PouchDB adapter that uses Google Drive as a backend sto
|
|
|
6
6
|
|
|
7
7
|
- **🚀 Append-Only Log**: Uses an efficient append-only log pattern for fast, conflict-free writes.
|
|
8
8
|
- **⚡ Lazy Loading**: Optimizes memory and bandwidth by loading only the **Index** into memory. Document bodies are fetched on-demand.
|
|
9
|
-
- **🛡️
|
|
9
|
+
- **🛡️ Multi-writer safe**: Metadata writes merge rather than replace, and are read back before anything is deleted. See [Concurrent writers](#concurrent-writers).
|
|
10
10
|
- **📦 Auto-Compaction**: Automatically merges logs for performance.
|
|
11
11
|
- **🌍 Universal**: Works natively in Node.js 18+, Browsers, and Edge environments (no `googleapis` dependency).
|
|
12
12
|
|
|
@@ -60,6 +60,57 @@ const adapterPlugin = GoogleDriveAdapter({
|
|
|
60
60
|
});
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
+
### Live changes from other clients
|
|
64
|
+
|
|
65
|
+
Set `pollingIntervalMs` to have the adapter watch `_meta.json` and replay what other
|
|
66
|
+
clients write. Without it, a client only hears about its own writes — connect-and-read
|
|
67
|
+
works, continuous sync between two connected clients does not.
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
const adapterPlugin = GoogleDriveAdapter({
|
|
71
|
+
accessToken: 'YOUR_GOOGLE_ACCESS_TOKEN',
|
|
72
|
+
folderId: 'my-folder-id',
|
|
73
|
+
pollingIntervalMs: 2000 // check for remote writes every 2s
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
db.changes({ live: true, since: 'now' }).on('change', change => { /* ... */ });
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
A tick costs one `files.list`, whatever has changed; only a tick that sees a new
|
|
80
|
+
`md5Checksum` (or, failing that, a new `modifiedTime`) goes on to fetch anything.
|
|
81
|
+
`db.close()` and `db.destroy()` stop it.
|
|
82
|
+
|
|
83
|
+
## Concurrent writers
|
|
84
|
+
|
|
85
|
+
Several clients may share one folder. What that costs, and what it does not:
|
|
86
|
+
|
|
87
|
+
- **Writes from different clients do not overwrite each other.** `_meta.json` is the
|
|
88
|
+
only shared mutable state. Every write to it is built on a copy read from Drive
|
|
89
|
+
moments earlier, merges into that copy instead of replacing it, and is read back
|
|
90
|
+
afterwards to confirm it survived. A writer also remembers the change logs it
|
|
91
|
+
wrote and restores any that go missing on its next load.
|
|
92
|
+
- **Compaction deletes nothing until its metadata write is confirmed.** Until then,
|
|
93
|
+
the change logs are still the only copy of the changes in them.
|
|
94
|
+
- **Sequence numbers come from Drive, not from a local counter**, so two clients
|
|
95
|
+
writing minutes apart cannot mint the same one.
|
|
96
|
+
- **A writer catches up before it writes**, so it sees what its peers have appended.
|
|
97
|
+
A client that only *reads* needs `pollingIntervalMs` to notice anything.
|
|
98
|
+
|
|
99
|
+
Caveats worth knowing before you fan out:
|
|
100
|
+
|
|
101
|
+
- **There is no compare-and-swap.** Drive API v3 dropped ETags, so `If-Match` is
|
|
102
|
+
accepted and ignored - the adapter still sends it (the emulated test server does
|
|
103
|
+
enforce it), but nothing may assume it was honoured. Two clients writing metadata
|
|
104
|
+
within the same round trip can still produce a lost update; the read-back and the
|
|
105
|
+
restore-on-load above are what repair it, so a client that writes and immediately
|
|
106
|
+
disconnects forever is the one case that can leave a change log unreferenced.
|
|
107
|
+
- **Pass `folderId`, not just `folderName`, when several clients may start at once.**
|
|
108
|
+
Duplicate `_meta.json` files are detected and resolved; duplicate *folders* are
|
|
109
|
+
not, and two clients that each create "my-app-db-folder" get two databases.
|
|
110
|
+
- **A document written concurrently by two clients resolves last-writer-wins in the
|
|
111
|
+
index**, not through pouchdb-merge. Both revisions stay on Drive, but the losing
|
|
112
|
+
one will not show up as a conflict branch.
|
|
113
|
+
|
|
63
114
|
## Architecture
|
|
64
115
|
|
|
65
116
|
The adapter implements a **"Remote-First"** architecture:
|
package/lib/adapter.d.ts
CHANGED
|
File without changes
|