@docstack/client 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/LICENSE +10 -0
- package/README.md +1 -1
- package/lib/core/attribute.d.ts +11 -2
- package/lib/core/class.d.ts +40 -6
- package/lib/core/content-transfer.d.ts +178 -0
- package/lib/core/crypto-engine/index.d.ts +82 -1
- package/lib/core/crypto-engine/utils.d.ts +36 -1
- package/lib/core/datamodel/index.d.ts +16 -0
- package/lib/core/domain.d.ts +1 -1
- package/lib/core/guarded-db.d.ts +52 -0
- package/lib/core/index.d.ts +106 -1
- package/lib/core/policy-engine/index.d.ts +35 -0
- package/lib/core/query-engine/classes.d.ts +24 -0
- package/lib/core/query-engine/executor.d.ts +11 -0
- package/lib/core/query-engine/index.d.ts +2 -1
- package/lib/core/query-engine/planner.d.ts +19 -0
- package/lib/core/stack.d.ts +676 -7
- package/lib/core/sync/class-filter.d.ts +106 -0
- package/lib/core/sync/filter-identity.d.ts +53 -0
- package/lib/core/sync/index.d.ts +334 -0
- package/lib/core/sync/internal-docs.d.ts +159 -0
- package/lib/index.d.ts +27 -1
- package/lib/index.js +12686 -4
- package/lib/index.umd.js +5017 -620
- package/lib/plugins/pouchdb.d.ts +43 -3
- package/lib/utils/logger/index.d.ts +28 -4
- package/lib/utils/logger/transport.d.ts +52 -11
- package/package.json +18 -10
- package/lib/core/attribute.js +0 -406
- package/lib/core/attribute.js.map +0 -1
- package/lib/core/class.js +0 -761
- package/lib/core/class.js.map +0 -1
- package/lib/core/crypto-engine/index.js +0 -229
- package/lib/core/crypto-engine/index.js.map +0 -1
- package/lib/core/crypto-engine/utils.js +0 -88
- package/lib/core/crypto-engine/utils.js.map +0 -1
- package/lib/core/datamodel/index.js +0 -1308
- package/lib/core/datamodel/index.js.map +0 -1
- package/lib/core/domain.js +0 -423
- package/lib/core/domain.js.map +0 -1
- package/lib/core/index.js +0 -532
- package/lib/core/index.js.map +0 -1
- package/lib/core/job-engine/index.js +0 -220
- package/lib/core/job-engine/index.js.map +0 -1
- package/lib/core/policy-engine/index.js +0 -232
- package/lib/core/policy-engine/index.js.map +0 -1
- package/lib/core/query-engine/accumulators.js +0 -258
- package/lib/core/query-engine/accumulators.js.map +0 -1
- package/lib/core/query-engine/evaluator.js +0 -179
- package/lib/core/query-engine/evaluator.js.map +0 -1
- package/lib/core/query-engine/executor.js +0 -405
- package/lib/core/query-engine/executor.js.map +0 -1
- package/lib/core/query-engine/index.js +0 -4
- package/lib/core/query-engine/index.js.map +0 -1
- package/lib/core/query-engine/parser.js +0 -515
- package/lib/core/query-engine/parser.js.map +0 -1
- package/lib/core/query-engine/planner.js +0 -330
- package/lib/core/query-engine/planner.js.map +0 -1
- package/lib/core/stack.js +0 -1826
- package/lib/core/stack.js.map +0 -1
- package/lib/core/test-utils/docstack.js +0 -222
- package/lib/core/test-utils/docstack.js.map +0 -1
- package/lib/core/trigger/index.js +0 -81
- package/lib/core/trigger/index.js.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/plugins/pouchdb.js +0 -412
- package/lib/plugins/pouchdb.js.map +0 -1
- package/lib/utils/crypto/index.js +0 -34
- package/lib/utils/crypto/index.js.map +0 -1
- package/lib/utils/index.js +0 -58
- package/lib/utils/index.js.map +0 -1
- package/lib/utils/logger/index.js +0 -20
- package/lib/utils/logger/index.js.map +0 -1
- package/lib/utils/logger/transport.js +0 -28
- package/lib/utils/logger/transport.js.map +0 -1
- package/lib/workers/dataModel.js +0 -48
- package/lib/workers/dataModel.js.map +0 -1
package/lib/core/stack.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import Class from "./class.js";
|
|
2
2
|
import Domain from "./domain.js";
|
|
3
|
-
import { Stack, StackOptions, AuthSessionProof, ClientCredentials, CachedClass, ClassModelPropagationStart, ClassModelPropagationComplete, CachedDomain, DomainModel } from "@docstack/shared";
|
|
3
|
+
import { Stack, StackOptions, AuthSessionProof, ClientCredentials, CachedClass, ClassModelPropagationStart, ClassModelPropagationComplete, CachedDomain, DomainModel, ChangesSubscription } from "@docstack/shared";
|
|
4
4
|
import { SystemDoc, Patch, ClassModel, Document, RelationDocument } from "@docstack/shared";
|
|
5
|
+
import { StackSyncHandle } from "./sync/index.js";
|
|
6
|
+
import type { StackSyncOptions, SyncStatus } from "./sync/index.js";
|
|
5
7
|
import type { SelectAST, UnionAST } from "./query-engine/index.js";
|
|
6
8
|
import { JobEngine } from "./job-engine/index.js";
|
|
7
9
|
import { PolicyEngine } from "./policy-engine/index.js";
|
|
8
10
|
import { CryptoEngine } from "./crypto-engine/index.js";
|
|
11
|
+
import type { ContentExport, ContentExportOptions, ContentImportOptions, ContentImportReport } from "./content-transfer.js";
|
|
9
12
|
export declare const BASE_SCHEMA: ClassModel["schema"];
|
|
10
13
|
export declare const CLASS_SCHEMA: ClassModel["schema"];
|
|
11
14
|
/**
|
|
@@ -45,6 +48,22 @@ declare class ClientStack extends Stack {
|
|
|
45
48
|
* ```
|
|
46
49
|
*/
|
|
47
50
|
db: PouchDB.Database<{}>;
|
|
51
|
+
/**
|
|
52
|
+
* The unguarded PouchDB instance. Only the sync layer reaches for it, through
|
|
53
|
+
* {@link getReplicationHandle}.
|
|
54
|
+
* @internal
|
|
55
|
+
*/
|
|
56
|
+
private rawDb;
|
|
57
|
+
/**
|
|
58
|
+
* `bulkDocs`/`bulkGet` as PouchDB defines them, captured before {@link StackPlugin}
|
|
59
|
+
* replaces them.
|
|
60
|
+
* @internal
|
|
61
|
+
*/
|
|
62
|
+
private pristineDbMethods;
|
|
63
|
+
/** Memoised {@link getReplicationHandle} result. @internal */
|
|
64
|
+
private replicationDb?;
|
|
65
|
+
/** The stack's replication, once {@link sync} has been called. @internal */
|
|
66
|
+
private syncHandle?;
|
|
48
67
|
/** The unique name identifier for this stack instance, derived from the connection string. */
|
|
49
68
|
name: string;
|
|
50
69
|
lastDocId: number;
|
|
@@ -62,7 +81,57 @@ declare class ClientStack extends Stack {
|
|
|
62
81
|
[className: string]: CachedClass | CachedDomain;
|
|
63
82
|
};
|
|
64
83
|
patchCount: number;
|
|
65
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Stored class models, keyed by whatever the caller looked them up with (name or id).
|
|
86
|
+
*
|
|
87
|
+
* `getClassModel` runs at least once per document on every read and write path (the
|
|
88
|
+
* policy engine resolves each document's class through it), and each miss is an
|
|
89
|
+
* unindexed find. Entries include `null` for names that resolved to nothing - the
|
|
90
|
+
* caches are cleared whenever a class document is written, so a class created later
|
|
91
|
+
* evicts its own negative entry.
|
|
92
|
+
*
|
|
93
|
+
* Invalidated as a pair with {@link classSnapshotCache}: synchronously by the write
|
|
94
|
+
* path (StackPlugin calls {@link invalidateWriteCaches} after every batch that
|
|
95
|
+
* touches a class model) and again by the shared changes feed for writes this stack
|
|
96
|
+
* did not make itself. Cleared wholesale rather than per-entry because a rename
|
|
97
|
+
* leaves the old name keyed to a model that no longer answers to it.
|
|
98
|
+
*/
|
|
99
|
+
private classModelCache;
|
|
100
|
+
/**
|
|
101
|
+
* Built, non-subscribing Class instances, keyed by class name and pinned to the
|
|
102
|
+
* model revision they were built from.
|
|
103
|
+
*
|
|
104
|
+
* Building a Class rebuilds every Attribute and re-derives the Zod schema, per
|
|
105
|
+
* attribute - work `getClassSnapshot` used to repeat on every call. The `_rev` pin
|
|
106
|
+
* means a stale entry can never be served even if invalidation lags: the model is
|
|
107
|
+
* looked up first (through {@link classModelCache}) and the snapshot is reused only
|
|
108
|
+
* when its revision still matches.
|
|
109
|
+
*/
|
|
110
|
+
private classSnapshotCache;
|
|
111
|
+
/**
|
|
112
|
+
* Every live changes subscription this stack has handed out; released on close.
|
|
113
|
+
*/
|
|
114
|
+
listeners: ChangesSubscription[];
|
|
115
|
+
/**
|
|
116
|
+
* The one live feed behind every class-document subscription.
|
|
117
|
+
*
|
|
118
|
+
* PouchDB attaches a `destroyed` listener to the database for each
|
|
119
|
+
* `db.changes({ live: true })` and holds it until that feed is cancelled, so a feed
|
|
120
|
+
* per watched class crosses Node's ten-listener limit - and prints
|
|
121
|
+
* `MaxListenersExceededWarning` - on an app with a handful of classes on screen. It
|
|
122
|
+
* is also wasted work: the local adapter runs every filter over the same change
|
|
123
|
+
* stream, so N filtered feeds each see every change anyway.
|
|
124
|
+
*/
|
|
125
|
+
private classDocFeed?;
|
|
126
|
+
/**
|
|
127
|
+
* Change handlers, keyed `"<metaKey>:<name>"`. Empty means {@link classDocFeed} can be
|
|
128
|
+
* cancelled.
|
|
129
|
+
*
|
|
130
|
+
* The meta key is part of the key because classes and domains are separate namespaces:
|
|
131
|
+
* a relation document is named by `~domain` and carries no `~class`, so routing both
|
|
132
|
+
* through one keyspace would let a class and a same-named domain hear each other.
|
|
133
|
+
*/
|
|
134
|
+
private classDocSubscribers;
|
|
66
135
|
modelWorker: Worker | null;
|
|
67
136
|
/**
|
|
68
137
|
* Engine for executing background jobs and scheduled tasks.
|
|
@@ -92,13 +161,91 @@ declare class ClientStack extends Stack {
|
|
|
92
161
|
*/
|
|
93
162
|
authSession?: AuthSessionProof;
|
|
94
163
|
private cryptoEngineDisabled;
|
|
164
|
+
/**
|
|
165
|
+
* Application patches held back because they write encrypted attributes and the stack
|
|
166
|
+
* has no key yet. Replayed by {@link unlock}. Kept in memory on purpose - reopening
|
|
167
|
+
* the stack runs the same options through the same check.
|
|
168
|
+
*/
|
|
169
|
+
private deferredPatches;
|
|
95
170
|
private constructor();
|
|
96
171
|
private initialize;
|
|
97
172
|
/**
|
|
98
|
-
* Returns the
|
|
99
|
-
*
|
|
173
|
+
* Returns the stack's PouchDB database handle.
|
|
174
|
+
*
|
|
175
|
+
* The handle is guarded: reads and ordinary writes behave exactly as PouchDB
|
|
176
|
+
* documents them, and `put`/`post`/`remove`/`bulkDocs` all run the stack's
|
|
177
|
+
* authoring path. The two routes that would skip it - `bulkDocs` with
|
|
178
|
+
* `new_edits: false`, and the `_`-prefixed adapter methods - throw
|
|
179
|
+
* {@link StackWriteGuardError}; replicating into a stack goes through
|
|
180
|
+
* {@link sync} instead.
|
|
181
|
+
*
|
|
182
|
+
* @returns The guarded PouchDB database
|
|
100
183
|
*/
|
|
101
184
|
getDb(): PouchDB.Database<{}>;
|
|
185
|
+
/**
|
|
186
|
+
* Returns the database with PouchDB's own `bulkDocs`/`bulkGet` restored, for the
|
|
187
|
+
* sync layer's exclusive use.
|
|
188
|
+
*
|
|
189
|
+
* Replication needs both halves of the plugin out of the way: it writes documents
|
|
190
|
+
* with revisions it already owns (`new_edits: false`), and it must read documents
|
|
191
|
+
* exactly as they are stored - the plugin's `bulkGet` decrypts on read, which would
|
|
192
|
+
* push plaintext to a remote that is meant to hold ciphertext.
|
|
193
|
+
*
|
|
194
|
+
* @returns A handle suitable for `PouchDB.replicate`/`PouchDB.sync`
|
|
195
|
+
* @internal
|
|
196
|
+
*/
|
|
197
|
+
getReplicationHandle(): PouchDB.Database<{}>;
|
|
198
|
+
/**
|
|
199
|
+
* Starts replicating this stack against a remote.
|
|
200
|
+
*
|
|
201
|
+
* DocStack owns the lifecycle - the filter that keeps `~system`, the crypto marker,
|
|
202
|
+
* design documents, locks, sessions and the patch ledger on this device; the schema
|
|
203
|
+
* gate that refuses a remote written by a newer build; the convergence state a UI
|
|
204
|
+
* renders; and cancellation when the stack closes. It owns nothing about the
|
|
205
|
+
* transport: the remote is whatever PouchDB database the caller hands over, so
|
|
206
|
+
* credentials and adapter configuration stay in the application.
|
|
207
|
+
*
|
|
208
|
+
* Calling it again replaces the previous replication.
|
|
209
|
+
*
|
|
210
|
+
* @param options - See {@link StackSyncOptions}.
|
|
211
|
+
* @returns The handle, once replication is running.
|
|
212
|
+
* @throws {SyncSchemaMismatchError} When the remote was last written by a newer schema.
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* ```typescript
|
|
216
|
+
* const sync = await stack.sync({
|
|
217
|
+
* remote: () => new PouchDB("workspace", { adapter: "googledrive", accessToken }),
|
|
218
|
+
* direction: "both",
|
|
219
|
+
* live: true,
|
|
220
|
+
* retry: true,
|
|
221
|
+
* });
|
|
222
|
+
*
|
|
223
|
+
* sync.addEventListener("status", (event) => {
|
|
224
|
+
* console.log((event as CustomEvent).detail.state);
|
|
225
|
+
* });
|
|
226
|
+
* ```
|
|
227
|
+
*/
|
|
228
|
+
sync(options: StackSyncOptions): Promise<StackSyncHandle>;
|
|
229
|
+
/**
|
|
230
|
+
* Returns this stack's replication handle, or `null` if {@link sync} was never called.
|
|
231
|
+
*/
|
|
232
|
+
getSyncHandle(): StackSyncHandle | null;
|
|
233
|
+
/**
|
|
234
|
+
* Returns where this stack's replication stands, or `null` if it has none.
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```typescript
|
|
238
|
+
* const status = stack.getSyncStatus();
|
|
239
|
+
* if (status?.lastConvergedAt) {
|
|
240
|
+
* ui.setLabel(`Synced ${formatAgo(status.lastConvergedAt)}`);
|
|
241
|
+
* }
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
244
|
+
getSyncStatus(): SyncStatus | null;
|
|
245
|
+
/**
|
|
246
|
+
* Stops this stack's replication. Idempotent; called automatically by {@link close}.
|
|
247
|
+
*/
|
|
248
|
+
cancelSync(): void;
|
|
102
249
|
/**
|
|
103
250
|
* Retrieves information about the database including document count and update sequence.
|
|
104
251
|
* @returns Database information object
|
|
@@ -119,18 +266,207 @@ declare class ClientStack extends Stack {
|
|
|
119
266
|
* Called automatically by {@link authenticate}, but can be set manually for custom auth flows.
|
|
120
267
|
* @param proof - The authentication session proof containing session and encryption keys
|
|
121
268
|
*/
|
|
122
|
-
setAuthSession(proof: AuthSessionProof): void
|
|
269
|
+
setAuthSession(proof: AuthSessionProof): Promise<void>;
|
|
123
270
|
/**
|
|
124
271
|
* Clears the current authentication session and removes the document encryption key.
|
|
125
272
|
* Call this when a user logs out.
|
|
126
273
|
*/
|
|
127
274
|
clearAuthSession(): void;
|
|
275
|
+
/**
|
|
276
|
+
* Whether the stack is operating without its document encryption key.
|
|
277
|
+
*
|
|
278
|
+
* A locked stack reads everything that needs no key and refuses writes to classes
|
|
279
|
+
* carrying encrypted attributes, rather than storing them in the clear. Patches that
|
|
280
|
+
* would write encrypted data are deferred until {@link unlock}. Stacks with the crypto
|
|
281
|
+
* engine disabled are never locked - there is no key to be missing.
|
|
282
|
+
*
|
|
283
|
+
* @returns `true` when encryption is enabled but no key is held.
|
|
284
|
+
*/
|
|
285
|
+
isLocked(): boolean;
|
|
286
|
+
/**
|
|
287
|
+
* Supplies the document encryption key to a locked stack.
|
|
288
|
+
*
|
|
289
|
+
* The key is checked against the stack's canary before it is accepted, so passing the
|
|
290
|
+
* wrong one throws instead of quietly producing unreadable writes. On the first unlock
|
|
291
|
+
* of a stack that has none, the canary is minted from the key given - which is what
|
|
292
|
+
* makes every later open verifiable.
|
|
293
|
+
*
|
|
294
|
+
* Unlocking resumes any bootstrap deferred while locked, then emits `unlocked`.
|
|
295
|
+
*
|
|
296
|
+
* @param documentKey - The hex-encoded document key, from wherever the application
|
|
297
|
+
* provisions it.
|
|
298
|
+
* @throws If the stack has encryption disabled, or the key does not match the canary.
|
|
299
|
+
*
|
|
300
|
+
* @example
|
|
301
|
+
* ```typescript
|
|
302
|
+
* const stack = await ClientStack.create('db-app'); // opens locked
|
|
303
|
+
* await stack.unlock(await myServer.fetchDocumentKey());
|
|
304
|
+
* stack.isLocked(); // false
|
|
305
|
+
* ```
|
|
306
|
+
*/
|
|
307
|
+
unlock(documentKey: string): Promise<this>;
|
|
308
|
+
/**
|
|
309
|
+
* Encrypts bootstrap documents that were seeded before this stack had a key.
|
|
310
|
+
*
|
|
311
|
+
* The seed system user is the one document DocStack must write before a key can
|
|
312
|
+
* exist: the first open of a database has no wrapped key to recover one from, and
|
|
313
|
+
* refusing to seed it would leave nothing to authenticate against. It is therefore
|
|
314
|
+
* written in the clear - its password is the published constant `"system"`, so
|
|
315
|
+
* nothing secret is exposed - and repaired here.
|
|
316
|
+
*
|
|
317
|
+
* Rewriting it through the authoring path encrypts its attributes and lets
|
|
318
|
+
* `auto-wrap-document-key` run for the first time, producing the
|
|
319
|
+
* `wrappedDocumentKey` that lets another device recover this same document key.
|
|
320
|
+
* Without this step a stack bootstrapped locked could never authenticate, because
|
|
321
|
+
* the trigger no-ops when no key is held. See ADR-0018.
|
|
322
|
+
*/
|
|
323
|
+
private rekeyBootstrapDocuments;
|
|
128
324
|
/**
|
|
129
325
|
* Exports all documents from the database.
|
|
130
326
|
* Useful for debugging or creating backups.
|
|
131
327
|
* @returns All documents including their content
|
|
132
328
|
*/
|
|
133
329
|
dump: () => Promise<PouchDB.Core.AllDocsResponse<{}>>;
|
|
330
|
+
/**
|
|
331
|
+
* Lists the content classes and domains this stack holds.
|
|
332
|
+
*
|
|
333
|
+
* "Content" means created by an application: DocStack's own classes are `~`-prefixed
|
|
334
|
+
* and its datamodel documents use the reserved names in `META_CLASSES`.
|
|
335
|
+
*
|
|
336
|
+
* @returns The class names and domain names an export would cover.
|
|
337
|
+
*/
|
|
338
|
+
/**
|
|
339
|
+
* Every class name in this stack, DocStack's own included.
|
|
340
|
+
*
|
|
341
|
+
* The fail-open path for a live query: when the classes a query reads cannot be
|
|
342
|
+
* determined from its AST, watching all of them is wasteful but correct, and watching
|
|
343
|
+
* none is silently wrong. Cheap to act on - subscriptions share one database
|
|
344
|
+
* listener, so the cost is a set entry per class rather than a feed. See ADR-0025.
|
|
345
|
+
*
|
|
346
|
+
* For the application's classes alone, use {@link getContentClassNames}.
|
|
347
|
+
*
|
|
348
|
+
* @returns The class names, sorted.
|
|
349
|
+
*/
|
|
350
|
+
/**
|
|
351
|
+
* The names of classes whose documents are local to one run.
|
|
352
|
+
*
|
|
353
|
+
* See {@link ClassModel.ephemeral}. Resolved from the stored models rather than a
|
|
354
|
+
* hardcoded list, so an application that declares its own ephemeral class - a cache,
|
|
355
|
+
* a derived view, anything a peer neither needs nor should receive - is covered
|
|
356
|
+
* without DocStack knowing about it.
|
|
357
|
+
*
|
|
358
|
+
* @returns The ephemeral class names, sorted.
|
|
359
|
+
*/
|
|
360
|
+
/**
|
|
361
|
+
* Classes declared `simple`, held for synchronous lookup.
|
|
362
|
+
*
|
|
363
|
+
* {@link StackPlugin} needs the answer inside `bulkDocs`, before it decides whether to
|
|
364
|
+
* load the class at all - and loading the class to find out is the very cost the flag
|
|
365
|
+
* exists to avoid. Refreshed when the datamodel is in place and whenever a class model
|
|
366
|
+
* changes. See ADR-0028.
|
|
367
|
+
*/
|
|
368
|
+
private simpleClassNames;
|
|
369
|
+
/**
|
|
370
|
+
* Whether a class stores its documents as given.
|
|
371
|
+
*
|
|
372
|
+
* See {@link ClassModel.simple}. Answers `false` for a class it has not heard of,
|
|
373
|
+
* which is the safe direction: an unknown class gets the full authoring path.
|
|
374
|
+
*
|
|
375
|
+
* @param className - The `~class` of a document.
|
|
376
|
+
*/
|
|
377
|
+
isSimpleClass: (className: unknown) => boolean;
|
|
378
|
+
/**
|
|
379
|
+
* Re-reads which classes are `simple`.
|
|
380
|
+
*
|
|
381
|
+
* @returns The names, for callers that want them.
|
|
382
|
+
*/
|
|
383
|
+
refreshSimpleClasses: () => Promise<string[]>;
|
|
384
|
+
getEphemeralClassNames: () => Promise<string[]>;
|
|
385
|
+
/**
|
|
386
|
+
* Empties the ephemeral classes.
|
|
387
|
+
*
|
|
388
|
+
* Called when the stack opens, which is what "one run" means: contents cover the
|
|
389
|
+
* session that is starting, a logout leaves them alone, and a crash is cleaned up by
|
|
390
|
+
* the next open rather than leaving documents on disk forever.
|
|
391
|
+
*
|
|
392
|
+
* Failures are logged and swallowed. A stack that cannot clear its scratch data should
|
|
393
|
+
* still open.
|
|
394
|
+
*
|
|
395
|
+
* @returns How many documents were removed.
|
|
396
|
+
*/
|
|
397
|
+
private purgeEphemeralDocuments;
|
|
398
|
+
getClassNames: () => Promise<string[]>;
|
|
399
|
+
getContentClassNames: () => Promise<{
|
|
400
|
+
classes: string[];
|
|
401
|
+
domains: string[];
|
|
402
|
+
}>;
|
|
403
|
+
/**
|
|
404
|
+
* Exports this stack's application content, and nothing else.
|
|
405
|
+
*
|
|
406
|
+
* Deliberately narrower than {@link dump}, which returns the database verbatim -
|
|
407
|
+
* class models, patches, users, sessions, policies, design documents, and encrypted
|
|
408
|
+
* attributes as unreadable payloads. That is a backup of *this* database. This is the
|
|
409
|
+
* portable one: the documents an application put in, ready for
|
|
410
|
+
* {@link importContent} to place into a stack whose schema its own patches built and
|
|
411
|
+
* whose document key is its own.
|
|
412
|
+
*
|
|
413
|
+
* What it does **not** do:
|
|
414
|
+
*
|
|
415
|
+
* - **It does not bypass encryption.** Documents are read through the decrypting path,
|
|
416
|
+
* so encrypted attributes come out as plaintext. That is what makes the export
|
|
417
|
+
* portable across keys - and what makes the result as sensitive as the data itself.
|
|
418
|
+
* A locked stack cannot decrypt, so the export is refused rather than silently
|
|
419
|
+
* emitting `null` where values should be (see `allowLossyWhenLocked`).
|
|
420
|
+
* - **It does not bypass read policies.** Documents the current session may not read
|
|
421
|
+
* are absent, exactly as they are absent from `findDocuments`.
|
|
422
|
+
* - **It carries no schema, no patches and no system documents.**
|
|
423
|
+
*
|
|
424
|
+
* @param options - Which classes and domains to cover; see {@link ContentExportOptions}.
|
|
425
|
+
* @returns The portable envelope.
|
|
426
|
+
* @throws Error when the stack is locked and an exported class has encrypted
|
|
427
|
+
* attributes, unless `allowLossyWhenLocked` is set.
|
|
428
|
+
*
|
|
429
|
+
* @example
|
|
430
|
+
* ```typescript
|
|
431
|
+
* const payload = await stack.exportContent({ classes: ["Task", "Project"] });
|
|
432
|
+
* download(new Blob([JSON.stringify(payload)], { type: "application/json" }));
|
|
433
|
+
* ```
|
|
434
|
+
*/
|
|
435
|
+
exportContent: (options?: ContentExportOptions) => Promise<ContentExport>;
|
|
436
|
+
/**
|
|
437
|
+
* Imports content produced by {@link exportContent} into this stack.
|
|
438
|
+
*
|
|
439
|
+
* The counterpart, and it is not symmetric: an export is a read, an import is a
|
|
440
|
+
* reconciliation. The payload carries data and no schema, so this stack's datamodel
|
|
441
|
+
* decides what is allowed in.
|
|
442
|
+
*
|
|
443
|
+
* - **Reconciled against the datamodel.** Every document's class must already exist
|
|
444
|
+
* here; a missing one is reported rather than invented, because the export carries
|
|
445
|
+
* no schema to create it from. Attributes the target class does not define are
|
|
446
|
+
* dropped by default.
|
|
447
|
+
* - **Written through the authoring path**, so schema validation, relation checks and
|
|
448
|
+
* triggers all run - and so encrypted attributes are **encrypted under this stack's
|
|
449
|
+
* document key**, not the one they were exported from.
|
|
450
|
+
* - **Documents before relations**, because a relation is rejected unless both ends
|
|
451
|
+
* already exist.
|
|
452
|
+
*
|
|
453
|
+
* Not a transaction: a failure part way through leaves what was already written. The
|
|
454
|
+
* report says what landed.
|
|
455
|
+
*
|
|
456
|
+
* @param payload - An envelope from {@link exportContent}.
|
|
457
|
+
* @param options - How to reconcile; see {@link ContentImportOptions}.
|
|
458
|
+
* @returns What was written, skipped, and why.
|
|
459
|
+
* @throws Error when the payload is not a recognised export, or when a `"fail"` option
|
|
460
|
+
* is set and the condition it names occurs.
|
|
461
|
+
*
|
|
462
|
+
* @example
|
|
463
|
+
* ```typescript
|
|
464
|
+
* const report = await stack.importContent(JSON.parse(await file.text()));
|
|
465
|
+
* report.documents.written; // 128
|
|
466
|
+
* report.issues; // [{ docId: "Task-9", kind: "missing-class", ... }]
|
|
467
|
+
* ```
|
|
468
|
+
*/
|
|
469
|
+
importContent: (payload: ContentExport, options?: ContentImportOptions) => Promise<ContentImportReport>;
|
|
134
470
|
private ensureDefaultPolicyForClass;
|
|
135
471
|
/**
|
|
136
472
|
* Creates and initializes a new ClientStack instance.
|
|
@@ -157,6 +493,34 @@ declare class ClientStack extends Stack {
|
|
|
157
493
|
* ```
|
|
158
494
|
*/
|
|
159
495
|
static create(conn: string, options?: StackOptions): Promise<ClientStack>;
|
|
496
|
+
/**
|
|
497
|
+
* Applies application-supplied patches, holding back any that need a document key.
|
|
498
|
+
*
|
|
499
|
+
* A locked stack must not write encrypted attributes in the clear, so a patch that
|
|
500
|
+
* would do so is kept for {@link unlock} instead. This is a barrier rather than a
|
|
501
|
+
* filter: patches apply in order and a later one may depend on the schema an earlier
|
|
502
|
+
* one installs, so the first deferral stops the run.
|
|
503
|
+
*
|
|
504
|
+
* The held-back patches live on the instance, not in the database - reopening the
|
|
505
|
+
* stack replays the same options through the same check, so there is no persisted
|
|
506
|
+
* state to drift.
|
|
507
|
+
*
|
|
508
|
+
* @param patches - Patches not yet present in this stack.
|
|
509
|
+
*/
|
|
510
|
+
private applyConsumerPatches;
|
|
511
|
+
/**
|
|
512
|
+
* Decides whether applying a patch would write an encrypted attribute.
|
|
513
|
+
*
|
|
514
|
+
* The judgement is made immediately before the patch would be applied, against the
|
|
515
|
+
* schema as it stands then, plus any class model the patch carries itself - a patch
|
|
516
|
+
* can introduce an encrypted attribute and write a document using it in one go, as
|
|
517
|
+
* `~sys-0.0.8` does. Everything earlier has already landed, so nothing needs to
|
|
518
|
+
* simulate schema evolution ahead of time.
|
|
519
|
+
*
|
|
520
|
+
* @param patch - The patch about to be applied.
|
|
521
|
+
* @returns `true` if any document in it belongs to a class with encrypted attributes.
|
|
522
|
+
*/
|
|
523
|
+
private patchNeedsDocumentKey;
|
|
160
524
|
/**
|
|
161
525
|
* Authenticates a user and establishes a session.
|
|
162
526
|
*
|
|
@@ -180,6 +544,43 @@ declare class ClientStack extends Stack {
|
|
|
180
544
|
* ```
|
|
181
545
|
*/
|
|
182
546
|
authenticate(credentials: ClientCredentials): Promise<AuthSessionProof>;
|
|
547
|
+
/**
|
|
548
|
+
* Finishes the work that could not be done while the stack had no document key.
|
|
549
|
+
*
|
|
550
|
+
* Reached from both ways a key arrives - {@link unlock} and {@link authenticate} -
|
|
551
|
+
* because the consequences are the same either way: the canary has to exist for later
|
|
552
|
+
* opens to be verifiable, bootstrap documents seeded in the clear have to be
|
|
553
|
+
* encrypted, and patches held back have to be applied.
|
|
554
|
+
*/
|
|
555
|
+
private onDocumentKeyAvailable;
|
|
556
|
+
/**
|
|
557
|
+
* Mints an identifier for a new document.
|
|
558
|
+
*
|
|
559
|
+
* Random, not sequential, and that is the whole point. Ids used to be
|
|
560
|
+
* `${type}-${lastDocId + 1}`, from a counter that only *local* writes advance: a
|
|
561
|
+
* document arriving by replication goes through {@link getReplicationHandle}, which
|
|
562
|
+
* bypasses that path by design, so the counter stood still while ids were consumed.
|
|
563
|
+
* The next local write then minted an id the database already held, PouchDB resolved
|
|
564
|
+
* the two as revisions of one document, and the new one was gone - with no error,
|
|
565
|
+
* because the conflict was swallowed. Two devices did it to each other from their
|
|
566
|
+
* very first document, both starting at `1`.
|
|
567
|
+
*
|
|
568
|
+
* No counter repair fixes that. Feeding replicated documents back into the counter
|
|
569
|
+
* still leaves two offline devices minting the same id, because a sequence derived
|
|
570
|
+
* from local state cannot be unique across devices that have not met. The identifier
|
|
571
|
+
* has to stop being derived from local state at all. See ADR-0023.
|
|
572
|
+
*
|
|
573
|
+
* The class prefix stays, so an id still says what it is.
|
|
574
|
+
*
|
|
575
|
+
* @param type - The class or domain name, used as the prefix.
|
|
576
|
+
* @returns An id of the form `Task-9f2c...`, 96 random bits wide.
|
|
577
|
+
*
|
|
578
|
+
* @example
|
|
579
|
+
* ```typescript
|
|
580
|
+
* stack.generateDocId("Task"); // "Task-3f9a2b7c1d4e5f60a1b2c3d4"
|
|
581
|
+
* ```
|
|
582
|
+
*/
|
|
583
|
+
generateDocId(type: string): string;
|
|
183
584
|
getLastDocId(): Promise<number>;
|
|
184
585
|
getSystem(): Promise<SystemDoc>;
|
|
185
586
|
private loadPatches;
|
|
@@ -210,10 +611,176 @@ declare class ClientStack extends Stack {
|
|
|
210
611
|
onClassLock: (className: string) => PouchDB.Core.Changes<{}>;
|
|
211
612
|
addClassLock: (className: string) => Promise<boolean>;
|
|
212
613
|
clearClassLock: (className: string) => Promise<boolean>;
|
|
213
|
-
|
|
614
|
+
/**
|
|
615
|
+
* Opens the shared class-document feed if it is not already running.
|
|
616
|
+
*
|
|
617
|
+
* Dispatch is keyed on the document's `~class`, which is what the per-class filters
|
|
618
|
+
* used to test. A change with no document therefore cannot be routed and is dropped,
|
|
619
|
+
* exactly as the filters dropped it: PouchDB hands a filter only `{_id, _rev,
|
|
620
|
+
* _deleted}` for a hard deletion, so `~class` was already absent. DocStack deletes
|
|
621
|
+
* are soft - the document arrives with `active: false` - so this is not the delete
|
|
622
|
+
* path.
|
|
623
|
+
*/
|
|
624
|
+
/** The subscriber-map key for a name in one of the two namespaces. */
|
|
625
|
+
private static subscriberKey;
|
|
626
|
+
/**
|
|
627
|
+
* Resolves which subscribers a change belongs to.
|
|
628
|
+
*
|
|
629
|
+
* A document names its owner in exactly one of two fields - `~class` for a class's
|
|
630
|
+
* documents, `~domain` for a domain's relation documents - so the routing key comes
|
|
631
|
+
* from whichever is present. A change with neither cannot be routed and is dropped,
|
|
632
|
+
* exactly as the per-class filters dropped it: PouchDB hands a filter only
|
|
633
|
+
* `{_id, _rev, _deleted}` for a hard deletion, so no meta field was there either.
|
|
634
|
+
* DocStack deletes are soft - the document arrives with `active: false` - so this is
|
|
635
|
+
* not the delete path.
|
|
636
|
+
*/
|
|
637
|
+
private static routingKeyFor;
|
|
638
|
+
private ensureClassDocFeed;
|
|
639
|
+
private addClassDocSubscriber;
|
|
640
|
+
private removeClassDocSubscriber;
|
|
641
|
+
/**
|
|
642
|
+
* Subscribes to changes on the documents of a class, or of a domain.
|
|
643
|
+
*
|
|
644
|
+
* Returns a handle onto {@link classDocFeed} rather than a feed of its own, so the
|
|
645
|
+
* database carries one `destroyed` listener no matter how many are watched.
|
|
646
|
+
* Cancelling releases only this subscriber; the feed stops once the last one goes.
|
|
647
|
+
*
|
|
648
|
+
* Prefer {@link subscribeClassDocs} / {@link subscribeDomainDocs}, which route changes
|
|
649
|
+
* through the decrypting preparation step (ADR-0020). Whichever is used, the handle
|
|
650
|
+
* must be handed to {@link releaseListener} when the watcher is done.
|
|
651
|
+
*
|
|
652
|
+
* @param className - The class or domain whose documents to watch.
|
|
653
|
+
* @param metaKey - Which field names the owner: `~class` (default) for a class's
|
|
654
|
+
* documents, `~domain` for a domain's relation documents. Separate namespaces.
|
|
655
|
+
* @returns A cancellable subscription handle.
|
|
656
|
+
*/
|
|
657
|
+
onClassDoc: (className: string, metaKey?: "~class" | "~domain") => ChangesSubscription;
|
|
658
|
+
/**
|
|
659
|
+
* Prepares a document delivered by the changes feed for a listener.
|
|
660
|
+
*
|
|
661
|
+
* The changes feed is the one read path that does not pass through
|
|
662
|
+
* {@link StackPlugin}: decryption lives in the `bulkGet` wrapper, which is what makes
|
|
663
|
+
* `getCards` and `findDocuments` transparent, while `include_docs` hands back exactly
|
|
664
|
+
* what is stored. Every read decrypted except the one that pushed, so a live view
|
|
665
|
+
* received an `EncryptedPayload` object where it had just rendered a string.
|
|
666
|
+
*
|
|
667
|
+
* @param doc - The document from `change.doc`.
|
|
668
|
+
* @param classObj - The class, when known; without it encrypted values are still
|
|
669
|
+
* recognised by shape.
|
|
670
|
+
* @returns A copy safe to hand to a consumer. Never contains an `EncryptedPayload`.
|
|
671
|
+
*
|
|
672
|
+
* @example
|
|
673
|
+
* ```typescript
|
|
674
|
+
* const doc = await stack.prepareChangeDocument(change.doc, classObj);
|
|
675
|
+
* doc.ssn; // plaintext, or null when it cannot be opened
|
|
676
|
+
* ```
|
|
677
|
+
*/
|
|
678
|
+
prepareChangeDocument: (doc: Document, classObj?: Class) => Promise<Document>;
|
|
679
|
+
/**
|
|
680
|
+
* Evicts derived caches after documents were written.
|
|
681
|
+
*
|
|
682
|
+
* Called synchronously by StackPlugin after every successful `bulkDocs` batch (which
|
|
683
|
+
* every local write funnels through, `put`/`post`/`remove` and replication included),
|
|
684
|
+
* and again by the shared changes feed for writes made outside this instance -
|
|
685
|
+
* another tab on the same database, most commonly. The write-path call is what makes
|
|
686
|
+
* a policy or schema write visible to the very next read: the changes feed delivers
|
|
687
|
+
* asynchronously, and a cache invalidated only by the feed would serve stale answers
|
|
688
|
+
* in that window.
|
|
689
|
+
*
|
|
690
|
+
* Class-model writes clear the model and snapshot caches wholesale rather than by
|
|
691
|
+
* key - a rename leaves the old name keyed to a model that no longer answers to it,
|
|
692
|
+
* and class writes are rare enough that precision buys nothing.
|
|
693
|
+
*
|
|
694
|
+
* @param docs - The documents just written; omit to invalidate everything.
|
|
695
|
+
*/
|
|
696
|
+
invalidateWriteCaches: (docs?: unknown[]) => void;
|
|
697
|
+
/**
|
|
698
|
+
* Whether a database-level `limit` returns the same rows as limiting in memory.
|
|
699
|
+
*
|
|
700
|
+
* `findDocuments` filters per document *after* the query - policy checks drop
|
|
701
|
+
* unreadable documents, and a locked crypto engine drops documents whose visible
|
|
702
|
+
* fields are all encrypted. A limit applied before either would under-fill. The
|
|
703
|
+
* query engine asks this before pushing a SQL LIMIT into the fetch.
|
|
704
|
+
*
|
|
705
|
+
* @param className - The class being queried.
|
|
706
|
+
*/
|
|
707
|
+
canApplyQueryLimitEarly: (className: string) => Promise<boolean>;
|
|
708
|
+
/**
|
|
709
|
+
* Creates the stack's standing Mango indexes.
|
|
710
|
+
*
|
|
711
|
+
* Every `findDocuments` selector carries `~class` and `active`, and without an index
|
|
712
|
+
* pouchdb-find answers each one with a full `allDocs` scan - linear in database
|
|
713
|
+
* size, per call. One fixed index serves them all. This replaces the old commented
|
|
714
|
+
* per-query `createIndex` inside `findDocuments`, which built a fresh index for
|
|
715
|
+
* every distinct selector shape and buried the database in design documents - the
|
|
716
|
+
* "breaks find and even db" the comment there warned about.
|
|
717
|
+
*
|
|
718
|
+
* Failure is deliberately non-fatal: an index is an optimization, and pouchdb-find
|
|
719
|
+
* falls back to scanning exactly as before.
|
|
720
|
+
*/
|
|
721
|
+
private ensureMangoIndexes;
|
|
722
|
+
/** Registry of on-demand sort indexes; a `_local` doc, so per-device and unreplicated. */
|
|
723
|
+
private static readonly SORT_INDEX_REGISTRY_ID;
|
|
724
|
+
/** Prefix shared by every sort-index design document this stack creates. */
|
|
725
|
+
private static readonly SORT_INDEX_DDOC_PREFIX;
|
|
726
|
+
/** Most sort indexes a stack will maintain; past this, queries sort in memory. */
|
|
727
|
+
private static readonly MAX_SORT_INDEXES;
|
|
728
|
+
/** How stale a sort index may go before {@link cleanupSortIndexes} removes it. */
|
|
729
|
+
private static readonly SORT_INDEX_MAX_IDLE_MS;
|
|
730
|
+
/** Fields whose sort index exists this session; avoids re-running createIndex. */
|
|
731
|
+
private sortIndexSession;
|
|
732
|
+
private readSortIndexRegistry;
|
|
733
|
+
/**
|
|
734
|
+
* Creates (or confirms) a Mango index for sorting by `field`, and records the use.
|
|
735
|
+
*
|
|
736
|
+
* Indexes are made on demand by the query engine when an ORDER BY can ride the
|
|
737
|
+
* database, and every index is a standing cost: a view updated on every write from
|
|
738
|
+
* then on. Three things keep that bounded: a cap ({@link MAX_SORT_INDEXES}) past
|
|
739
|
+
* which this returns `false` and the caller sorts in memory; a usage registry (a
|
|
740
|
+
* `_local` document, per device) stamped on each use; and
|
|
741
|
+
* {@link cleanupSortIndexes}, run at init, dropping indexes idle past
|
|
742
|
+
* {@link SORT_INDEX_MAX_IDLE_MS}. A dropped index is not an error - the next sorted
|
|
743
|
+
* query recreates it.
|
|
744
|
+
*
|
|
745
|
+
* @param field - The document field to index for sorting (under `~class`).
|
|
746
|
+
* @returns `true` when the index exists and may be used for a sorted query.
|
|
747
|
+
*/
|
|
748
|
+
ensureSortIndex: (field: string) => Promise<boolean>;
|
|
749
|
+
/**
|
|
750
|
+
* Drops sort indexes that have gone unused.
|
|
751
|
+
*
|
|
752
|
+
* Covers indexes this device created *and* ones that replicated in as design
|
|
753
|
+
* documents from a peer: every `_design/docstack-sort-*` doc is considered, and one
|
|
754
|
+
* with no registry entry is adopted with the current time as first-seen, so it gets
|
|
755
|
+
* a full idle period before removal. Runs automatically at init; callable directly
|
|
756
|
+
* for an immediate sweep.
|
|
757
|
+
*
|
|
758
|
+
* @param options.olderThanMs - Idle threshold; defaults to {@link SORT_INDEX_MAX_IDLE_MS}.
|
|
759
|
+
* @returns Which fields were removed and which kept.
|
|
760
|
+
*/
|
|
761
|
+
cleanupSortIndexes: (options?: {
|
|
762
|
+
olderThanMs?: number;
|
|
763
|
+
}) => Promise<{
|
|
764
|
+
removed: string[];
|
|
765
|
+
kept: string[];
|
|
766
|
+
}>;
|
|
214
767
|
initdb(): Promise<this>;
|
|
215
768
|
private ensureCryptoConfigDocument;
|
|
216
769
|
private ensureCryptoMarkerEncryption;
|
|
770
|
+
/**
|
|
771
|
+
* Checks a stack's stored crypto configuration against how it is being opened.
|
|
772
|
+
*
|
|
773
|
+
* The canary is the admission test for keys: `encryptedMarker` holds a value only the
|
|
774
|
+
* stack's own document key can decrypt, so a wrong key is caught here rather than
|
|
775
|
+
* becoming unreadable data later. Note what is *not* checked - whether the stack
|
|
776
|
+
* already holds encrypted documents. A key is admitted on proof, so a second device
|
|
777
|
+
* can open a stack the first one wrote, which is the whole point of the wrapped-key
|
|
778
|
+
* path. See ADR-0018.
|
|
779
|
+
*
|
|
780
|
+
* @param existing - The stored `~crypto-engine-config` document.
|
|
781
|
+
* @throws If the engine flag disagrees with the stored one, or a key is present but
|
|
782
|
+
* fails to decrypt the canary.
|
|
783
|
+
*/
|
|
217
784
|
private validateCryptoConfig;
|
|
218
785
|
/**
|
|
219
786
|
* Closes the stack and cleans up all resources.
|
|
@@ -245,6 +812,33 @@ declare class ClientStack extends Stack {
|
|
|
245
812
|
* @param fresh - If `true`, bypasses the cache and fetches from database
|
|
246
813
|
* @returns The Domain instance, or `null` if not found
|
|
247
814
|
*/
|
|
815
|
+
/**
|
|
816
|
+
* Reads a class's current stored model without subscribing or caching it.
|
|
817
|
+
*
|
|
818
|
+
* The counterpart to {@link getClass} for code that wants a schema rather than a live
|
|
819
|
+
* view: validation, encryption, and anything else that runs per write or per row.
|
|
820
|
+
* Two properties matter and pull in opposite directions in {@link getClass}:
|
|
821
|
+
*
|
|
822
|
+
* - It is always current. The cache is invalidated by a changes feed, which is
|
|
823
|
+
* asynchronous, so during a burst of schema writes - patch application, most
|
|
824
|
+
* obviously - the cached instance can still be the previous schema. Validating a
|
|
825
|
+
* document against that fails.
|
|
826
|
+
* - It does not subscribe. A Class built by {@link Class.get} watches its documents
|
|
827
|
+
* until closed, so building one per written document leaves live feeds behind and
|
|
828
|
+
* PouchDB eventually warns about the `destroyed` listeners they hold.
|
|
829
|
+
*
|
|
830
|
+
* The returned instance emits no `doc` events and needs no `close()`.
|
|
831
|
+
*
|
|
832
|
+
* @param className - The name or ID of the class.
|
|
833
|
+
* @returns The class, or `null` if there is no model by that name.
|
|
834
|
+
*
|
|
835
|
+
* @example
|
|
836
|
+
* ```typescript
|
|
837
|
+
* const classObj = await stack.getClassSnapshot(doc["~class"]);
|
|
838
|
+
* classObj?.getEncryptedAttributes();
|
|
839
|
+
* ```
|
|
840
|
+
*/
|
|
841
|
+
getClassSnapshot: (className: string) => Promise<Class | null>;
|
|
248
842
|
getDomain: (domainName: string, fresh?: boolean) => Promise<Domain | null>;
|
|
249
843
|
initIndex(): Promise<void>;
|
|
250
844
|
/**
|
|
@@ -286,10 +880,46 @@ declare class ClientStack extends Stack {
|
|
|
286
880
|
*/
|
|
287
881
|
findDocuments: <T extends Document | RelationDocument = Document>(selector: {
|
|
288
882
|
[key: string]: any;
|
|
289
|
-
}, fields?: string[], skip?: number, limit?: number
|
|
883
|
+
}, fields?: string[], skip?: number, limit?: number, sort?: {
|
|
884
|
+
[field: string]: "asc" | "desc";
|
|
885
|
+
}[]) => Promise<{
|
|
290
886
|
[key: string]: any;
|
|
291
887
|
docs: T[];
|
|
292
888
|
}>;
|
|
889
|
+
/**
|
|
890
|
+
* Runs raw fetched documents through the read pipeline: per-document policy
|
|
891
|
+
* check, decryption, and field visibility. Shared by {@link findDocuments} and
|
|
892
|
+
* {@link findDocumentsIterator} so the two cannot drift.
|
|
893
|
+
*/
|
|
894
|
+
private processFoundDocuments;
|
|
895
|
+
/**
|
|
896
|
+
* Reads documents matching a selector as an async stream, in `_id` order.
|
|
897
|
+
*
|
|
898
|
+
* Pages through the database with a keyset cursor on `_id` (which the primary
|
|
899
|
+
* index serves) instead of materializing the full result: peak memory is one
|
|
900
|
+
* batch, and total work across all pages is the same one scan a single big read
|
|
901
|
+
* would do. Each batch goes through the same policy/decryption pipeline as
|
|
902
|
+
* {@link findDocuments}. The cursor advances by the last *fetched* document, not
|
|
903
|
+
* the last *readable* one, so pages thinned out by policy filtering cannot stall
|
|
904
|
+
* the iteration.
|
|
905
|
+
*
|
|
906
|
+
* @param selector - A Mango selector; `active: true` is injected unless present.
|
|
907
|
+
* @param options.fields - Projection; `_id` is always included (the cursor needs it).
|
|
908
|
+
* @param options.batchSize - Documents fetched per page (default 100).
|
|
909
|
+
*
|
|
910
|
+
* @example
|
|
911
|
+
* ```typescript
|
|
912
|
+
* for await (const doc of stack.findDocumentsIterator({ "~class": "Task" })) {
|
|
913
|
+
* render(doc);
|
|
914
|
+
* }
|
|
915
|
+
* ```
|
|
916
|
+
*/
|
|
917
|
+
findDocumentsIterator: <T extends Document | RelationDocument = Document>(selector: {
|
|
918
|
+
[key: string]: any;
|
|
919
|
+
}, options?: {
|
|
920
|
+
fields?: string[];
|
|
921
|
+
batchSize?: number;
|
|
922
|
+
}) => AsyncGenerator<T, void, void>;
|
|
293
923
|
private processReadableDocument;
|
|
294
924
|
/**
|
|
295
925
|
* Finds a single document matching a selector.
|
|
@@ -336,6 +966,13 @@ declare class ClientStack extends Stack {
|
|
|
336
966
|
search?: string;
|
|
337
967
|
}) => Promise<Domain[]>;
|
|
338
968
|
incrementLastDocId(): Promise<number>;
|
|
969
|
+
/**
|
|
970
|
+
* Advances the document-id counter by `count` in one database write.
|
|
971
|
+
*
|
|
972
|
+
* Batch creation hands out `count` ids from the in-memory counter and commits them
|
|
973
|
+
* here once, instead of a get+put round-trip per document.
|
|
974
|
+
*/
|
|
975
|
+
advanceLastDocId(count: number): Promise<number>;
|
|
339
976
|
reset(): Promise<this>;
|
|
340
977
|
destroyDb(): Promise<boolean>;
|
|
341
978
|
static clear(conn: string): Promise<unknown>;
|
|
@@ -489,9 +1126,41 @@ declare class ClientStack extends Stack {
|
|
|
489
1126
|
* const { rows } = await stack.query('SELECT * FROM Task WHERE priority = ?', 'high');
|
|
490
1127
|
* ```
|
|
491
1128
|
*/
|
|
1129
|
+
/**
|
|
1130
|
+
* Replaces `?` placeholder nodes in a parsed AST with the caller's parameter values.
|
|
1131
|
+
*
|
|
1132
|
+
* Values are restricted to plain scalars: an object here could carry Mango
|
|
1133
|
+
* operators of its own and reach the database as part of a pushed-down selector,
|
|
1134
|
+
* changing what the query matches. Structured values belong in the document
|
|
1135
|
+
* model, not in a comparison.
|
|
1136
|
+
*/
|
|
1137
|
+
private static bindQueryParams;
|
|
492
1138
|
query: (sql: string, ...params: any[]) => Promise<{
|
|
493
1139
|
rows: any;
|
|
494
1140
|
ast: (SelectAST | UnionAST)[];
|
|
495
1141
|
}>;
|
|
1142
|
+
/**
|
|
1143
|
+
* Executes a SQL query as an async stream of rows.
|
|
1144
|
+
*
|
|
1145
|
+
* The streaming counterpart to {@link query}: single-table plans without
|
|
1146
|
+
* aggregation, DISTINCT, ORDER BY, or subqueries stream row by row on top of
|
|
1147
|
+
* {@link findDocumentsIterator} - peak memory is one page regardless of result
|
|
1148
|
+
* size, and a LIMIT stops the underlying scan early. More complex plans execute
|
|
1149
|
+
* normally and yield from the materialized result, so the API is uniform.
|
|
1150
|
+
* Row order on the streaming path is `_id` order.
|
|
1151
|
+
*
|
|
1152
|
+
* @param sql - The SQL SELECT statement.
|
|
1153
|
+
* @param params - Values for `?` placeholders.
|
|
1154
|
+
*
|
|
1155
|
+
* @example
|
|
1156
|
+
* ```typescript
|
|
1157
|
+
* for await (const row of stack.queryStream("SELECT t.title FROM Task AS t WHERE t.done = FALSE;")) {
|
|
1158
|
+
* render(row);
|
|
1159
|
+
* }
|
|
1160
|
+
* ```
|
|
1161
|
+
*/
|
|
1162
|
+
queryStream: (sql: string, ...params: any[]) => AsyncGenerator<{
|
|
1163
|
+
[column: string]: any;
|
|
1164
|
+
}, void, void>;
|
|
496
1165
|
}
|
|
497
1166
|
export default ClientStack;
|