@docstack/pouchdb-adapter-googledrive 0.1.5 → 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/lib/drive.d.ts CHANGED
@@ -1,4 +1,25 @@
1
- import { GoogleDriveAdapterOptions, ChangeEntry, IndexEntry } from './types';
1
+ import { GoogleDriveAdapterOptions, ChangeEntry, IndexEntry, FilePointer } from './types';
2
+ /**
3
+ * Sequence numbers are `tick * SEQ_SLOTS + writerSlot`.
4
+ *
5
+ * Writers mint them blind to one another - there is no lock and no compare-and-swap -
6
+ * so two clients reading the same counter will always be able to derive the same next
7
+ * tick. Reserving the low digits for a per-writer slot means they can share a tick and
8
+ * still never share a *sequence number*, which is the part that matters: `_changes`
9
+ * filters on `seq > since`, so a second document sharing a checkpointed sequence
10
+ * number is never emitted to a replication target again.
11
+ *
12
+ * A million slots keeps the chance that two concurrent writers hash to the same one
13
+ * near 1 in 20,000 for a ten-client fleet, and leaves room for 9e9 ticks inside
14
+ * Number.MAX_SAFE_INTEGER.
15
+ */
16
+ export declare const SEQ_SLOTS = 1000000;
17
+ /** Stable slot for a writer id. FNV-1a, folded into the slot space. */
18
+ export declare function writerSlotFor(writerId: string): number;
19
+ /** The writer id embedded in a change-log filename, if the name carries one.
20
+ * New format: changes-<seq>-<writerId>-<random>.ndjson (4 dash-parts).
21
+ * Old format: changes-<seq>-<random>.ndjson (3 parts) - no id to extract. */
22
+ export declare function writerIdFromLogName(name: string): string | null;
2
23
  /**
3
24
  * DriveHandler - Lazy Loading Implementation
4
25
  *
@@ -22,6 +43,27 @@ export declare class DriveHandler {
22
43
  private metaMd5;
23
44
  private metaModifiedTime;
24
45
  private localDocsEtag;
46
+ private metaFileId;
47
+ /** Identifies this handler among the writers sharing a folder. Change-log
48
+ * filenames carry it, so two writers can never produce the same name and an
49
+ * orphaned log can be traced back to whoever wrote it. Not readonly: if the
50
+ * folder shows another writer whose id hashes to our sequence slot, we re-roll
51
+ * to a free one before minting anything (see rerollIfSlotContested). */
52
+ private writerId;
53
+ /** Change-log file ids this handler wrote, minus any a compaction has retired.
54
+ * Drive API v3 has no compare-and-swap (ETags were dropped), so another
55
+ * writer's read-modify-write of _meta.json can drop a log id that landed in
56
+ * between. Anything still in here but missing from the remote changeLogIds was
57
+ * dropped that way and gets put back - see reconcileOwnLogs(). */
58
+ private ownLogIds;
59
+ /** This writer's reservation in the low digits of every sequence number it mints.
60
+ * Derived from writerId; changes only when writerId is re-rolled off a
61
+ * contested slot. */
62
+ private writerSlot;
63
+ /** Serializes this handler's own _meta.json read-modify-write cycles. Says
64
+ * nothing about other clients - that is what commitMeta's verify pass is for -
65
+ * but stops one handler racing itself when several writes are in flight. */
66
+ private metaLock;
25
67
  private index;
26
68
  private docCache;
27
69
  private pendingChanges;
@@ -48,6 +90,15 @@ export declare class DriveHandler {
48
90
  * Index -> Cache -> Fetch
49
91
  */
50
92
  get(id: string): Promise<any | null>;
93
+ /**
94
+ * Fetch a specific (possibly non-winning/conflicting) revision's body by its
95
+ * own tracked location - used by _get(opts.rev)/_bulkGet when the requested
96
+ * rev isn't the current winner. Deliberately doesn't touch docCache (keyed
97
+ * per-id, not per-rev - a conflict fetch is rare enough not to warrant a
98
+ * per-rev cache key) and doesn't force `_rev` to the index's winning rev the
99
+ * way get() does.
100
+ */
101
+ getRevisionBody(id: string, rev: string, location: FilePointer): Promise<any | null>;
51
102
  /** Generic Download with Caching and Parsing */
52
103
  private fetchFile;
53
104
  /** Get multiple docs (Atomic-ish) used for _allDocs */
@@ -62,21 +113,104 @@ export declare class DriveHandler {
62
113
  appendChanges(changes: ChangeEntry[]): Promise<void>;
63
114
  private appendLocalDocs;
64
115
  private tryAppendChanges;
116
+ /** Forget a change log we uploaded but never managed to reference from
117
+ * _meta.json. Nothing points at it, so it is safe to remove. */
118
+ private discardLog;
65
119
  /** Update Index with a new change */
66
120
  private updateIndex;
67
121
  private checkConflicts;
68
122
  /** Compact: Create SnapshotIndex + SnapshotData */
69
123
  compact(): Promise<void>;
70
- private atomicUpdateMeta;
124
+ /** Run `fn` after every meta mutation this handler has already queued has
125
+ * settled. Never call this from inside a commitMeta modifier - it would wait on
126
+ * itself. */
127
+ private withMetaLock;
128
+ private backoff;
129
+ /** Locate _meta.json and read its body straight from Drive, past every cache. */
130
+ private readRemoteMeta;
131
+ /** Read a known _meta.json by id - no folder listing, no cache. Used by the
132
+ * post-write verification pass, which only needs the body. */
133
+ private readMetaBody;
134
+ private adoptMeta;
135
+ /** True when the folder holds changes this handler has not replayed - another
136
+ * writer appended, or compacted, since our last load. */
137
+ private hasUnprocessedLogs;
138
+ /** True when a change log we wrote has fallen out of `changeLogIds` without a
139
+ * compaction retiring it - i.e. someone else's write dropped it. */
140
+ private hasOrphanedOwnLogs;
141
+ /** Put back any such log. Returns `latest` by identity when there is nothing to
142
+ * repair, so callers can tell the two cases apart. */
143
+ private reconcileOwnLogs;
144
+ /**
145
+ * Read-modify-write `_meta.json`.
146
+ *
147
+ * `modify` receives the current remote metadata (with any of our dropped logs
148
+ * already restored, which `repaired` reports) and returns the value to write, or
149
+ * null to abandon the commit because its assumptions no longer hold. This method
150
+ * also returns null when it runs out of attempts. Either way nothing durable has
151
+ * changed and the caller must not act as though it had.
152
+ */
153
+ private commitMeta;
154
+ /** Create _meta.json for a folder that has none. Two clients opening the same
155
+ * empty folder both end up here and Drive will happily keep two files with the
156
+ * same name, after which every client picks between them at random. Settle it
157
+ * deterministically instead: lowest file id wins, the loser deletes its own and
158
+ * adopts the winner. */
159
+ private ensureMetaFile;
71
160
  private findOrCreateFolder;
161
+ /** Every change log in the folder, whatever _meta.json has to say about them.
162
+ *
163
+ * The folder is the authority on which change logs exist; _meta.json is only a
164
+ * cache of that, and a lossy one - it is a whole-file read-modify-write with no
165
+ * compare-and-swap behind it, so a writer whose metadata lands and is then
166
+ * overwritten by a slower writer loses its reference. The file is still right
167
+ * here. Listing for it is what stops a lost update from becoming a lost
168
+ * document. */
169
+ private listChangeLogs;
170
+ /**
171
+ * Give up a sequence slot another writer is already using.
172
+ *
173
+ * Slots make sequence collisions structurally impossible only between writers on
174
+ * *different* slots; two ids hashing to the same slot are back to the dense
175
+ * allocation this scheme replaced. The filenames the folder listing hands us
176
+ * carry every writer's id, so a contested slot is visible - and since this
177
+ * handler re-rolls before minting anything against what it just saw, the
178
+ * exposure shrinks from "the whole session" to "rival's first log not yet
179
+ * visible in a listing".
180
+ *
181
+ * Logs already written keep their old name and numbers; ownLogIds tracks file
182
+ * ids, not names, so nothing else cares.
183
+ */
184
+ private rerollIfSlotContested;
185
+ /** Every file in the folder with this name. Drive allows duplicates, so this is
186
+ * how the callers that care (see ensureMetaFile) find out there are any. */
187
+ private findFiles;
72
188
  private findFile;
73
189
  private downloadJson;
74
190
  private downloadFileAny;
75
191
  private downloadNdjson;
76
192
  private writeChangeFile;
193
+ /** Write `meta` to _meta.json. `target` is the file to write, as already
194
+ * located by the caller; pass null to force creation, or omit it to look the
195
+ * file up. */
77
196
  private saveMeta;
78
197
  private countTotalChanges;
79
198
  private cleanupOldFiles;
199
+ /**
200
+ * Watch _meta.json for writes by other clients.
201
+ *
202
+ * This is the only thing that makes `db.changes({ live: true })` fire for a
203
+ * *remote* write: on a change it calls load(), which replays the newly
204
+ * referenced logs and emits exactly those through notifyListeners. Without it a
205
+ * client only ever hears about what it wrote itself, so connect-and-read works
206
+ * and continuous sync between two connected clients does not.
207
+ *
208
+ * Change is detected by md5Checksum, falling back to modifiedTime. There is
209
+ * deliberately no ETag comparison: Drive API v3 has none (see
210
+ * docs/adr/0001-metadata-writes-without-compare-and-swap.md), so that branch
211
+ * could only ever compare '' against '' - and sitting first in the chain, it
212
+ * shadowed the two comparisons that do work.
213
+ */
80
214
  private startPolling;
81
215
  private notifyListeners;
82
216
  onChange(cb: (changes: Record<string, any>) => void): () => void;