@jarenjs/linq 0.56.0 → 0.67.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.
package/types/db.d.ts CHANGED
@@ -13,16 +13,18 @@
13
13
  * subpath has them; no other subpath of the package refers to them.
14
14
  */
15
15
 
16
- import type { AsyncSequence, AsyncExplanation, Expr, BoolExpr, OrderOptions } from './index.js';
16
+ import type { AsyncSequence, AsyncExplanation, Expr, ExprBase, BoolExpr, OrderOptions } from './index.js';
17
17
  import type { ModelDocument, CollectionSpec, InferMeta } from './model.js';
18
18
  import type {
19
19
  EntityMeta, MetaMap, TypedEntitySet, TypedStore, TypedLoadSpec, Loaded,
20
20
  } from '@jarenjs/db/typed';
21
21
  import type {
22
22
  Collection, ExecuteOptions, LiveOptions, LiveQuery, LoadExplanation, OpenStoreOptions,
23
- SaveReport, Store, StoreCapabilities,
23
+ SavepointController, SaveReport, StoreCapabilities, TransactionStore,
24
+ EntityCursorOptions, QueryCursor, LoadContinuation, Page,
24
25
  } from '@jarenjs/db';
25
26
  import type { JarenValidator } from '@jarenjs/validate';
27
+ import type { Runtime } from '@jarenjs/core/runtime';
26
28
 
27
29
  // ————— open —————
28
30
 
@@ -110,6 +112,12 @@ export type IncludeSpec<E extends MetaMap<E>, M extends EntityMeta> =
110
112
  readonly orderBy?: OrderKey<M['doc']> | readonly OrderKey<M['doc']>[];
111
113
  readonly take?: number;
112
114
  readonly skip?: number;
115
+ /** The per-root bounds (MODEL-FORMAT §10.4): rows of this relation
116
+ * per parent and serialised bytes per parent; crossing one is the
117
+ * store's `JD2073`, never a truncated graph. `Infinity` spells the
118
+ * unbounded case (emitted as `null`). */
119
+ readonly maxRows?: number;
120
+ readonly maxBytes?: number;
113
121
  readonly include?: Includes<E, M>;
114
122
  };
115
123
 
@@ -118,9 +126,35 @@ export type Includes<E extends MetaMap<E>, M extends EntityMeta> = {
118
126
  readonly [K in keyof M['relations'] & string]?: IncludeSpec<E, TargetMeta<E, M, K>>;
119
127
  };
120
128
 
129
+ /** The order-key VALUE types a graph declared, in order: `orderBy` starts
130
+ * the tuple, every `thenBy` appends to it. */
131
+ export type OrderOf<S> = S extends { order: infer O extends readonly unknown[] } ? O : [];
132
+ type WithOrder<S, O extends readonly unknown[]> = Omit<S, 'order'> & { order: O };
133
+
134
+ /** The continuation a page over this graph emits, and `after()` takes:
135
+ * the declared order-key values as a tuple whose shape follows
136
+ * `orderBy`/`thenBy` — a two-key ordering needs a two-value `keys` —
137
+ * plus the row's primary key, the tie-breaker the store appends, and
138
+ * the ordering's identity. Unsigned and structural (MODEL-FORMAT §10.5):
139
+ * signing, scoping and expiry are the host's. */
140
+ export type Continuation<S, M extends EntityMeta> = LoadContinuation & {
141
+ readonly keys: OrderOf<S>;
142
+ readonly key: M['key'];
143
+ };
144
+
145
+ /** A page's options over this graph (the store's `PageOptions`, the
146
+ * continuation typed by the declared ordering). */
147
+ export interface GraphPageOptions<S, M extends EntityMeta> extends EntityCursorOptions {
148
+ limit?: number;
149
+ after?: Continuation<S, M>;
150
+ maxBytes?: number;
151
+ consistency?: 'live' | 'snapshot';
152
+ }
153
+
121
154
  /** The graph over one entity set: an immutable builder of the `load`
122
- * spec, typed by what it included. `S` accumulates the include
123
- * specification `Loaded<>` reads. */
155
+ * spec, typed by what it included and by what it ordered by. `S`
156
+ * accumulates the include specification `Loaded<>` reads and the
157
+ * `order` tuple `after()`/`page()` are typed by. */
124
158
  export interface Graph<E extends MetaMap<E>, M extends EntityMeta, S> {
125
159
  /** Include one more relation member. (`NoInfer` keeps the spec's
126
160
  * callbacks contextually typed while `I` is inferred from the literal —
@@ -130,14 +164,24 @@ export interface Graph<E extends MetaMap<E>, M extends EntityMeta, S> {
130
164
  ): Graph<E, M, S & { include: { [P in K]: I } }>;
131
165
  /** Filter the root rows; consecutive calls conjoin. */
132
166
  where(predicate: (it: Expr<M['doc']>) => BoolExpr | boolean): Graph<E, M, S>;
133
- orderBy(key: (it: Expr<M['doc']>) => unknown, options?: OrderOptions): Graph<E, M, S>;
134
- orderByDescending(key: (it: Expr<M['doc']>) => unknown, options?: OrderOptions): Graph<E, M, S>;
135
- thenBy(key: (it: Expr<M['doc']>) => unknown, options?: OrderOptions): Graph<E, M, S>;
136
- thenByDescending(key: (it: Expr<M['doc']>) => unknown, options?: OrderOptions): Graph<E, M, S>;
167
+ /** Start the ordering: the key's value type opens the `order` tuple. */
168
+ orderBy<V>(key: (it: Expr<M['doc']>) => ExprBase<V>, options?: OrderOptions): Graph<E, M, WithOrder<S, [V]>>;
169
+ orderByDescending<V>(key: (it: Expr<M['doc']>) => ExprBase<V>, options?: OrderOptions): Graph<E, M, WithOrder<S, [V]>>;
170
+ /** Extend the ordering: the key's value type is appended to the tuple. */
171
+ thenBy<V>(key: (it: Expr<M['doc']>) => ExprBase<V>, options?: OrderOptions): Graph<E, M, WithOrder<S, [...OrderOf<S>, V]>>;
172
+ thenByDescending<V>(key: (it: Expr<M['doc']>) => ExprBase<V>, options?: OrderOptions): Graph<E, M, WithOrder<S, [...OrderOf<S>, V]>>;
137
173
  take(count: number): Graph<E, M, S>;
138
174
  skip(count: number): Graph<E, M, S>;
139
- /** The keyset cursor (§10.5): the key of the last row of the previous page. */
140
- after(cursor: M['key']): Graph<E, M, S>;
175
+ /** Resume after a continuation (§10.5): the value a `page()` over this
176
+ * ordering emitted — its `keys` tuple follows the declared ordering,
177
+ * its `key` is the row's primary key. A bare key is not a continuation. */
178
+ after(cursor: Continuation<S, M>): Graph<E, M, S>;
179
+ /** One bounded page over the composite keyset: `{ items, continuation,
180
+ * hasMore, snapshot }`, never more than `limit` roots or `maxBytes`
181
+ * serialised bytes; `snapshot` is true only over an immutable ordering
182
+ * (the primary key), and `consistency: 'snapshot'` over any other is
183
+ * the store's `JD0036`. Untracked unless `tracking: true`. */
184
+ page(options?: GraphPageOptions<S, M>): Promise<Page<Loaded<E, M, S>, Continuation<S, M>>>;
141
185
  /** The include depth bound (§10.4, default 3). */
142
186
  maxDepth(depth: number): Graph<E, M, S>;
143
187
  /** The same graph, loaded without registering snapshots. */
@@ -148,6 +192,10 @@ export interface Graph<E extends MetaMap<E>, M extends EntityMeta, S> {
148
192
  toJSON(): TypedLoadSpec<E, M>;
149
193
  /** `load(spec)`: the store's one statement, typed by the includes. */
150
194
  toArray(): Promise<Array<Loaded<E, M, S>>>;
195
+ /** `loadCursor(spec, options)`: one root graph per pull, its includes
196
+ * attached and bounded, from the same one statement; `return()`
197
+ * releases it. Untracked unless `tracking: true` is spelled per call. */
198
+ cursor(options?: EntityCursorOptions): QueryCursor<Loaded<E, M, S>>;
151
199
  /** `explainLoad(spec)`: the SQL, the includes, the pagination strategy. */
152
200
  explain(): LoadExplanation;
153
201
  }
@@ -161,6 +209,9 @@ export type EntityHandle<E extends MetaMap<E>, M extends EntityMeta> =
161
209
  include<K extends keyof M['relations'] & string, const I extends IncludeSpec<E, TargetMeta<E, M, K>> = true>(
162
210
  pick: (u: RelationPicker<M>) => Picked<K>, spec?: I | NoInfer<IncludeSpec<E, TargetMeta<E, M, K>>>,
163
211
  ): Graph<E, M, { include: { [P in K]: I } }>;
212
+ /** Open a graph with nothing included: the root clauses, the keyset
213
+ * continuation and the page over the rows alone. */
214
+ graph(): Graph<E, M, {}>;
164
215
  /** A live query over a chain of this set (the whole set when none
165
216
  * is given), through the store's entity-root registration. */
166
217
  live<T = M['doc']>(source?: AsyncSequence<T, any> | object, options?: LiveOptions): Promise<TypedLiveQuery<T>>;
@@ -175,6 +226,124 @@ export interface EntityClientMembers {
175
226
  live<T>(source: AsyncSequence<T, any> | object, options?: LiveOptions): Promise<TypedLiveQuery<T>>;
176
227
  }
177
228
 
229
+ // ————— the ledger —————
230
+
231
+ /** The record `createDbLedger` keeps per `(op, scope, key)`: exactly
232
+ * `idempotencyLedgerModel`'s (`@jarenjs/contract/ledger`), the
233
+ * `generation` the fence verifies included. */
234
+ export interface DbLedgerRecord {
235
+ id: string;
236
+ generation: string;
237
+ op: string;
238
+ scope: string;
239
+ key: string;
240
+ hash: string;
241
+ status: 'started' | 'committed' | 'failed';
242
+ response: unknown;
243
+ retryable: boolean | null;
244
+ createdAt: number;
245
+ updatedAt: number;
246
+ expiresAt: number;
247
+ }
248
+
249
+ /** The ref a `new` claim hands back: the record's id and the generation
250
+ * the claim minted — portable across processes, stale once the key
251
+ * expires or is reclaimed. */
252
+ export interface DbLedgerRef {
253
+ readonly id: string;
254
+ readonly generation: string;
255
+ }
256
+
257
+ export type DbClaimResult =
258
+ | { state: 'new'; ref: DbLedgerRef }
259
+ | { state: 'replay'; response: unknown }
260
+ | { state: 'in-progress' }
261
+ | { state: 'mismatch' };
262
+
263
+ export interface DbLedgerOptions {
264
+ /** The declared collection; `'ledger'` (the model's) by default. */
265
+ collection?: string;
266
+ /** The retention of a key; 86,400,000 ms by default. */
267
+ ttlMs?: number;
268
+ /** The host's runtime record: its `now` is the clock, its `uuid`
269
+ * mints every generation. */
270
+ runtime?: Partial<Runtime>;
271
+ /** The clock; wins over the runtime's. Given neither, the ledger
272
+ * follows the instants the binding passes. */
273
+ now?: () => number;
274
+ }
275
+
276
+ /** The ledger the http binding calls, over the store: structurally the
277
+ * contract package's `Ledger`, every method asynchronous. A `commit` or
278
+ * `fail` whose ref settles no started record rejects `JL2007`. */
279
+ export interface DbLedger {
280
+ claim(claim: { op: string; scope: string; key: string; hash: string; now?: number }): Promise<DbClaimResult>;
281
+ commit(ref: unknown, response: unknown, now?: number): Promise<void>;
282
+ fail(ref: unknown, retryable: boolean, response?: unknown, now?: number): Promise<void>;
283
+ lookup(key: { op: string; scope: string; key: string; now?: number }): Promise<DbLedgerRecord | null>;
284
+ /** Drop every expired record; answers how many. */
285
+ sweep(now?: number): Promise<number>;
286
+ }
287
+
288
+ /** What `createDbLedger` needs of a client: the declared collections
289
+ * and a transaction — the root client (claims take the write lock up
290
+ * front, `mode: 'immediate'`) or the one a transaction callback
291
+ * received (claims and settlements nest in that transaction). */
292
+ export type LedgerClient =
293
+ | Pick<Client<any, any>, 'collections' | 'transaction' | 'close'>
294
+ | Pick<TransactionClientOf<any, any>, 'collections' | 'transaction'>;
295
+
296
+ /** The contract ledger over a declared collection of the client's
297
+ * store — no import of `@jarenjs/contract`, no driver, the client's
298
+ * own surface only (DB-CLIENT.md §2.6). A `TypeError` names a client
299
+ * that is not one, a collection the model does not declare, a bad
300
+ * `ttlMs` or `now`. */
301
+ export function createDbLedger(client: LedgerClient, options?: DbLedgerOptions): DbLedger;
302
+
303
+ /** How a transaction relates to the client's unit of work: `'own'` (the
304
+ * default) gives the callback a tracker of its own, so two handlers on
305
+ * one client hold two records for the same entity key; `'shared'` opts
306
+ * back into the client's, for a caller who staged changes outside the
307
+ * transaction and means to save them inside it. */
308
+ export interface TransactionOptions {
309
+ readonly unitOfWork?: 'own' | 'shared';
310
+ /** `'immediate'` takes the write lock up front (`BEGIN IMMEDIATE`),
311
+ * so a body that reads before it writes never meets the read→write
312
+ * upgrade busy the handler cannot retry; `'deferred'` (the default)
313
+ * is the savepoint as always. A nested `tx.transaction()` is a
314
+ * savepoint whichever mode the root chose. */
315
+ readonly mode?: 'deferred' | 'immediate';
316
+ /** Abandons the call while it is still QUEUED: the callback never
317
+ * runs and no statement is issued (`JD2064`). */
318
+ readonly signal?: AbortSignal;
319
+ }
320
+
321
+ /**
322
+ * The client a transaction callback receives: the same handles, the
323
+ * same inference, over the store that is INSIDE the transaction.
324
+ *
325
+ * `tx.entities.X` and `tx.collections.Y` run as the transaction's owner
326
+ * and `tx.transaction(...)` nests, while the outer client's handles are
327
+ * an unrelated caller — one awaited from in here waits for the commit it
328
+ * is part of, which `JD0012` names rather than hangs on.
329
+ */
330
+ export type TransactionClientOf<E extends MetaMap<E>, C = Record<string, unknown>> = {
331
+ /** The scope-bound store — the escape hatch, still inside. It carries
332
+ * no `close`: a transaction never owns the connection's lifetime. */
333
+ readonly store: TransactionStore;
334
+ readonly capabilities: StoreCapabilities;
335
+ readonly entities: { readonly [K in keyof E & string]: EntityHandle<E, E[K]> };
336
+ readonly collections: { readonly [K in keyof C & string]: CollectionHandle<C[K]> };
337
+ /** Nest through this transaction's savepoint. */
338
+ transaction<R>(fn: (tx: TransactionClientOf<E, C>) => R | Promise<R>): Promise<Awaited<R>>;
339
+ /** Named partial rollback (MODEL-FORMAT §5.2) — the store's
340
+ * `tx.savepoints`, forwarded unchanged: create, roll back to and
341
+ * release a checkpoint by label without a sentinel exception. Only a
342
+ * live transaction has it; the root client deliberately has no twin,
343
+ * and a handle kept past its callback is `JD2070`. */
344
+ readonly savepoints: SavepointController;
345
+ } & ([keyof E] extends [never] ? {} : EntityClientMembers);
346
+
178
347
  /** The client: one frozen record of handles per declared name, the
179
348
  * store beneath it, and the pass-throughs. */
180
349
  export type Client<E extends MetaMap<E>, C = Record<string, unknown>> = {
@@ -183,6 +352,15 @@ export type Client<E extends MetaMap<E>, C = Record<string, unknown>> = {
183
352
  readonly capabilities: StoreCapabilities;
184
353
  readonly entities: { readonly [K in keyof E & string]: EntityHandle<E, E[K]> };
185
354
  readonly collections: { readonly [K in keyof C & string]: CollectionHandle<C[K]> };
186
- transaction<R>(fn: (store: Store) => R | Promise<R>): Promise<Awaited<R>>;
355
+ /** A transaction, with a typed client of its own. */
356
+ transaction<R>(fn: (tx: TransactionClientOf<E, C>) => R | Promise<R>,
357
+ options?: TransactionOptions): Promise<Awaited<R>>;
187
358
  close(options?: { graceMs?: number }): Promise<void>;
188
359
  } & ([keyof E] extends [never] ? {} : EntityClientMembers);
360
+
361
+ /** Author a logical envelope using the DB's canonical format validator. */
362
+ export function defineReplication(header: Omit<import('@jarenjs/db').ReplicationEnvelope, '$replication' | 'operations'>): {
363
+ change(table: string, key: string, before: Record<string, unknown> | null, after: Record<string, unknown> | null): ReturnType<typeof defineReplication>;
364
+ toDocument(): import('@jarenjs/db').ReplicationEnvelope;
365
+ toJSON(): import('@jarenjs/db').ReplicationEnvelope;
366
+ };
package/types/index.d.ts CHANGED
@@ -475,6 +475,13 @@ export interface Provider<T = unknown> {
475
475
  export interface AsyncProvider<T = unknown> {
476
476
  readonly __item?: T;
477
477
  execute(document: unknown, options: { externals: Record<string, unknown> }): unknown | Promise<unknown>;
478
+ /** The cursor protocol, optional: the same document as an item
479
+ * cursor, one item per pull, `return()` releasing whatever it holds.
480
+ * A `for await` over the chain hands its pushed document here when
481
+ * the provider offers it, so iteration never materialises what the
482
+ * provider can stream; a provider without it is run whole. */
483
+ cursor?(document: unknown, options: { externals: Record<string, unknown> }):
484
+ AsyncIterator<unknown> & { return(): Promise<unknown> };
478
485
  readonly root?: string;
479
486
  readonly roots?: readonly string[];
480
487
  readonly scope?: unknown;
@@ -657,6 +664,13 @@ export interface MapAsyncOptions {
657
664
 
658
665
  export interface AsyncExplanation {
659
666
  barriers: { operator: string, reason: string }[];
667
+ /** What THIS surface does with the item stream: one item at a time, or
668
+ * a buffer at its first local barrier. Over a provider the pushed
669
+ * document's own class — a set residual, an external the database
670
+ * cannot bind — is the provider's `explain(document, { externals:
671
+ * bindings })` to report, and its cursor carries the same answer. */
672
+ streaming: 'row' | 'buffered';
673
+ barrier: { construct: string, reason: string } | null;
660
674
  /** The relation hops the chain's callbacks navigated (as `Explanation`). */
661
675
  hops: Hop[];
662
676
  /** The values `params()` bound, by name (as `Explanation`). */
@@ -770,6 +784,57 @@ export function fromAsync<T>(
770
784
  options?: LinqOptions,
771
785
  ): AsyncSequence<T, {}>;
772
786
 
787
+ /** One side of a federation, as `explain()` reports it (§12.1). */
788
+ export interface FederatedSide {
789
+ readonly source: string;
790
+ readonly root: string;
791
+ readonly estimatedRows: number | null;
792
+ /** The join key on this side, as the document spells it. */
793
+ readonly key: string;
794
+ /** The document this side's own source is asked. */
795
+ readonly document: unknown;
796
+ /** Whether this side is pulled row by row, or answered whole. */
797
+ readonly streaming: 'row' | 'buffered';
798
+ }
799
+
800
+ /** What a federated document will do, without doing any of it (§12.1). */
801
+ export interface FederationPlan {
802
+ readonly strategy: 'hash';
803
+ readonly budget: { readonly maxRows: number; readonly maxBytes: number };
804
+ readonly build: FederatedSide;
805
+ readonly probe: FederatedSide;
806
+ /** The join itself is the engine's, over the two reduced sides. */
807
+ readonly resident: { readonly document: unknown };
808
+ }
809
+
810
+ /** One named source of a federation: an ordinary provider source whose
811
+ * root is `$.<name>[*]`, sharing one scope with its siblings — which is
812
+ * what admits the join the federation then executes. */
813
+ export interface FederatedSource<T = unknown> extends AsyncProvider<T> {
814
+ explain(document: unknown): FederationPlan;
815
+ readonly root: string;
816
+ }
817
+
818
+ /** The explicit cross-source boundary (§12.1).
819
+ *
820
+ * A query document reads one input, and an ordinary join across two
821
+ * unrelated sources stays `JL0005`. `federate()` is the one way to opt
822
+ * out of that, by naming the sources and the bounds together: each
823
+ * side's own filters and projection run at its source, the smaller side
824
+ * fills a bounded hash table, the other is probed against it, and the
825
+ * caller's own document decides over the two reduced sets. A side that
826
+ * reaches `maxRows` or `maxBytes` raises `JL2008` at the row that would
827
+ * have broken the bound. */
828
+ export function federate(spec: {
829
+ sources: Record<string, AsyncProvider | { provider: AsyncProvider; estimatedRows?: number }>;
830
+ maxRows: number;
831
+ maxBytes: number;
832
+ strategy?: 'hash';
833
+ }): {
834
+ source<T = unknown>(name: string): FederatedSource<T>;
835
+ readonly names: readonly string[];
836
+ };
837
+
773
838
  /** The push→pull adapter for feed/end readers (§12). */
774
839
  export function createPushQueue<T = unknown>(options?: { highWaterMark?: number }): {
775
840
  feed(value: T): boolean;
package/types/model.d.ts CHANGED
@@ -328,11 +328,44 @@ export type IndexPath<D> = ((doc: Expr<D>) => unknown) | string | readonly (((do
328
328
  * a composite, or a JSONPath string; default name `by_<segments>`. */
329
329
  export function index<D = unknown>(path: IndexPath<D>, options?: IndexOptions): IndexSpec<D>;
330
330
 
331
+ /** One node of an index expression: a member (a lambda or a JSONPath
332
+ * string), a JSON scalar, or a call to a function the HOST declares. */
333
+ export type ExpressionNode<D = unknown> =
334
+ | ((doc: Expr<D>) => unknown)
335
+ | string
336
+ | number
337
+ | boolean
338
+ | { member: ((doc: Expr<D>) => unknown) | string }
339
+ | { value: string | number | boolean }
340
+ | { call: string; args?: readonly ExpressionNode<D>[] };
341
+
342
+ /** One index over a COMPUTED value. */
343
+ export interface ExpressionIndexSpec<D = unknown> {
344
+ readonly __doc?: D;
345
+ readonly name: string;
346
+ readonly expression: unknown;
347
+ readonly unique?: boolean;
348
+ }
349
+
350
+ /**
351
+ * An index over a computed value: a closed expression over declared
352
+ * members, JSON scalars and functions the host declares deterministic.
353
+ *
354
+ * The function is resolved where the declarations are —
355
+ * `openStore({ expressions })` — so a name this pen has never heard of
356
+ * is not an error here; a wrong arity and a missing declaration are
357
+ * `JD0004` at open, before any DDL. Default name `by_<call>_<members>`.
358
+ */
359
+ export function expressionIndex<D = unknown>(
360
+ expression: ExpressionNode<D>,
361
+ options?: { name?: string; unique?: boolean },
362
+ ): ExpressionIndexSpec<D>;
363
+
331
364
  export interface CollectionOptions<D> {
332
365
  /** An RFC 6901 pointer, a captured member path (`(d) => d.id` → `/id`), or `null` (the store allocates). */
333
366
  key?: string | ((doc: Expr<D>) => unknown) | null;
334
367
  identity?: 'caller' | 'uuid' | 'integer';
335
- indexes?: readonly IndexSpec<D>[];
368
+ indexes?: readonly (IndexSpec<D> | ExpressionIndexSpec<D>)[];
336
369
  renamedFrom?: string;
337
370
  }
338
371