@docstack/client 0.1.5 → 0.1.8

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.
Files changed (80) hide show
  1. package/LICENSE +10 -0
  2. package/README.md +1 -1
  3. package/lib/core/attribute.d.ts +11 -2
  4. package/lib/core/class.d.ts +29 -6
  5. package/lib/core/content-transfer.d.ts +178 -0
  6. package/lib/core/crypto-engine/index.d.ts +82 -1
  7. package/lib/core/crypto-engine/utils.d.ts +36 -1
  8. package/lib/core/datamodel/index.d.ts +16 -0
  9. package/lib/core/domain.d.ts +1 -1
  10. package/lib/core/guarded-db.d.ts +53 -0
  11. package/lib/core/index.d.ts +138 -2
  12. package/lib/core/job-engine/schedule.d.ts +90 -0
  13. package/lib/core/job-engine/scheduler.d.ts +206 -0
  14. package/lib/core/policy-engine/index.d.ts +35 -0
  15. package/lib/core/query-engine/classes.d.ts +24 -0
  16. package/lib/core/query-engine/executor.d.ts +11 -0
  17. package/lib/core/query-engine/index.d.ts +2 -1
  18. package/lib/core/query-engine/planner.d.ts +19 -0
  19. package/lib/core/stack.d.ts +690 -7
  20. package/lib/core/sync/class-filter.d.ts +106 -0
  21. package/lib/core/sync/filter-identity.d.ts +53 -0
  22. package/lib/core/sync/index.d.ts +352 -0
  23. package/lib/core/sync/internal-docs.d.ts +159 -0
  24. package/lib/core/sync/tenants.d.ts +67 -0
  25. package/lib/index.d.ts +38 -1
  26. package/lib/index.js +5865 -664
  27. package/lib/index.umd.js +5903 -681
  28. package/lib/plugins/pouchdb.d.ts +43 -3
  29. package/lib/utils/logger/index.d.ts +28 -4
  30. package/lib/utils/logger/transport.d.ts +52 -11
  31. package/package.json +18 -10
  32. package/lib/core/attribute.js +0 -406
  33. package/lib/core/attribute.js.map +0 -1
  34. package/lib/core/class.js +0 -774
  35. package/lib/core/class.js.map +0 -1
  36. package/lib/core/crypto-engine/index.js +0 -229
  37. package/lib/core/crypto-engine/index.js.map +0 -1
  38. package/lib/core/crypto-engine/utils.js +0 -88
  39. package/lib/core/crypto-engine/utils.js.map +0 -1
  40. package/lib/core/datamodel/index.js +0 -1308
  41. package/lib/core/datamodel/index.js.map +0 -1
  42. package/lib/core/domain.js +0 -423
  43. package/lib/core/domain.js.map +0 -1
  44. package/lib/core/index.js +0 -532
  45. package/lib/core/index.js.map +0 -1
  46. package/lib/core/job-engine/index.js +0 -220
  47. package/lib/core/job-engine/index.js.map +0 -1
  48. package/lib/core/policy-engine/index.js +0 -232
  49. package/lib/core/policy-engine/index.js.map +0 -1
  50. package/lib/core/query-engine/accumulators.js +0 -258
  51. package/lib/core/query-engine/accumulators.js.map +0 -1
  52. package/lib/core/query-engine/evaluator.js +0 -179
  53. package/lib/core/query-engine/evaluator.js.map +0 -1
  54. package/lib/core/query-engine/executor.js +0 -405
  55. package/lib/core/query-engine/executor.js.map +0 -1
  56. package/lib/core/query-engine/index.js +0 -4
  57. package/lib/core/query-engine/index.js.map +0 -1
  58. package/lib/core/query-engine/parser.js +0 -515
  59. package/lib/core/query-engine/parser.js.map +0 -1
  60. package/lib/core/query-engine/planner.js +0 -330
  61. package/lib/core/query-engine/planner.js.map +0 -1
  62. package/lib/core/stack.js +0 -1827
  63. package/lib/core/stack.js.map +0 -1
  64. package/lib/core/test-utils/docstack.js +0 -222
  65. package/lib/core/test-utils/docstack.js.map +0 -1
  66. package/lib/core/trigger/index.js +0 -81
  67. package/lib/core/trigger/index.js.map +0 -1
  68. package/lib/index.js.map +0 -1
  69. package/lib/plugins/pouchdb.js +0 -412
  70. package/lib/plugins/pouchdb.js.map +0 -1
  71. package/lib/utils/crypto/index.js +0 -34
  72. package/lib/utils/crypto/index.js.map +0 -1
  73. package/lib/utils/index.js +0 -58
  74. package/lib/utils/index.js.map +0 -1
  75. package/lib/utils/logger/index.js +0 -20
  76. package/lib/utils/logger/index.js.map +0 -1
  77. package/lib/utils/logger/transport.js +0 -28
  78. package/lib/utils/logger/transport.js.map +0 -1
  79. package/lib/workers/dataModel.js +0 -48
  80. package/lib/workers/dataModel.js.map +0 -1
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Class-level replication filtering.
3
+ *
4
+ * The obvious way for an application to filter a DocStack stack is by class - "sync
5
+ * Tasks and Projects, leave Drafts on this device". Doing that correctly needs two
6
+ * pieces of knowledge an application author has no reason to have, which is why it
7
+ * lives here rather than in a hand-written predicate:
8
+ *
9
+ * 1. **An allow-list has to keep the data model.** Class models, domains, policies,
10
+ * users and groups are not "documents of a class the user picked" - they are what
11
+ * makes the replica readable at all. An `include: ["Task"]` that took the phrase
12
+ * literally would produce a remote holding Task documents and no Task class, which
13
+ * the next device could not open.
14
+ * 2. **Relations are not classified by `~class`.** A relation document carries
15
+ * `~domain` plus `sourceClass`/`targetClass`, so a filter that only looks at
16
+ * `~class` lets every relation through - including relations pointing at documents
17
+ * that were filtered out, which arrive on the peer as dangling references.
18
+ *
19
+ * @module
20
+ */
21
+ /**
22
+ * Classes that describe the stack itself rather than its subject matter.
23
+ *
24
+ * Kept by an allow-list unless {@link ClassFilterOptions.includeDataModel} turns that
25
+ * off. `~self` is the bootstrap class model; `patch` is here for completeness even
26
+ * though the internal-document filter already keeps it local.
27
+ */
28
+ export declare const DATA_MODEL_CLASSES: readonly string[];
29
+ /**
30
+ * Which classes replicate.
31
+ *
32
+ * `include` and `exclude` may be combined; `exclude` wins where they overlap. Entries
33
+ * are matched against a document's `~class` and against a relation's
34
+ * `sourceClass`/`targetClass`, which hold class ids - the same string for any class
35
+ * created through `Class.create`, where the id is the name.
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * // Everything except one class.
40
+ * { exclude: ["Draft"] }
41
+ *
42
+ * // Only these classes, plus the data model that makes them readable.
43
+ * { include: ["Task", "Project"] }
44
+ * ```
45
+ */
46
+ export interface ClassFilterOptions {
47
+ /** Replicate only these classes. Omit to replicate all but `exclude`. */
48
+ include?: string[];
49
+ /**
50
+ * Never replicate these classes. Applied after `include`.
51
+ *
52
+ * This drops documents *of* those classes. The class models themselves still
53
+ * replicate, so the remote stays a readable replica - a class model is
54
+ * `~class: "class"`, not `~class: "<the class>"`. To keep a class model on the
55
+ * device too, add its id to `internalDocs.extraDocIds`; a class created through
56
+ * `Class.create` has its name as its id.
57
+ */
58
+ exclude?: string[];
59
+ /**
60
+ * Keep {@link DATA_MODEL_CLASSES} when `include` is set. Defaults to `true`; turn
61
+ * it off only when the remote is not meant to be a readable replica.
62
+ */
63
+ includeDataModel?: boolean;
64
+ }
65
+ type MaybeClassedDoc = {
66
+ _id?: string;
67
+ "~class"?: unknown;
68
+ "~domain"?: unknown;
69
+ sourceClass?: unknown;
70
+ targetClass?: unknown;
71
+ [key: string]: unknown;
72
+ };
73
+ /**
74
+ * Reports whether a set of options would filter anything at all.
75
+ *
76
+ * @param options - The class filter options.
77
+ * @returns `true` when at least one rule is present.
78
+ */
79
+ export declare const hasClassRules: (options?: ClassFilterOptions) => boolean;
80
+ /**
81
+ * Builds a class-level replication filter.
82
+ *
83
+ * PouchDB filter functions use include-semantics: `true` replicates the document. The
84
+ * returned function is pure and carries a configuration-derived identity so that
85
+ * changing which classes replicate also changes the replication checkpoint - see
86
+ * {@link withFilterIdentity}.
87
+ *
88
+ * Documents that carry no `~class` and no `~domain` pass through untouched. That
89
+ * matters for deletions: a tombstone is `{ _id, _rev, _deleted }` with no class on it,
90
+ * and dropping those would mean a deletion never reaches the peer.
91
+ *
92
+ * @param options - Which classes to replicate.
93
+ * @returns A predicate suitable for `replicate`'s `filter` option.
94
+ *
95
+ * @example
96
+ * ```typescript
97
+ * const filter = createClassFilter({ include: ["Task"] });
98
+ * filter({ _id: "Task-1", "~class": "Task" }); // true
99
+ * filter({ _id: "Draft-1", "~class": "Draft" }); // false
100
+ * filter({ _id: "Task", "~class": "class" }); // true - the data model
101
+ * filter({ _id: "r1", "~domain": "TaskDraft",
102
+ * sourceClass: "Task", targetClass: "Draft" }); // false - dangling end
103
+ * ```
104
+ */
105
+ export declare const createClassFilter: (options?: ClassFilterOptions) => ((doc: MaybeClassedDoc) => boolean);
106
+ export {};
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Filter identity, and why a replication filter needs one.
3
+ *
4
+ * PouchDB derives a replication's checkpoint id from the source id, the target id and
5
+ * `opts.filter.toString()` (`generateReplicationId` in `pouchdb-replication`). The
6
+ * checkpoint is what "resume where we left off" means, so two replications that share
7
+ * an id share a resume point.
8
+ *
9
+ * That is a problem for filters built by a factory. Every filter DocStack produces is
10
+ * the *same closure source text* whatever options went into it, so without help they
11
+ * would all hash to one checkpoint: switching a stack from `exclude: ["Draft"]` to
12
+ * `exclude: ["Archive"]` would resume from the old checkpoint and never re-scan history
13
+ * for the documents the new filter admits. They would simply be missing on the remote,
14
+ * with no error anywhere.
15
+ *
16
+ * Stamping the function with a `toString` derived from its configuration makes the
17
+ * checkpoint follow the configuration: change what you filter, and replication starts
18
+ * again from the beginning and backfills. Keep the configuration, and it resumes.
19
+ *
20
+ * @module
21
+ */
22
+ /**
23
+ * Replaces a filter's `toString` with a stable, configuration-derived identity.
24
+ *
25
+ * @param filter - The filter function to stamp.
26
+ * @param identity - A string that changes if and only if the filter's behaviour does.
27
+ * @returns The same function, stamped.
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * const filter = withFilterIdentity(
32
+ * (doc) => doc["~class"] !== "Draft",
33
+ * describeFilter("app", { exclude: ["Draft"] })
34
+ * );
35
+ * String(filter); // "docstack-filter/1:app:{\"exclude\":[\"Draft\"]}"
36
+ * ```
37
+ */
38
+ export declare const withFilterIdentity: <T extends (...args: any[]) => any>(filter: T, identity: string) => T;
39
+ /**
40
+ * Builds the identity string for a filter of a given kind and configuration.
41
+ *
42
+ * @param kind - What sort of filter this is, e.g. `"internal"` or `"class"`.
43
+ * @param config - The options the filter was built from.
44
+ * @returns A string suitable for {@link withFilterIdentity}.
45
+ */
46
+ export declare const describeFilter: (kind: string, config: unknown) => string;
47
+ /**
48
+ * Combines identities for a composed filter.
49
+ *
50
+ * @param parts - The identities of the filters being composed, in order.
51
+ * @returns One identity covering all of them.
52
+ */
53
+ export declare const composeFilterIdentity: (parts: string[]) => string;
@@ -0,0 +1,352 @@
1
+ import { type InternalDocFilterOptions } from "./internal-docs.js";
2
+ import { type ClassFilterOptions } from "./class-filter.js";
3
+ import type ClientStack from "../stack.js";
4
+ /**
5
+ * Which way documents move.
6
+ *
7
+ * `"both"` runs a single bidirectional `PouchDB.sync`; the one-way values run
8
+ * `PouchDB.replicate` in the named direction.
9
+ */
10
+ export type SyncDirection = "push" | "pull" | "both";
11
+ /**
12
+ * Where a stack's replication currently stands.
13
+ *
14
+ * - `"stopped"` - never started, or cancelled.
15
+ * - `"starting"` - resolving the remote and running the schema gate.
16
+ * - `"active"` - documents are moving.
17
+ * - `"idle"` - a replication cycle finished with nothing left to send; this is the
18
+ * state that carries a meaningful `lastConvergedAt`.
19
+ * - `"error"` - the last cycle failed. With `retry: true` replication keeps trying and
20
+ * will return to `"active"` on its own.
21
+ * - `"denied"` - the remote refused a write (permissions), which retrying will not fix.
22
+ */
23
+ export type SyncState = "stopped" | "starting" | "active" | "idle" | "error" | "denied";
24
+ /**
25
+ * How a stack finds its remote.
26
+ *
27
+ * A function is the useful form for transports that need per-stack configuration or a
28
+ * credential that expires - DocStack calls it again on every {@link StackSyncHandle.restart},
29
+ * so a refreshed token reaches the new replication without the caller reaching into
30
+ * DocStack. DocStack never learns anything about the transport itself.
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * // Google Drive, one folder per stack, token owned by the application.
35
+ * const remote = (stack) => new PouchDB(stack.getDbName(), {
36
+ * adapter: "googledrive",
37
+ * accessToken: async () => auth.getAccessToken(),
38
+ * folderName: `tokido/${stack.name}`,
39
+ * });
40
+ * ```
41
+ */
42
+ export type RemoteResolver = string | PouchDB.Database | ((stack: ClientStack) => string | PouchDB.Database | Promise<string | PouchDB.Database>);
43
+ /**
44
+ * Options for {@link ClientStack.sync}.
45
+ *
46
+ * Everything except `remote` has a working default; the defaults describe a
47
+ * continuous, self-healing, internal-documents-excluded sync.
48
+ */
49
+ export interface StackSyncOptions {
50
+ /** The remote to replicate against. See {@link RemoteResolver}. */
51
+ remote: RemoteResolver;
52
+ /** Direction of travel. Defaults to `"both"`. */
53
+ direction?: SyncDirection;
54
+ /** Keep replicating as changes happen. Defaults to `true`. */
55
+ live?: boolean;
56
+ /** Retry on transient failure with PouchDB's backoff. Defaults to `true`. */
57
+ retry?: boolean;
58
+ /** Documents per batch. Passed to PouchDB as `batch_size`. */
59
+ batchSize?: number;
60
+ /** Concurrent batches. Passed to PouchDB as `batches_limit`. */
61
+ batchesLimit?: number;
62
+ /** Changes-feed heartbeat, in milliseconds, or `false` to disable. */
63
+ heartbeat?: number | false;
64
+ /** Changes-feed timeout, in milliseconds, or `false` to disable. */
65
+ timeout?: number | false;
66
+ /**
67
+ * Which classes replicate. See {@link ClassFilterOptions} - an allow-list keeps the
68
+ * data model automatically, and relations are judged by their endpoints.
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * stack.sync({ remote, classes: { exclude: ["Draft"] } });
73
+ * ```
74
+ */
75
+ classes?: ClassFilterOptions;
76
+ /**
77
+ * An extra predicate, ANDed with the filters above. Return `true` to replicate the
78
+ * document. Must be pure - PouchDB calls it once per change.
79
+ *
80
+ * Prefer `classes` where it fits: a bare function has no configuration DocStack can
81
+ * see, so its identity for checkpointing purposes is its own source text. Two
82
+ * closures over different data with the same source read as the same filter, and
83
+ * replication resumes where the other one left off. Build a fresh function whose
84
+ * source differs, or use `classes`.
85
+ */
86
+ filter?: (doc: any) => boolean;
87
+ /**
88
+ * Which of DocStack's own documents to keep on this device, or `false` to replicate
89
+ * everything including `~system`. `false` is for stack-to-stack mirroring of a whole
90
+ * database and is otherwise a bad idea. Defaults to {@link InternalDocFilterOptions}'
91
+ * own defaults.
92
+ */
93
+ internalDocs?: InternalDocFilterOptions | false;
94
+ /**
95
+ * Refuse to start when the remote was last written by a newer schema. Defaults to
96
+ * `true`. See {@link SyncSchemaMismatchError}.
97
+ */
98
+ checkSchemaVersion?: boolean;
99
+ }
100
+ /** A point-in-time reading of one stack's replication. */
101
+ export interface SyncStatus {
102
+ /** The stack this status belongs to. */
103
+ stack: string;
104
+ /** See {@link SyncState}. */
105
+ state: SyncState;
106
+ /** Direction this handle was started with. */
107
+ direction: SyncDirection;
108
+ /** Whether the handle is following changes or ran once. */
109
+ live: boolean;
110
+ /**
111
+ * When the replica last converged - a cycle completed with nothing left to send.
112
+ * `null` until the first one. This is the value a UI should render as
113
+ * "last synced", not `lastActiveAt`.
114
+ */
115
+ lastConvergedAt: number | null;
116
+ /** When documents last moved, in either direction. */
117
+ lastActiveAt: number | null;
118
+ /** The last failure seen, kept even after replication recovers. */
119
+ lastError: {
120
+ name?: string;
121
+ message: string;
122
+ status?: number;
123
+ } | null;
124
+ /** Documents written to the remote since this handle started. */
125
+ pushed: number;
126
+ /** Documents written locally since this handle started. */
127
+ pulled: number;
128
+ }
129
+ /**
130
+ * Raised when a remote is ahead of this device's data model.
131
+ *
132
+ * Pulling from it would deliver documents shaped by patches this device has not
133
+ * applied; they would be stored verbatim (replication writes bypass validation, by
134
+ * design) and then fail to read back. Refusing early leaves the local stack intact and
135
+ * tells the application to ship the newer build.
136
+ */
137
+ export declare class SyncSchemaMismatchError extends Error {
138
+ name: string;
139
+ /** The stack that refused to sync. */
140
+ readonly stack: string;
141
+ /** The schema version this device has applied, if any. */
142
+ readonly localVersion: string | undefined;
143
+ /** The schema version the remote was last written with. */
144
+ readonly remoteVersion: string;
145
+ constructor(stack: string, localVersion: string | undefined, remoteVersion: string);
146
+ }
147
+ /**
148
+ * The document DocStack keeps on a remote to record which schema wrote it.
149
+ *
150
+ * A `_local/` document on purpose: it is shared by every device talking to that remote
151
+ * (they all open the same database) but never replicates into anybody's stack, so it
152
+ * cannot be confused with the local `~system` record.
153
+ */
154
+ export declare const SYNC_META_DOC_ID = "_local/docstack-sync";
155
+ /** The contents of {@link SYNC_META_DOC_ID}. */
156
+ export interface SyncMetaDoc {
157
+ _id: string;
158
+ _rev?: string;
159
+ /** Highest schema version any device has pushed to this remote. */
160
+ schemaVersion?: string;
161
+ /** Application version of the device that last wrote it, for diagnostics. */
162
+ appVersion?: string;
163
+ /** When it was last written. */
164
+ updatedAt: number;
165
+ }
166
+ /**
167
+ * Reads the schema version a remote was last written with.
168
+ *
169
+ * Prefers DocStack's own marker and falls back to a `~system` document, which is only
170
+ * present when the remote is itself a DocStack stack replicated wholesale.
171
+ *
172
+ * @param remote - The remote database.
173
+ * @returns The recorded version, or `null` for a remote nobody has written yet.
174
+ */
175
+ export declare const readRemoteSchemaVersion: (remote: PouchDB.Database) => Promise<string | null>;
176
+ /**
177
+ * Records this device's schema version on a remote, if it is the newest seen.
178
+ *
179
+ * @param remote - The remote database.
180
+ * @param schemaVersion - The local schema version; a missing value writes nothing.
181
+ * @param appVersion - The local application version, stored for diagnostics.
182
+ */
183
+ export declare const publishSchemaVersion: (remote: PouchDB.Database, schemaVersion: string | undefined, appVersion?: string) => Promise<void>;
184
+ /**
185
+ * One stack's replication: its lifecycle, its filter, and its convergence state.
186
+ *
187
+ * Created by {@link ClientStack.sync} - construct it through the stack rather than
188
+ * directly, so the stack can cancel it when it closes.
189
+ *
190
+ * Dispatches DOM events, matching the rest of DocStack: `"status"` on every state
191
+ * change (`detail` is a {@link SyncStatus}), plus `"change"`, `"active"`, `"idle"`,
192
+ * `"denied"`, `"error"` and `"complete"`.
193
+ *
194
+ * @example
195
+ * ```typescript
196
+ * const sync = await stack.sync({ remote: () => driveDb });
197
+ *
198
+ * sync.addEventListener("status", (event) => {
199
+ * const status = (event as CustomEvent<SyncStatus>).detail;
200
+ * ui.setSyncBadge(status.state, status.lastConvergedAt);
201
+ * });
202
+ *
203
+ * await sync.waitForConvergence();
204
+ * ```
205
+ */
206
+ export declare class StackSyncHandle extends EventTarget {
207
+ private readonly stack;
208
+ private readonly options;
209
+ private readonly direction;
210
+ private readonly live;
211
+ private replication;
212
+ private remote;
213
+ private status;
214
+ private cancelled;
215
+ /** @internal - use {@link ClientStack.sync}. */
216
+ constructor(stack: ClientStack, options: StackSyncOptions);
217
+ /** The current reading. Safe to call at any time; returns a copy. */
218
+ getStatus(): SyncStatus;
219
+ /** The resolved remote, once {@link start} has run. */
220
+ getRemote(): PouchDB.Database | null;
221
+ /**
222
+ * Resolves the remote, runs the schema gate and starts replicating.
223
+ *
224
+ * @returns This handle, once replication is running. Rejects if the remote cannot
225
+ * be resolved or the gate refuses; the handle is left `"error"` in that case.
226
+ * @throws {SyncSchemaMismatchError} When the remote is ahead of this device.
227
+ */
228
+ start(): Promise<this>;
229
+ /**
230
+ * Stops replicating and releases the changes-feed listeners.
231
+ *
232
+ * Idempotent. Called for every live handle when the stack closes.
233
+ */
234
+ cancel(): void;
235
+ /**
236
+ * Cancels and starts again, re-resolving the remote.
237
+ *
238
+ * This is the call to make when a credential is refreshed: a {@link RemoteResolver}
239
+ * function runs again, so the new replication is built on the new token while the
240
+ * counters and `lastConvergedAt` this handle has accumulated stay put.
241
+ *
242
+ * @returns This handle, once replication is running again.
243
+ */
244
+ restart(): Promise<this>;
245
+ /**
246
+ * Resolves the next time the replica converges.
247
+ *
248
+ * @param timeoutMs - How long to wait before rejecting. Defaults to 30 seconds;
249
+ * pass `0` to wait indefinitely.
250
+ * @returns The status at the moment of convergence.
251
+ */
252
+ waitForConvergence(timeoutMs?: number): Promise<SyncStatus>;
253
+ private resolveRemote;
254
+ private checkSchema;
255
+ /**
256
+ * Composes the filter chain: DocStack's internal-document rules, then the caller's
257
+ * class rules, then their own predicate. All three use include-semantics, so the
258
+ * chain is a conjunction - a document replicates only if every stage admits it.
259
+ *
260
+ * The composed function carries an identity derived from every stage's
261
+ * configuration. PouchDB hashes `filter.toString()` into the replication checkpoint,
262
+ * and without that these closures would all render as the same source text: changing
263
+ * which classes replicate would silently resume from the previous configuration's
264
+ * checkpoint and never backfill the newly-admitted documents.
265
+ */
266
+ /**
267
+ * The document ids this stack's configured patches seed.
268
+ *
269
+ * Empty when the stack was opened without patches, which is the common case.
270
+ */
271
+ /**
272
+ * Class names declared ephemeral, resolved once when replication starts.
273
+ *
274
+ * See {@link ClassModel.ephemeral}.
275
+ */
276
+ private ephemeralClasses;
277
+ private seededDocIdsFromPatches;
278
+ private buildFilter;
279
+ private buildReplicationOptions;
280
+ private startReplication;
281
+ private wireCommonEvents;
282
+ private onChange;
283
+ private markConverged;
284
+ private recordError;
285
+ private describeError;
286
+ private setState;
287
+ }
288
+ /** Options for {@link DocStack.sync}, which starts one handle per stack. */
289
+ export interface DocStackSyncOptions extends Omit<StackSyncOptions, "remote"> {
290
+ /**
291
+ * The remote for a given stack. Called once per stack, so an application with a
292
+ * database per workspace answers with a folder per workspace.
293
+ */
294
+ remote: RemoteResolver;
295
+ /**
296
+ * Which stacks to sync. Defaults to all of them.
297
+ */
298
+ stacks?: string[];
299
+ /**
300
+ * The tenant entitlement this replication serves, compiled into per-stack
301
+ * configuration by {@link deriveTenantScope}: stacks outside the scope are not
302
+ * synced at all - withheld structurally, not filtered - and stacks holding a mix of
303
+ * declarations get a class filter. Combines with `stacks` (which pre-narrows the
304
+ * candidates) but not with `classes`, whose slot the compiled rules occupy; narrow
305
+ * further with `filter`. See ADR-0030.
306
+ */
307
+ tenants?: string[];
308
+ }
309
+ /**
310
+ * Every stack's replication under one object.
311
+ *
312
+ * Returned by {@link DocStack.sync}. Holds one {@link StackSyncHandle} per stack and
313
+ * re-dispatches their `"status"` events, so an application renders one badge from one
314
+ * listener however many databases it has open.
315
+ *
316
+ * @example
317
+ * ```typescript
318
+ * const sync = await docstack.sync({ remote: (stack) => driveFor(stack.name) });
319
+ * sync.addEventListener("status", () => render(sync.getStatus()));
320
+ * ```
321
+ */
322
+ export declare class DocStackSyncHandle extends EventTarget {
323
+ /** One handle per stack, keyed by stack name. */
324
+ readonly handles: Map<string, StackSyncHandle>;
325
+ /** @internal - use {@link DocStack.sync}. */
326
+ add(name: string, handle: StackSyncHandle): void;
327
+ /** The stacks this handle covers. What is missing from this list is not
328
+ * replicating - compare against `DocStack.getStacks()`, or use
329
+ * `DocStack.getSyncCoverage()` which does exactly that. */
330
+ get names(): string[];
331
+ /** @internal - use {@link DocStack.removeStack}. Cancels and drops one stack's
332
+ * replication; the rest are untouched. */
333
+ remove(name: string): boolean;
334
+ /** Every stack's status, keyed by stack name. */
335
+ getStatus(): Record<string, SyncStatus>;
336
+ /**
337
+ * The oldest convergence across all stacks - the honest answer to "when was
338
+ * everything last up to date". `null` while any stack has never converged.
339
+ */
340
+ getLastConvergedAt(): number | null;
341
+ /** Cancels every stack's replication. */
342
+ cancel(): void;
343
+ /** Restarts every stack's replication, re-resolving each remote. */
344
+ restart(): Promise<void>;
345
+ }
346
+ export { createReplicationFilter, isInternalDoc, resolveInternalClasses, INTERNAL_DOC_IDS, INTERNAL_DOC_ID_PREFIXES, INTERNAL_DOC_CLASSES, OPTIONAL_INTERNAL_DOC_CLASSES, } from "./internal-docs.js";
347
+ export type { InternalDocFilterOptions } from "./internal-docs.js";
348
+ export { createClassFilter, hasClassRules, DATA_MODEL_CLASSES } from "./class-filter.js";
349
+ export type { ClassFilterOptions } from "./class-filter.js";
350
+ export { deriveTenantScope, classTenants } from "./tenants.js";
351
+ export type { TenantScope } from "./tenants.js";
352
+ export { withFilterIdentity, describeFilter, composeFilterIdentity } from "./filter-identity.js";
@@ -0,0 +1,159 @@
1
+ /**
2
+ * The taxonomy of documents a DocStack stack keeps for itself.
3
+ *
4
+ * Every stack writes a handful of documents that describe *this device's copy* of the
5
+ * database rather than its contents: the system record, the crypto marker, the local
6
+ * patch ledger, class-propagation locks, Mango design documents. Replicating those is
7
+ * never right - two devices each write their own, so they collide on identical ids
8
+ * with unrelated revisions, and pulling a peer's `~system` document would hand
9
+ * `checkSystem` a schema version the local patches have not reached.
10
+ *
11
+ * The list lives here, in the client, because only DocStack knows it - an application
12
+ * author cannot guess it and the set grows as DocStack does.
13
+ *
14
+ * @module
15
+ */
16
+ /**
17
+ * Document identifiers that are device-local in full.
18
+ *
19
+ * - `~system` carries this database's `dbInfo`, `startupTime` and, critically, the
20
+ * `schemaVersion` that {@link ClientStack.checkSystem} reads on every mount.
21
+ * - `~crypto-engine-config` is the marker that pins a database to its encryption
22
+ * setting; a peer's copy says nothing about this one.
23
+ * - `lastDocId` is a local id counter.
24
+ */
25
+ export declare const INTERNAL_DOC_IDS: readonly string[];
26
+ /**
27
+ * Identifier prefixes that mark a document as device-local.
28
+ *
29
+ * - `_local/` never replicates in PouchDB anyway; listed so the predicate is usable
30
+ * outside a replication filter too.
31
+ * - `_design/` documents are Mango indexes built on demand by
32
+ * {@link ClientStack.addDesignDocumentPKs}, including the `-temp` variants.
33
+ * - `~lock-` guards an in-flight class-model propagation on *this* device.
34
+ * - `~log-` is this client's own diagnostic record. Replicating diagnostics costs quota,
35
+ * bandwidth and remote storage on every sync, in both directions, and tells a peer
36
+ * nothing it can use - `logLevel: 'info'` should not be a network cost. They used to
37
+ * carry neither a `~class` nor a recognisable id, so all of them replicated: 111 of
38
+ * 134 documents on one measured remote. See ADR-0023.
39
+ */
40
+ export declare const INTERNAL_DOC_ID_PREFIXES: readonly string[];
41
+ /**
42
+ * `~class` values whose documents describe device-local state.
43
+ *
44
+ * - `~lock` guards an in-flight class-model propagation on *this* device.
45
+ * - `~JobRun` records that *this* client ran a job. A peer's execution history is not
46
+ * something this one can act on, and both write their own.
47
+ */
48
+ export declare const INTERNAL_DOC_CLASSES: readonly string[];
49
+ /**
50
+ * `~class` values that are device-local by default but that a caller may choose to
51
+ * replicate. Sessions belong to the device that authenticated; the patch ledger is
52
+ * written by whichever device applied the patch, and patches themselves ship with the
53
+ * application code rather than through the database.
54
+ */
55
+ export declare const OPTIONAL_INTERNAL_DOC_CLASSES: {
56
+ /** `~UserSession` - one per device, per login. */
57
+ readonly sessions: "~UserSession";
58
+ /** `patch` - the local record of which patches this device has applied. */
59
+ readonly patchLedger: "patch";
60
+ };
61
+ /**
62
+ * Options controlling which of DocStack's own documents stay on this device.
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * // Replicate the patch ledger too, and keep an app-specific draft class local.
67
+ * const filter = createReplicationFilter({
68
+ * replicatePatchLedger: true,
69
+ * extraClasses: ["Draft"],
70
+ * });
71
+ * ```
72
+ */
73
+ export interface InternalDocFilterOptions {
74
+ /** Replicate `~UserSession` documents. Defaults to `false`. */
75
+ replicateSessions?: boolean;
76
+ /**
77
+ * Replicate the documents the system patches seed - the `~*` class models,
78
+ * `Group-Admin`, `Policy-Admin`, `AuthMod-Classic`, the bootstrap `class` and
79
+ * `domain`, the `system` user. Defaults to `false`, because every client applies
80
+ * those patches for itself and so already holds them.
81
+ *
82
+ * This does *not* govern documents of DocStack's classes that an application created
83
+ * - an account, a group, a policy written at runtime. Those bind the synchronised
84
+ * group together and always replicate.
85
+ */
86
+ replicateSystemDocuments?: boolean;
87
+ /**
88
+ * Class names declared `ephemeral`, whose documents describe one run of one client.
89
+ *
90
+ * See {@link ClassModel.ephemeral}. {@link ClientStack.sync} fills this in from the
91
+ * stack's own class models, so a class declared ephemeral is excluded structurally
92
+ * rather than by the filter recognising a payload shape.
93
+ */
94
+ ephemeralClasses?: string[];
95
+ /**
96
+ * Document ids an application's own patches seed, treated like the system-seeded ones.
97
+ *
98
+ * Consumer patches are applied on every client too, so the documents they seed are
99
+ * reconstructible everywhere and need not travel. {@link ClientStack.sync} fills this
100
+ * in from the stack's configured patches.
101
+ */
102
+ extraSeededDocIds?: string[];
103
+ /** Replicate the local `patch` ledger. Defaults to `false`. */
104
+ replicatePatchLedger?: boolean;
105
+ /** Additional exact document ids to keep device-local. */
106
+ extraDocIds?: string[];
107
+ /** Additional id prefixes to keep device-local. */
108
+ extraIdPrefixes?: string[];
109
+ /** Additional `~class` values to keep device-local. */
110
+ extraClasses?: string[];
111
+ }
112
+ /** The shape the predicates need: anything with an id and possibly a `~class`. */
113
+ type MaybeInternalDoc = {
114
+ _id?: string;
115
+ "~class"?: unknown;
116
+ [key: string]: unknown;
117
+ };
118
+ /**
119
+ * Resolves the full set of device-local classes for a given set of options.
120
+ *
121
+ * @param options - Filter options; defaults keep sessions and the patch ledger local.
122
+ * @returns The `~class` values that should not leave this device.
123
+ */
124
+ export declare const resolveInternalClasses: (options?: InternalDocFilterOptions) => string[];
125
+ /**
126
+ * Reports whether a document describes device-local state rather than stack data.
127
+ *
128
+ * @param doc - The document to classify. A missing or id-less value counts as internal,
129
+ * so a malformed change never escapes to a remote.
130
+ * @param options - Which of the optional categories to treat as replicable.
131
+ * @returns `true` when the document should stay on this device.
132
+ *
133
+ * @example
134
+ * ```typescript
135
+ * isInternalDoc({ _id: "~system" }); // true
136
+ * isInternalDoc({ _id: "Task-1", "~class": "Task" }); // false
137
+ * isInternalDoc({ _id: "sess-a", "~class": "~UserSession" }); // true
138
+ * isInternalDoc({ _id: "sess-a", "~class": "~UserSession" }, { replicateSessions: true }); // false
139
+ * ```
140
+ */
141
+ export declare const isInternalDoc: (doc: MaybeInternalDoc | null | undefined, options?: InternalDocFilterOptions) => boolean;
142
+ /**
143
+ * Builds the replication filter DocStack passes to PouchDB.
144
+ *
145
+ * PouchDB filter functions use include-semantics: returning `true` replicates the
146
+ * document. The returned function is pure and closes over nothing but the resolved
147
+ * option lists, so it is safe to hand to `PouchDB.replicate`/`PouchDB.sync`, which
148
+ * call it once per change on the source's changes feed.
149
+ *
150
+ * @param options - Which of the optional categories to treat as replicable.
151
+ * @returns A predicate suitable for `replicate`'s `filter` option.
152
+ *
153
+ * @example
154
+ * ```typescript
155
+ * PouchDB.sync(local, remote, { live: true, retry: true, filter: createReplicationFilter() });
156
+ * ```
157
+ */
158
+ export declare const createReplicationFilter: (options?: InternalDocFilterOptions) => ((doc: MaybeInternalDoc) => boolean);
159
+ export {};