@docstack/client 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/LICENSE +10 -0
- package/README.md +1 -1
- package/lib/core/attribute.d.ts +11 -2
- package/lib/core/class.d.ts +29 -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 +4976 -607
- package/lib/index.umd.js +5004 -621
- 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 -774
- 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 -1827
- 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
|
@@ -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,334 @@
|
|
|
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
|
+
/**
|
|
301
|
+
* Every stack's replication under one object.
|
|
302
|
+
*
|
|
303
|
+
* Returned by {@link DocStack.sync}. Holds one {@link StackSyncHandle} per stack and
|
|
304
|
+
* re-dispatches their `"status"` events, so an application renders one badge from one
|
|
305
|
+
* listener however many databases it has open.
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* ```typescript
|
|
309
|
+
* const sync = await docstack.sync({ remote: (stack) => driveFor(stack.name) });
|
|
310
|
+
* sync.addEventListener("status", () => render(sync.getStatus()));
|
|
311
|
+
* ```
|
|
312
|
+
*/
|
|
313
|
+
export declare class DocStackSyncHandle extends EventTarget {
|
|
314
|
+
/** One handle per stack, keyed by stack name. */
|
|
315
|
+
readonly handles: Map<string, StackSyncHandle>;
|
|
316
|
+
/** @internal - use {@link DocStack.sync}. */
|
|
317
|
+
add(name: string, handle: StackSyncHandle): void;
|
|
318
|
+
/** Every stack's status, keyed by stack name. */
|
|
319
|
+
getStatus(): Record<string, SyncStatus>;
|
|
320
|
+
/**
|
|
321
|
+
* The oldest convergence across all stacks - the honest answer to "when was
|
|
322
|
+
* everything last up to date". `null` while any stack has never converged.
|
|
323
|
+
*/
|
|
324
|
+
getLastConvergedAt(): number | null;
|
|
325
|
+
/** Cancels every stack's replication. */
|
|
326
|
+
cancel(): void;
|
|
327
|
+
/** Restarts every stack's replication, re-resolving each remote. */
|
|
328
|
+
restart(): Promise<void>;
|
|
329
|
+
}
|
|
330
|
+
export { createReplicationFilter, isInternalDoc, resolveInternalClasses, INTERNAL_DOC_IDS, INTERNAL_DOC_ID_PREFIXES, INTERNAL_DOC_CLASSES, OPTIONAL_INTERNAL_DOC_CLASSES, } from "./internal-docs.js";
|
|
331
|
+
export type { InternalDocFilterOptions } from "./internal-docs.js";
|
|
332
|
+
export { createClassFilter, hasClassRules, DATA_MODEL_CLASSES } from "./class-filter.js";
|
|
333
|
+
export type { ClassFilterOptions } from "./class-filter.js";
|
|
334
|
+
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 {};
|