@docstack/client 0.1.6 → 0.2.0

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.
@@ -142,7 +142,9 @@ export declare class SyncSchemaMismatchError extends Error {
142
142
  readonly localVersion: string | undefined;
143
143
  /** The schema version the remote was last written with. */
144
144
  readonly remoteVersion: string;
145
- constructor(stack: string, localVersion: string | undefined, remoteVersion: string);
145
+ /** Which half of the gate refused: the system schema, or the application's consumer patches. */
146
+ readonly scope: "system" | "consumer";
147
+ constructor(stack: string, localVersion: string | undefined, remoteVersion: string, scope?: "system" | "consumer");
146
148
  }
147
149
  /**
148
150
  * The document DocStack keeps on a remote to record which schema wrote it.
@@ -158,6 +160,12 @@ export interface SyncMetaDoc {
158
160
  _rev?: string;
159
161
  /** Highest schema version any device has pushed to this remote. */
160
162
  schemaVersion?: string;
163
+ /**
164
+ * Highest *consumer* patch version any device has pushed. The system version
165
+ * alone cannot see consumer-schema skew - two devices on the same build always
166
+ * agree on it, whatever their application patches are doing (ADR-0040).
167
+ */
168
+ consumerSchemaVersion?: string;
161
169
  /** Application version of the device that last wrote it, for diagnostics. */
162
170
  appVersion?: string;
163
171
  /** When it was last written. */
@@ -173,6 +181,13 @@ export interface SyncMetaDoc {
173
181
  * @returns The recorded version, or `null` for a remote nobody has written yet.
174
182
  */
175
183
  export declare const readRemoteSchemaVersion: (remote: PouchDB.Database) => Promise<string | null>;
184
+ /**
185
+ * Reads the highest consumer patch version recorded on a remote.
186
+ *
187
+ * `null` for a remote nobody has written, or one written only by builds that
188
+ * predate the consumer half of the gate.
189
+ */
190
+ export declare const readRemoteConsumerSchemaVersion: (remote: PouchDB.Database) => Promise<string | null>;
176
191
  /**
177
192
  * Records this device's schema version on a remote, if it is the newest seen.
178
193
  *
@@ -180,7 +195,7 @@ export declare const readRemoteSchemaVersion: (remote: PouchDB.Database) => Prom
180
195
  * @param schemaVersion - The local schema version; a missing value writes nothing.
181
196
  * @param appVersion - The local application version, stored for diagnostics.
182
197
  */
183
- export declare const publishSchemaVersion: (remote: PouchDB.Database, schemaVersion: string | undefined, appVersion?: string) => Promise<void>;
198
+ export declare const publishSchemaVersion: (remote: PouchDB.Database, schemaVersion: string | undefined, appVersion?: string, consumerSchemaVersion?: string | null) => Promise<void>;
184
199
  /**
185
200
  * One stack's replication: its lifecycle, its filter, and its convergence state.
186
201
  *
@@ -296,6 +311,15 @@ export interface DocStackSyncOptions extends Omit<StackSyncOptions, "remote"> {
296
311
  * Which stacks to sync. Defaults to all of them.
297
312
  */
298
313
  stacks?: string[];
314
+ /**
315
+ * The tenant entitlement this replication serves, compiled into per-stack
316
+ * configuration by {@link deriveTenantScope}: stacks outside the scope are not
317
+ * synced at all - withheld structurally, not filtered - and stacks holding a mix of
318
+ * declarations get a class filter. Combines with `stacks` (which pre-narrows the
319
+ * candidates) but not with `classes`, whose slot the compiled rules occupy; narrow
320
+ * further with `filter`. See ADR-0030.
321
+ */
322
+ tenants?: string[];
299
323
  }
300
324
  /**
301
325
  * Every stack's replication under one object.
@@ -315,6 +339,13 @@ export declare class DocStackSyncHandle extends EventTarget {
315
339
  readonly handles: Map<string, StackSyncHandle>;
316
340
  /** @internal - use {@link DocStack.sync}. */
317
341
  add(name: string, handle: StackSyncHandle): void;
342
+ /** The stacks this handle covers. What is missing from this list is not
343
+ * replicating - compare against `DocStack.getStacks()`, or use
344
+ * `DocStack.getSyncCoverage()` which does exactly that. */
345
+ get names(): string[];
346
+ /** @internal - use {@link DocStack.removeStack}. Cancels and drops one stack's
347
+ * replication; the rest are untouched. */
348
+ remove(name: string): boolean;
318
349
  /** Every stack's status, keyed by stack name. */
319
350
  getStatus(): Record<string, SyncStatus>;
320
351
  /**
@@ -331,4 +362,6 @@ export { createReplicationFilter, isInternalDoc, resolveInternalClasses, INTERNA
331
362
  export type { InternalDocFilterOptions } from "./internal-docs.js";
332
363
  export { createClassFilter, hasClassRules, DATA_MODEL_CLASSES } from "./class-filter.js";
333
364
  export type { ClassFilterOptions } from "./class-filter.js";
365
+ export { deriveTenantScope, classTenants } from "./tenants.js";
366
+ export type { TenantScope } from "./tenants.js";
334
367
  export { withFilterIdentity, describeFilter, composeFilterIdentity } from "./filter-identity.js";
@@ -0,0 +1,67 @@
1
+ import type { ClassModel } from "@docstack/shared";
2
+ import type { ClassFilterOptions } from "./class-filter.js";
3
+ import type ClientStack from "../stack.js";
4
+ /**
5
+ * Tenant scoping for replication channels.
6
+ *
7
+ * A tenant is a stack (ADR-0030). `Class.tenants` declares which tenant spaces a class
8
+ * belongs to - a static list, so partitioning and channel scopes are derivable before
9
+ * any data exists. This module compiles a channel's *entitlement* (the tenants it may
10
+ * see) into configuration the sync layer already understands: which stacks the channel
11
+ * is served at all, and a class filter for stacks holding a mix of declarations.
12
+ *
13
+ * The split matters (ADR-0030 §5): the *partition* is the datamodel's, but the
14
+ * *entitlement* is the serving side's to grant - it cannot come from the datamodel,
15
+ * because every application ships its own model and none is authoritative about the
16
+ * others' entitlements. Enforcement then rides the existing filter chain, which is a
17
+ * conjunction: a derived filter can only narrow what the declarations admit.
18
+ *
19
+ * The strongest grant here is the one that is not a filter at all: a stack outside the
20
+ * scope is simply never served, so its namespace is unreachable rather than filtered.
21
+ *
22
+ * @module
23
+ */
24
+ /** A channel's entitlement, compiled into sync configuration. */
25
+ export interface TenantScope {
26
+ /**
27
+ * Stacks this entitlement is served at all. A stack not listed is withheld
28
+ * structurally - no replication is started against it, which is a stronger grant
29
+ * than any filter.
30
+ */
31
+ stacks: string[];
32
+ /**
33
+ * Class rules per served stack, present only where the stack needs them: an
34
+ * `exclude` where classes declared for other tenants share an entitled stack, an
35
+ * `include` where a stack is served only because entitled classes live in it.
36
+ */
37
+ classes: Record<string, ClassFilterOptions>;
38
+ }
39
+ /**
40
+ * Normalizes a class model's tenant declaration.
41
+ *
42
+ * @param model - Any object carrying (or omitting) a `tenants` declaration.
43
+ * @returns The declared tenant names; empty for a tenant-neutral class.
44
+ */
45
+ export declare const classTenants: (model: Partial<Pick<ClassModel, "tenants">> | null | undefined) => string[];
46
+ /**
47
+ * Compiles an entitlement into the stacks it reaches and the class rules it needs.
48
+ *
49
+ * A stack is served when its name is an entitled tenant - a tenant *is* a stack - or
50
+ * when it holds at least one class declared for an entitled tenant. Within a served
51
+ * stack:
52
+ *
53
+ * - classes declared for an entitled tenant travel;
54
+ * - tenant-neutral classes follow their stack: they travel when the stack itself is the
55
+ * entitled tenant, which is what keeps a datamodel with no declarations at today's
56
+ * behavior exactly;
57
+ * - classes declared only for other tenants are excluded.
58
+ *
59
+ * Resolved once per call, the way the sync layer resolves ephemeral classes when a
60
+ * replication starts (ADR-0028): a declaration that changes later takes effect on the
61
+ * next `sync()`, never silently mid-stream.
62
+ *
63
+ * @param stacks - The candidate stacks, typically every open stack.
64
+ * @param entitlement - The tenant names this channel may see.
65
+ * @returns Which stacks to serve, and per-stack class rules where needed.
66
+ */
67
+ export declare const deriveTenantScope: (stacks: ClientStack[], entitlement: string[]) => Promise<TenantScope>;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Errors of the transaction engine (ADR-0039).
3
+ *
4
+ * Every one of them leaves the database untouched: a transaction failure is a refusal,
5
+ * never a partial application. The one exception is named where it happens -
6
+ * a commit on a non-atomic adapter can land a subset, and that outcome is reported as
7
+ * a `partial` status on the handle, not thrown as one of these.
8
+ *
9
+ * @module
10
+ */
11
+ /** Raised by `beginTransaction()` on a stack opened without `transactions: true`. */
12
+ export declare class TransactionsDisabledError extends Error {
13
+ name: string;
14
+ constructor(stackName: string);
15
+ }
16
+ /** Raised when a handle is used in a state that cannot accept the operation. */
17
+ export declare class TransactionStateError extends Error {
18
+ name: string;
19
+ constructor(transactionId: string, status: string, operation: string);
20
+ }
21
+ /**
22
+ * Raised when the validation sweep refuses a document - at stage time (the write is
23
+ * not staged) or at commit time (nothing is written, the transaction stays open).
24
+ */
25
+ export declare class TransactionValidationError extends Error {
26
+ name: string;
27
+ /** The document that failed. */
28
+ readonly docId: string | undefined;
29
+ constructor(message: string, docId?: string);
30
+ }
31
+ /**
32
+ * Raised by commit when a staged document's base revision no longer matches the
33
+ * stored winner - a direct write, another transaction's commit, or replication moved
34
+ * it. Nothing is written; the transaction stays open for re-staging or discard.
35
+ */
36
+ export declare class TransactionConflictError extends Error {
37
+ name: string;
38
+ readonly conflicts: {
39
+ id: string;
40
+ baseRev: string | undefined;
41
+ currentRev: string | undefined;
42
+ }[];
43
+ constructor(conflicts: {
44
+ id: string;
45
+ baseRev: string | undefined;
46
+ currentRev: string | undefined;
47
+ }[]);
48
+ }
49
+ /**
50
+ * Raised at stage time for documents transactions cannot carry: class models (their
51
+ * write propagates to other documents mid-pipeline and cannot be staged or rolled
52
+ * back - ADR-0039), `_local/` device state, and design documents.
53
+ */
54
+ export declare class TransactionUnsupportedDocError extends Error {
55
+ name: string;
56
+ constructor(docId: string, reason: string);
57
+ }
@@ -0,0 +1,165 @@
1
+ import { Document } from "@docstack/shared";
2
+ import type ClientStack from "../stack.js";
3
+ import { TransactionStage, StagedEntry } from "./stage.js";
4
+ import { MangoSort } from "./overlay.js";
5
+ import type { TransactionEngine, TransactionCommitReport } from "./index.js";
6
+ export type TransactionStatus = "open" | "committed" | "discarded" | "partial";
7
+ /**
8
+ * One transaction: a private write journal plus a read view that overlays it on
9
+ * committed state (ADR-0039).
10
+ *
11
+ * Writes through the handle are validated at the call site (the sweep - failing
12
+ * stages nothing) and stage in memory; nothing reaches the database until
13
+ * {@link commit}, which flushes the journal as one batch through the stack's full
14
+ * authoring pipeline. Reads through the handle see the journal; `stack.db`, other
15
+ * handles, replication and live subscriptions see only committed state.
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const t = stack.beginTransaction();
20
+ * await t.createDoc(null, "Task", { title: "write-up" });
21
+ * await t.db.put({ ...(await t.db.get("Task-77")), done: true });
22
+ * const drafted = await t.findDocuments({ "~class": { $eq: "Task" } });
23
+ * const report = await t.commit(); // or stack.commit(t)
24
+ * ```
25
+ */
26
+ export declare class TransactionHandle {
27
+ readonly id: string;
28
+ /** @internal */
29
+ readonly stage: TransactionStage;
30
+ /** @internal - ids this handle minted, counted into the id counter after commit. */
31
+ readonly mintedIds: Set<string>;
32
+ /**
33
+ * True for handles DocStack mints for its own machinery (patch application,
34
+ * ADR-0042). Internal handles may stage class models; public ones refuse them.
35
+ * @internal
36
+ */
37
+ readonly internal: boolean;
38
+ private statusValue;
39
+ private readonly stack;
40
+ private readonly engine;
41
+ /** The db-like surface: staged writes, overlaid reads. */
42
+ readonly db: TransactionDb;
43
+ /** @internal */
44
+ constructor(stack: ClientStack, engine: TransactionEngine, id: string, internal?: boolean);
45
+ get status(): TransactionStatus;
46
+ /** @internal */
47
+ setStatus(status: TransactionStatus): void;
48
+ stagedCount(): number;
49
+ /** @internal */
50
+ assertWritable(operation: string): void;
51
+ /**
52
+ * Stages a write. The sweep runs first: a document that fails validation, policy,
53
+ * or the locked-stack check is not staged and the journal is untouched.
54
+ * @internal
55
+ */
56
+ stageWrite(doc: Document, op?: "write" | "delete"): Promise<StagedEntry>;
57
+ /**
58
+ * Creates or updates a document in the transaction - `stack.createDoc`'s UX with
59
+ * a staged destination: `docId: null` mints an id, an existing id merges params
60
+ * over the overlay-visible document.
61
+ */
62
+ createDoc(docId: string | null, type: string, params: {
63
+ [key: string]: any;
64
+ }): Promise<Document>;
65
+ /** Batch counterpart of {@link createDoc}; validated sequentially, fail-fast. */
66
+ createDocs(docs: {
67
+ docId: string | null;
68
+ params: {
69
+ [key: string]: any;
70
+ };
71
+ }[], type: string): Promise<Document[]>;
72
+ /** Soft-deletes in the transaction: the overlay stops showing the document under the default `active: true`. */
73
+ deleteDocument(docId: string): Promise<boolean>;
74
+ /** The stack's polished read, against this transaction's view. */
75
+ findDocuments<T extends Document = Document>(selector: {
76
+ [key: string]: any;
77
+ }, fields?: string[], skip?: number, limit?: number, sort?: MangoSort): Promise<{
78
+ [key: string]: any;
79
+ docs: T[];
80
+ }>;
81
+ /**
82
+ * SQL against this transaction's view. The executor reaches data only through
83
+ * stack APIs, so a facade routes them at the overlay; LIMIT/OFFSET pushdown and
84
+ * sort indexes are disabled while staged - staged documents exist in no index,
85
+ * so windows and orderings must be computed after the merge.
86
+ */
87
+ query(sql: string, ...params: any[]): Promise<{
88
+ rows: any;
89
+ ast: (import("@docstack/shared").SelectAST | import("@docstack/shared").UnionAST)[];
90
+ }>;
91
+ /** Flushes the journal - sugar for `stack.commit(t)`. */
92
+ commit(): Promise<TransactionCommitReport>;
93
+ /** Drops the journal - sugar for `stack.discardTransaction(t)`. */
94
+ discard(): void;
95
+ }
96
+ /**
97
+ * The handle's db-like surface. Writes stage; reads overlay. Not a Proxy over the
98
+ * guarded db on purpose: this object *has no* adapter methods or escape hatches to
99
+ * forward, so staging cannot become a fourth door around the authoring path.
100
+ */
101
+ export declare class TransactionDb {
102
+ private readonly stack;
103
+ private readonly handle;
104
+ /** @internal */
105
+ constructor(stack: ClientStack, handle: TransactionHandle);
106
+ get(docId: string, options?: any): Promise<Document>;
107
+ bulkGet(request: {
108
+ docs: {
109
+ id: string;
110
+ rev?: string;
111
+ }[];
112
+ }): Promise<{
113
+ results: any[];
114
+ }>;
115
+ /**
116
+ * Raw-style Mango find over the transaction's view. Like `stack.db.find`, this
117
+ * skips the read pipeline (no policy filter, no decryption of committed rows);
118
+ * `findDocuments` on the handle is the polished read.
119
+ */
120
+ find(query: {
121
+ selector: {
122
+ [key: string]: any;
123
+ };
124
+ fields?: string[];
125
+ skip?: number;
126
+ limit?: number;
127
+ sort?: any;
128
+ }): Promise<{
129
+ docs: Document[];
130
+ }>;
131
+ put(doc: Document, options?: any): Promise<{
132
+ ok: true;
133
+ id: string;
134
+ rev?: string;
135
+ staged: true;
136
+ }>;
137
+ post(doc: Document): Promise<{
138
+ ok: true;
139
+ id: string;
140
+ rev?: string;
141
+ staged: true;
142
+ }>;
143
+ /** Hard removal, staged: the commit writes `_deleted: true`. Soft deletion is `handle.deleteDocument`. */
144
+ remove(doc: {
145
+ _id: string;
146
+ _rev?: string;
147
+ } | string, rev?: string): Promise<{
148
+ ok: true;
149
+ id: string;
150
+ staged: true;
151
+ }>;
152
+ /**
153
+ * Stages a batch. Validated sequentially - the first refusal unwinds every entry
154
+ * this call staged, so a failing batch stages nothing. Documents stage before
155
+ * relations, mirroring the commit batch, so a relation and its endpoint can
156
+ * arrive in one array in any order.
157
+ */
158
+ bulkDocs(docs: Document[] | {
159
+ docs: Document[];
160
+ }, options?: any): Promise<{
161
+ ok: true;
162
+ id: string;
163
+ staged: true;
164
+ }[]>;
165
+ }
@@ -0,0 +1,82 @@
1
+ import type ClientStack from "../stack.js";
2
+ import { TransactionHandle } from "./handle.js";
3
+ export { TransactionHandle, TransactionDb } from "./handle.js";
4
+ export type { TransactionStatus } from "./handle.js";
5
+ export { TransactionStage } from "./stage.js";
6
+ export type { StagedEntry, StagedOp } from "./stage.js";
7
+ export { stageCoversSelector, mergeStageIntoResults, widenProjection } from "./overlay.js";
8
+ export { classFromStage } from "./sweep.js";
9
+ export { TransactionsDisabledError, TransactionStateError, TransactionValidationError, TransactionConflictError, TransactionUnsupportedDocError, } from "./errors.js";
10
+ /** What one commit did, and on what guarantee. */
11
+ export type TransactionCommitReport = {
12
+ transactionId: string;
13
+ /** Documents that landed, with their new revisions. */
14
+ written: {
15
+ id: string;
16
+ rev: string;
17
+ }[];
18
+ /** Documents that did not - possible only on adapters where `atomicBatch` is false. */
19
+ failed: {
20
+ id: string;
21
+ error: string;
22
+ name?: string;
23
+ }[];
24
+ /** Journal size at the moment commit ran. */
25
+ stagedCount: number;
26
+ durationMs: number;
27
+ /**
28
+ * The storage adapter's honest guarantee for this commit: `atomicBatch: true`
29
+ * means the batch landed (or failed) as one storage transaction; `false` means
30
+ * per-document results, mitigated by the rev pre-flight but not eliminated.
31
+ */
32
+ adapter: {
33
+ name: string;
34
+ atomicBatch: boolean;
35
+ };
36
+ };
37
+ /**
38
+ * Named write transactions for one stack (ADR-0039).
39
+ *
40
+ * Enabled per stack by `transactions: true` in its configuration - the flag only
41
+ * unlocks {@link begin}; direct writes stay immediate, and the framework's own
42
+ * writers (scheduler, jobs, sync) always write directly. The stage lives above the
43
+ * plugin: nothing a transaction does touches the database until commit, and commit
44
+ * is exactly one `stack.db.bulkDocs` through the full authoring pipeline.
45
+ */
46
+ export declare class TransactionEngine {
47
+ private readonly stack;
48
+ private readonly enabled;
49
+ private readonly handles;
50
+ /** Commits serialize here so one commit's rev pre-flight cannot be invalidated by another's write. */
51
+ private commitChain;
52
+ constructor(stack: ClientStack, enabled: boolean);
53
+ isEnabled(): boolean;
54
+ /** How many transactions are currently open (or partial). */
55
+ openCount(): number;
56
+ begin(): TransactionHandle;
57
+ /**
58
+ * Opens a transaction for DocStack's own machinery - patch application
59
+ * (ADR-0042). Independent of the `transactions: true` config gate (the flag
60
+ * governs the consumer feature, not the framework's internals) and permitted to
61
+ * stage class models: an internal handle claims staged validation and a single
62
+ * class-write batch, never propagation atomicity.
63
+ * @internal
64
+ */
65
+ beginInternal(): TransactionHandle;
66
+ private resolve;
67
+ /**
68
+ * Drops a transaction's journal. Idempotent, and a no-op on a handle already in
69
+ * a terminal state - discarding what is already gone is not an error.
70
+ */
71
+ discard(t: TransactionHandle | string): void;
72
+ /** Discards every open transaction - what `close()` and `reset()` do. */
73
+ discardAll(): void;
74
+ /**
75
+ * Flushes a transaction's journal as one batch through the stack's authoring
76
+ * pipeline. Refusals - validation, or a staged document whose base revision
77
+ * moved - throw with nothing persisted and the transaction still open.
78
+ */
79
+ commit(t: TransactionHandle | string): Promise<TransactionCommitReport>;
80
+ private commitNow;
81
+ private adapterInfo;
82
+ }
@@ -0,0 +1,66 @@
1
+ import { Document } from "@docstack/shared";
2
+ import { TransactionStage } from "./stage.js";
3
+ /** The sort shape `findDocuments` accepts. */
4
+ export type MangoSort = {
5
+ [field: string]: "asc" | "desc";
6
+ }[] | string[];
7
+ /**
8
+ * True when a query with this selector could see documents this stage holds.
9
+ *
10
+ * Derived from the selector's `~class` / `~domain` constraint against the stage's
11
+ * partitions; a selector naming no class is answered conservatively. This is the
12
+ * per-query fast path: a find over a class the transaction never touched runs
13
+ * exactly as it would outside the transaction.
14
+ */
15
+ export declare const stageCoversSelector: (stage: TransactionStage, selector: {
16
+ [key: string]: any;
17
+ }) => boolean;
18
+ /**
19
+ * Fields the widened committed query (and the staged docs) must carry beyond the
20
+ * caller's projection, so masking, sorting and the read pipeline can work; the
21
+ * extras are stripped again after the merge.
22
+ */
23
+ export declare const widenProjection: (fields: string[] | undefined, sort: MangoSort | undefined) => {
24
+ queryFields: string[] | undefined;
25
+ extras: string[];
26
+ };
27
+ /**
28
+ * Merges a stage into a committed query result: every staged id masks its committed
29
+ * row (superseded and deleted alike), staged writes matching the selector join the
30
+ * set, then sort, window and projection apply in memory - the database's index can
31
+ * never see a staged document, so the window has to be computed after the union.
32
+ *
33
+ * `committedDocs` must come from a query WITHOUT skip/limit (the mask changes what
34
+ * the window contains) and carrying `widenProjection`'s fields.
35
+ */
36
+ export declare const mergeStageIntoResults: (stage: TransactionStage, selector: {
37
+ [key: string]: any;
38
+ }, committedDocs: Document[], options?: {
39
+ sort?: MangoSort;
40
+ skip?: number;
41
+ limit?: number;
42
+ fields?: string[];
43
+ extras?: string[];
44
+ }) => Document[];
45
+ /** A PouchDB-shaped `not_found`, so overlay reads refuse like the database does. */
46
+ export declare const notFoundError: (id: string) => any;
47
+ /**
48
+ * Read-your-writes for a point read: a staged delete is a 404, a staged write is the
49
+ * authored plaintext (its `_rev` is the base revision - the revision the commit will
50
+ * replace), anything else is the stack's ordinary decrypting read.
51
+ */
52
+ export declare const overlayGet: (stack: {
53
+ db: any;
54
+ }, stage: TransactionStage, id: string, options?: any) => Promise<Document>;
55
+ /** `bulkGet` counterpart of {@link overlayGet}, preserving request order. */
56
+ export declare const overlayBulkGet: (stack: {
57
+ db: any;
58
+ }, stage: TransactionStage, request: {
59
+ docs: {
60
+ id: string;
61
+ rev?: string;
62
+ }[];
63
+ [key: string]: any;
64
+ }) => Promise<{
65
+ results: any[];
66
+ }>;
@@ -0,0 +1,50 @@
1
+ import { Document } from "@docstack/shared";
2
+ /** What one staged entry means for its document id. */
3
+ export type StagedOp = "write" | "delete";
4
+ export interface StagedEntry {
5
+ /** The authored document, plaintext, cloned at stage time. */
6
+ doc: Document;
7
+ /** The winning revision the entry was staged against; absent for a new document. */
8
+ baseRev?: string;
9
+ op: StagedOp;
10
+ /** True when the id did not exist (in stage or store) when first staged. */
11
+ isNew: boolean;
12
+ stagedAt: number;
13
+ }
14
+ /**
15
+ * The write journal of one transaction: authored documents keyed by id, in memory.
16
+ *
17
+ * Also partitioned by class (`~class`, or `~domain` for relations) so a read can ask
18
+ * "could this stage affect a query over class X" in O(1) - the overlay only pays the
19
+ * merge for queries whose class the transaction actually touched (ADR-0039).
20
+ */
21
+ export declare class TransactionStage {
22
+ private entries;
23
+ private partitions;
24
+ private partitionKeys;
25
+ /**
26
+ * Stages an entry. Re-staging an id replaces the document but keeps the original
27
+ * `baseRev` and `isNew` - the conflict check is against the world as it was when
28
+ * the transaction first touched the id, not against its own previous draft.
29
+ */
30
+ set(id: string, entry: StagedEntry): void;
31
+ get(id: string): StagedEntry | undefined;
32
+ has(id: string): boolean;
33
+ get size(): number;
34
+ ids(): string[];
35
+ /** Entries in stage order (insertion order of first staging). */
36
+ values(): StagedEntry[];
37
+ hasPartition(name: string): boolean;
38
+ /** Keeps only the given ids - what a partial commit leaves behind. */
39
+ retain(ids: Set<string>): void;
40
+ /**
41
+ * A point-in-time copy of the journal, for {@link restore}. Used by the patch
42
+ * chain (ADR-0044) to unwind exactly one patch's staging - a pre-apply job's
43
+ * writes included - when a locked refusal converts that patch to a deferral
44
+ * while the already-staged prefix goes on to commit.
45
+ */
46
+ snapshot(): Map<string, StagedEntry>;
47
+ restore(snapshot: Map<string, StagedEntry>): void;
48
+ remove(id: string): void;
49
+ clear(): void;
50
+ }
@@ -0,0 +1,26 @@
1
+ import Class from "../class.js";
2
+ import type ClientStack from "../stack.js";
3
+ import { TransactionStage, StagedEntry } from "./stage.js";
4
+ /**
5
+ * Resolves a class from the transaction's own stage: the ADR-0043 rule
6
+ * (the batch outranks the store - it is what is about to be committed) applied to
7
+ * staging. A patch chain's job can create documents of a class an earlier patch
8
+ * staged (ADR-0044), and the sweep must judge them by that staged model, not by a
9
+ * committed predecessor or a not-found. Built DETACHED - `Class.get` + `setModel`,
10
+ * never `buildFromModel`, which writes rev-less models (ADR-0043).
11
+ */
12
+ export declare const classFromStage: (stack: ClientStack, stage: TransactionStage, className: string) => Class | null;
13
+ /**
14
+ * The validation sweep - the transaction's atomicity boundary in practice.
15
+ *
16
+ * Runs read-only checks against public stack APIs: it decides whether a document
17
+ * *could* be written, and touches nothing. Stage time runs it so a bad write fails at
18
+ * the call site with zero consequences; commit re-runs it so the batch is judged
19
+ * against the world as it stands at commit. The commit-time pipeline (the plugin's
20
+ * `bulkDocs`) remains the sole authority - this sweep is a deliberate subset, and a
21
+ * document it passes can still be refused there, atomically for the whole batch.
22
+ */
23
+ export declare const sweepEntry: (stack: ClientStack, stage: TransactionStage, entry: StagedEntry, options?: {
24
+ allowClassModels?: boolean;
25
+ skipPolicy?: boolean;
26
+ }) => Promise<void>;
package/lib/index.d.ts CHANGED
@@ -1,5 +1,16 @@
1
1
  import { DocStack } from "./core/index.js";
2
2
  export { ClientStack, Class, Domain, Attribute, Trigger, DocStack } from "./core/index.js";
3
+ /**
4
+ * Running jobs unattended.
5
+ *
6
+ * `JobEngine` executes a job when asked; `JobScheduler` decides when to ask, under the
7
+ * constraints a client imposes — an app that is closed most of the time, timers that
8
+ * freeze, several devices holding replicas of the same `~Job`, and job content that
9
+ * replicates and is executable. It is mounted at `stack.jobScheduler` and started by the
10
+ * application, which names the jobs allowed to run with nobody watching.
11
+ */
12
+ export { JobEngine, JobScheduler, JOB_SCHEDULE_DOC_ID, parseSchedule, nextOccurrence } from "./core/index.js";
13
+ export type { SchedulerOptions, SchedulerHost, JobScheduleState, TickReport, SkipReason, ParsedSchedule, } from "./core/index.js";
3
14
  /**
4
15
  * The sync layer: lifecycle, filtering, convergence state and the schema gate.
5
16
  *
@@ -7,15 +18,25 @@ export { ClientStack, Class, Domain, Attribute, Trigger, DocStack } from "./core
7
18
  * application hands over, so DocStack never learns about Google Drive, Firestore or
8
19
  * anything else, and no consumer pays for a transport it does not use.
9
20
  */
10
- export { StackSyncHandle, DocStackSyncHandle, SyncSchemaMismatchError, SYNC_META_DOC_ID, readRemoteSchemaVersion, publishSchemaVersion, createReplicationFilter, isInternalDoc, resolveInternalClasses, createClassFilter, hasClassRules, DATA_MODEL_CLASSES, withFilterIdentity, describeFilter, INTERNAL_DOC_IDS, INTERNAL_DOC_ID_PREFIXES, INTERNAL_DOC_CLASSES, OPTIONAL_INTERNAL_DOC_CLASSES, StackWriteGuardError, StackLockedError, deriveKeyId, isEncryptedPayload, } from "./core/index.js";
21
+ export { StackSyncHandle, DocStackSyncHandle, SyncSchemaMismatchError, SYNC_META_DOC_ID, readRemoteSchemaVersion, readRemoteConsumerSchemaVersion, publishSchemaVersion, createReplicationFilter, isInternalDoc, resolveInternalClasses, createClassFilter, hasClassRules, DATA_MODEL_CLASSES, withFilterIdentity, describeFilter, INTERNAL_DOC_IDS, INTERNAL_DOC_ID_PREFIXES, INTERNAL_DOC_CLASSES, OPTIONAL_INTERNAL_DOC_CLASSES, StackWriteGuardError, StackLockedError, deriveKeyId, isEncryptedPayload, deriveTenantScope, classTenants, } from "./core/index.js";
11
22
  export { SYSTEM_SEEDED_DOC_IDS, collectQueryClasses } from "./core/index.js";
12
23
  export type { EncryptedPayload, ClassBuildOptions } from "./core/index.js";
24
+ /**
25
+ * Named write transactions (ADR-0039).
26
+ *
27
+ * Opt-in per stack via `transactions: true`. A handle stages validated writes in
28
+ * memory and reads its own staged state overlaid on committed state; `commit`
29
+ * flushes the journal as one batch through the full authoring pipeline, and the
30
+ * report states the storage adapter's honest atomicity guarantee.
31
+ */
32
+ export { TransactionEngine, TransactionHandle, TransactionDb, TransactionsDisabledError, TransactionStateError, TransactionValidationError, TransactionConflictError, TransactionUnsupportedDocError, } from "./core/index.js";
33
+ export type { TransactionCommitReport, TransactionStatus } from "./core/index.js";
13
34
  /**
14
35
  * Moving application content between stacks, without the datamodel that describes it.
15
36
  */
16
37
  export { CONTENT_EXPORT_FORMAT, META_CLASSES, isContentClassName, isContentDocument, isContentRelation, } from "./core/index.js";
17
38
  export type { ContentExport, ContentExportOptions, ContentImportOptions, ContentImportReport, ContentImportIssue, } from "./core/index.js";
18
- export type { SyncDirection, SyncState, SyncStatus, StackSyncOptions, DocStackSyncOptions, RemoteResolver, SyncMetaDoc, InternalDocFilterOptions, ClassFilterOptions, } from "./core/index.js";
39
+ export type { SyncDirection, SyncState, SyncStatus, StackSyncOptions, DocStackSyncOptions, RemoteResolver, SyncMetaDoc, InternalDocFilterOptions, ClassFilterOptions, TenantScope, } from "./core/index.js";
19
40
  /**
20
41
  * Document-modelling types, re-exported from `@docstack/shared`.
21
42
  *
@@ -26,5 +47,5 @@ export type { SyncDirection, SyncState, SyncStatus, StackSyncOptions, DocStackSy
26
47
  * Note that `Document` shadows the DOM's global `Document` in whichever module imports
27
48
  * it; alias it (`import type { Document as DocStackDocument }`) in code that needs both.
28
49
  */
29
- export type { AttributeType, AttributeTypeConfig, AttributeModel, ClassModel, DomainModel, TriggerModel, Document, RelationDocument, Patch, SelectAST, UnionAST, ClientCredentials, DocstackReady, StackConfig, StackOptions, } from "@docstack/shared";
50
+ export type { AttributeType, AttributeTypeConfig, AttributeModel, ClassModel, DomainModel, TriggerModel, Document, RelationDocument, Patch, PatchJob, SelectAST, UnionAST, ClientCredentials, DocstackReady, StackConfig, StackOptions, } from "@docstack/shared";
30
51
  export default DocStack;