@lunora/server 1.0.0-alpha.24 → 1.0.0-alpha.26

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/dist/index.d.mts CHANGED
@@ -558,6 +558,7 @@ interface HttpRouteHandlerOptions<SearchParams extends ArgsValidator, Body exten
558
558
  * handler receives. There is no parsed `body` — streams are typically GET, and
559
559
  * the raw `request` is exposed if a handler needs to read the body itself.
560
560
  * `signal` is tripped when the client disconnects.
561
+ * @experimental Part of the HTTP-SSE stream surface; reconnect/POST-body design questions are still open.
561
562
  */
562
563
  interface HttpStreamHandlerOptions<SearchParams extends ArgsValidator, Params extends ArgsValidator> {
563
564
  ctx: HttpActionCtx;
@@ -604,6 +605,7 @@ interface HttpRouteBuilder<SearchParams extends ArgsValidator, Body extends Args
604
605
  * throw, an `event: error` frame is written with `{code, message}` before
605
606
  * the stream closes. The chunks are JSON-encoded; `R` is inferred from the
606
607
  * handler's yielded type.
608
+ * @experimental Reconnect/POST-body/wire-fidelity design questions are still open, so the shape may change.
607
609
  */
608
610
  stream: <R>(handler: (options: HttpStreamHandlerOptions<SearchParams, Params>) => AsyncGenerator<R, void, void> | AsyncIterable<R>) => LunoraRouteHandler;
609
611
  /**
@@ -1141,10 +1143,26 @@ interface DefinePluginOptions<TExtension extends Record<string, TableDefinition>
1141
1143
  middleware?: Middleware<TContextIn, TContextOut>;
1142
1144
  }
1143
1145
  /**
1146
+ * Call signatures for {@link definePlugin}. When `extension` is supplied the
1147
+ * returned plugin's `extension` is typed as PRESENT (not `?`), so the
1148
+ * canonical install pattern `defineSchema(...).extend(plugin.extension)`
1149
+ * typechecks without a non-null assertion — the shape every scaffold template
1150
+ * ships. The bare-options signature keeps `extension` optional for plugins
1151
+ * that carry only middleware.
1152
+ */
1153
+ interface DefinePluginFunction {
1154
+ <TExtension extends Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn>(key: string, options: DefinePluginOptions<TExtension, TContextIn, TContextOut> & {
1155
+ extension: SchemaExtension<TExtension>;
1156
+ }): Plugin<TExtension, TContextIn, TContextOut> & {
1157
+ readonly extension: SchemaExtension<TExtension>;
1158
+ };
1159
+ <TExtension extends Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn>(key: string, options: DefinePluginOptions<TExtension, TContextIn, TContextOut>): Plugin<TExtension, TContextIn, TContextOut>;
1160
+ }
1161
+ /**
1144
1162
  * Package a schema extension + middleware as a reusable plugin. Either
1145
1163
  * field is optional — `definePlugin("foo", {})` is valid but degenerate.
1146
1164
  */
1147
- declare const definePlugin: <TExtension extends Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn>(key: string, options: DefinePluginOptions<TExtension, TContextIn, TContextOut>) => Plugin<TExtension, TContextIn, TContextOut>;
1165
+ declare const definePlugin: DefinePluginFunction;
1148
1166
  /**
1149
1167
  * Bundle of registered functions a {@link Component} ships. Keys are the
1150
1168
  * function's local name (e.g. `check`, `reset`); the registered function
package/dist/index.d.ts CHANGED
@@ -558,6 +558,7 @@ interface HttpRouteHandlerOptions<SearchParams extends ArgsValidator, Body exten
558
558
  * handler receives. There is no parsed `body` — streams are typically GET, and
559
559
  * the raw `request` is exposed if a handler needs to read the body itself.
560
560
  * `signal` is tripped when the client disconnects.
561
+ * @experimental Part of the HTTP-SSE stream surface; reconnect/POST-body design questions are still open.
561
562
  */
562
563
  interface HttpStreamHandlerOptions<SearchParams extends ArgsValidator, Params extends ArgsValidator> {
563
564
  ctx: HttpActionCtx;
@@ -604,6 +605,7 @@ interface HttpRouteBuilder<SearchParams extends ArgsValidator, Body extends Args
604
605
  * throw, an `event: error` frame is written with `{code, message}` before
605
606
  * the stream closes. The chunks are JSON-encoded; `R` is inferred from the
606
607
  * handler's yielded type.
608
+ * @experimental Reconnect/POST-body/wire-fidelity design questions are still open, so the shape may change.
607
609
  */
608
610
  stream: <R>(handler: (options: HttpStreamHandlerOptions<SearchParams, Params>) => AsyncGenerator<R, void, void> | AsyncIterable<R>) => LunoraRouteHandler;
609
611
  /**
@@ -1141,10 +1143,26 @@ interface DefinePluginOptions<TExtension extends Record<string, TableDefinition>
1141
1143
  middleware?: Middleware<TContextIn, TContextOut>;
1142
1144
  }
1143
1145
  /**
1146
+ * Call signatures for {@link definePlugin}. When `extension` is supplied the
1147
+ * returned plugin's `extension` is typed as PRESENT (not `?`), so the
1148
+ * canonical install pattern `defineSchema(...).extend(plugin.extension)`
1149
+ * typechecks without a non-null assertion — the shape every scaffold template
1150
+ * ships. The bare-options signature keeps `extension` optional for plugins
1151
+ * that carry only middleware.
1152
+ */
1153
+ interface DefinePluginFunction {
1154
+ <TExtension extends Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn>(key: string, options: DefinePluginOptions<TExtension, TContextIn, TContextOut> & {
1155
+ extension: SchemaExtension<TExtension>;
1156
+ }): Plugin<TExtension, TContextIn, TContextOut> & {
1157
+ readonly extension: SchemaExtension<TExtension>;
1158
+ };
1159
+ <TExtension extends Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn>(key: string, options: DefinePluginOptions<TExtension, TContextIn, TContextOut>): Plugin<TExtension, TContextIn, TContextOut>;
1160
+ }
1161
+ /**
1144
1162
  * Package a schema extension + middleware as a reusable plugin. Either
1145
1163
  * field is optional — `definePlugin("foo", {})` is valid but degenerate.
1146
1164
  */
1147
- declare const definePlugin: <TExtension extends Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn>(key: string, options: DefinePluginOptions<TExtension, TContextIn, TContextOut>) => Plugin<TExtension, TContextIn, TContextOut>;
1165
+ declare const definePlugin: DefinePluginFunction;
1148
1166
  /**
1149
1167
  * Bundle of registered functions a {@link Component} ships. Keys are the
1150
1168
  * function's local name (e.g. `check`, `reset`); the registered function
package/dist/index.mjs CHANGED
@@ -9,10 +9,10 @@ export { defineIdentity } from './packem_shared/defineIdentity-DiX4zM9x.mjs';
9
9
  export { onConnect, onDisconnect } from './packem_shared/onConnect-CIPXKPyw.mjs';
10
10
  export { defineMigration } from './packem_shared/defineMigration-Hx01yIht.mjs';
11
11
  export { defineMutator } from './packem_shared/defineMutator-EIXAWhs9.mjs';
12
- export { composePluginMiddleware, defineComponent, definePlugin, defineSchemaExtension, installPlugins, mergeSchemaExtension } from './packem_shared/composePluginMiddleware-CcTj1_v2.mjs';
13
- export { PRESENCE_DEFAULT_TTL_MS, PRESENCE_TABLE, definePresence, presenceExtension } from './packem_shared/PRESENCE_DEFAULT_TTL_MS-BAD0QPAg.mjs';
12
+ export { composePluginMiddleware, defineComponent, definePlugin, defineSchemaExtension, installPlugins, mergeSchemaExtension } from './packem_shared/composePluginMiddleware-z62dttBo.mjs';
13
+ export { PRESENCE_DEFAULT_TTL_MS, PRESENCE_TABLE, definePresence, presenceExtension } from './packem_shared/PRESENCE_DEFAULT_TTL_MS-Cv1UPaJj.mjs';
14
14
  export { protectPublic } from './packem_shared/protectPublic-BlcGpiRc.mjs';
15
- export { defineAggregateIndex, defineRankIndex, defineSchema, defineTable, defineVectorIndex } from './packem_shared/defineAggregateIndex-znq4ygKQ.mjs';
15
+ export { defineAggregateIndex, defineRankIndex, defineSchema, defineTable, defineVectorIndex } from './packem_shared/defineAggregateIndex-DTULzh09.mjs';
16
16
  export { defineShape } from './packem_shared/defineShape-C5scNOrf.mjs';
17
17
  export { anyApi } from './types.mjs';
18
18
  export { cronJobs } from '@lunora/scheduler';
@@ -2,8 +2,8 @@ import { v } from '@lunora/values';
2
2
  import { initLunora } from './initLunora-CmMVk4i7.mjs';
3
3
  import { LunoraError } from './LunoraError-WbxmrpxR.mjs';
4
4
  import { onDisconnect } from './onConnect-CIPXKPyw.mjs';
5
- import { defineSchemaExtension, defineComponent } from './composePluginMiddleware-CcTj1_v2.mjs';
6
- import { defineTable } from './defineAggregateIndex-znq4ygKQ.mjs';
5
+ import { defineSchemaExtension, defineComponent } from './composePluginMiddleware-z62dttBo.mjs';
6
+ import { defineTable } from './defineAggregateIndex-DTULzh09.mjs';
7
7
 
8
8
  const DEFAULT_TTL_MS = 3e4;
9
9
  const MAX_DATA_BYTES = 4096;
@@ -28,7 +28,7 @@ const defineSchemaExtension = (key, options) => {
28
28
  ...options.vectorIndexes ? { vectorIndexes: options.vectorIndexes } : {}
29
29
  };
30
30
  };
31
- const definePlugin = (key, options) => {
31
+ const definePlugin = ((key, options) => {
32
32
  if (!key) {
33
33
  throw new LunoraError$1("INTERNAL", "definePlugin: `key` is required and must be a non-empty string");
34
34
  }
@@ -40,7 +40,7 @@ const definePlugin = (key, options) => {
40
40
  ...options.extension ? { extension: options.extension } : {},
41
41
  ...options.middleware ? { middleware: options.middleware } : {}
42
42
  };
43
- };
43
+ });
44
44
  const defineComponent = (key, options) => {
45
45
  const plugin = definePlugin(key, {
46
46
  ...options.extension ? { extension: options.extension } : {},
@@ -1,6 +1,6 @@
1
1
  import { LunoraError as LunoraError$1 } from '@lunora/errors';
2
2
  import { isOrWrapsFromValidator, v } from '@lunora/values';
3
- import { mergeSchemaExtension } from './composePluginMiddleware-CcTj1_v2.mjs';
3
+ import { mergeSchemaExtension } from './composePluginMiddleware-z62dttBo.mjs';
4
4
 
5
5
  const relationBuilder = {
6
6
  many: (table, options) => {
@@ -259,6 +259,49 @@ const attachStandaloneIndexes = (tables, aggregateIndexes, rankIndexes) => {
259
259
  table.rankIndexes.push(index);
260
260
  }
261
261
  };
262
+ const strayIncrementalKnob = (source) => {
263
+ if (source.cursor) {
264
+ return "cursor";
265
+ }
266
+ if (source.reconcileEveryMs !== void 0) {
267
+ return "reconcileEveryMs";
268
+ }
269
+ if (source.softDeleteColumn !== void 0) {
270
+ return "softDeleteColumn";
271
+ }
272
+ return void 0;
273
+ };
274
+ const validateExternalSourceMode = (name, source) => {
275
+ const isIncremental = source.mode === "incremental";
276
+ if (source.mode !== void 0 && source.mode !== "full-pull" && !isIncremental) {
277
+ throw new LunoraError$1(
278
+ "INTERNAL",
279
+ `defineSchema: table "${name}" uses \`mode: ${JSON.stringify(source.mode)}\` — supported modes are "full-pull" (default) and "incremental".`
280
+ );
281
+ }
282
+ if (!isIncremental) {
283
+ const strayKnob = strayIncrementalKnob(source);
284
+ if (strayKnob) {
285
+ throw new LunoraError$1(
286
+ "INTERNAL",
287
+ `defineSchema: table "${name}" sets \`${strayKnob}\` but is not \`mode: "incremental"\` — that knob only applies to incremental ingest. Set \`mode: "incremental"\` or remove \`${strayKnob}\`.`
288
+ );
289
+ }
290
+ return;
291
+ }
292
+ if (!source.cursor?.column || !source.cursor.query) {
293
+ throw new LunoraError$1(
294
+ "INTERNAL",
295
+ `defineSchema: table "${name}" is \`mode: "incremental"\` but has no \`cursor\` — add \`cursor: { column: "updated_at", query: "… WHERE … > $N" }\` binding the watermark as the trailing parameter.`
296
+ );
297
+ }
298
+ if (source.reconcileEveryMs === void 0 && source.softDeleteColumn === void 0) {
299
+ throw new LunoraError$1(
300
+ "INTERNAL",
301
+ `defineSchema: table "${name}" is \`mode: "incremental"\` with no delete-visibility path — an incremental pull never sees upstream deletes, so it would accumulate phantom rows. Add \`reconcileEveryMs\` (a periodic full-pull sweep) or \`softDeleteColumn\` (an upstream tombstone column).`
302
+ );
303
+ }
304
+ };
262
305
  const validateExternalSources = (tables) => {
263
306
  for (const [name, table] of Object.entries(tables)) {
264
307
  const source = table.externalSource;
@@ -277,12 +320,7 @@ const validateExternalSources = (tables) => {
277
320
  `defineSchema: sourced + .shardBy() table "${name}" needs a \`tenantBy\` mapper — without it every tenant's DO would run the same unscoped query and replicate the whole multitenant table (a cross-tenant leak). Add \`tenantBy: (shardKey) => [shardKey]\` binding the shard key into the query's parameters.`
278
321
  );
279
322
  }
280
- if (source.mode === "incremental") {
281
- throw new LunoraError$1(
282
- "INTERNAL",
283
- `defineSchema: table "${name}" uses \`mode: "incremental"\`, which is not yet implemented — only "full-pull" (the default) is supported. Remove \`mode\` or set it to "full-pull".`
284
- );
285
- }
323
+ validateExternalSourceMode(name, source);
286
324
  }
287
325
  };
288
326
  const defineSchema = (tables, vectorIndexes = {}, aggregateIndexes = {}, rankIndexes = {}) => {
package/dist/types.d.mts CHANGED
@@ -28,13 +28,38 @@ type ExternalSourceRefresh = "manual" | {
28
28
  everyMs: number;
29
29
  };
30
30
  /**
31
- * Delete-detection mode for external-source ingest. `"full-pull"` (default) reads
32
- * the whole tenant membership each tick and diffs it, so it observes upstream
33
- * deletes; `"incremental"` pulls only changed rows (cheap) and is blind to deletes
34
- * unless paired with a soft-delete column or a `reconcileEveryMs` full-pull sweep.
31
+ * Delete-detection mode for external-source ingest (plan 077 / 136).
32
+ *
33
+ * `"full-pull"` (the default) reads the **whole** tenant membership each tick and
34
+ * diffs it, so it observes upstream deletes for free — but costs a full read per tick
35
+ * (the Phase-0 bench put the ceiling at ~10k rows).
36
+ *
37
+ * `"incremental"` pulls **only rows past a durable watermark** (`cursor`), cheap for
38
+ * large low-churn tables above the full-pull cap. Because an absent row then means
39
+ * "unchanged", not "deleted", incremental requires a delete-visibility path: either a
40
+ * `reconcileEveryMs` periodic full-pull sweep, or a `softDeleteColumn` whose
41
+ * tombstones the pull returns. `defineSchema` throws (and the
42
+ * `external_source_incremental_no_delete_path` advisor lint fails the build) when an
43
+ * incremental source declares neither.
35
44
  */
36
45
  type ExternalSourceMode = "full-pull" | "incremental";
37
46
  /**
47
+ * Incremental-ingest cursor (plan 136): the monotonic watermark column plus the
48
+ * watermark-parameterized pull query. `column` names the field in the pulled rows
49
+ * whose max becomes the next watermark (e.g. `"updated_at"`). `query` is a second
50
+ * SQL that returns only rows changed since the watermark — the watermark binds as
51
+ * the parameter AFTER `tenantBy`'s params (e.g. Postgres
52
+ * `... WHERE tenant_id = $1 AND updated_at >= $2 ORDER BY updated_at`). Prefer `>=`
53
+ * with the idempotent upsert apply so rows sharing the boundary timestamp are never
54
+ * skipped (re-pulling them is a no-op).
55
+ */
56
+ interface ExternalSourceCursor {
57
+ /** The monotonic watermark column in the pulled rows; its max advances the stored watermark. */
58
+ column: string;
59
+ /** The incremental pull SQL. `tenantBy`'s params bind first, then the watermark as the trailing param. */
60
+ query: string;
61
+ }
62
+ /**
38
63
  * Config for `.source(...)` (plan 077): declares a table as **materialized from an
39
64
  * external Postgres/MySQL behind Cloudflare Hyperdrive**, not written by user
40
65
  * mutations. A system-driven poll loop reads the tenant slice and lands it in the
@@ -48,19 +73,33 @@ interface ExternalSourceDefinition {
48
73
  binding: string;
49
74
  /** Project the materialized rows to these columns (passed to the membership diff). Omit ⇒ the full mapped document. */
50
75
  columns?: ReadonlyArray<string>;
76
+ /** **Required for `mode: "incremental"`**: the watermark column + watermark-parameterized pull query (plan 136). Rejected on a `"full-pull"` source. */
77
+ cursor?: ExternalSourceCursor;
51
78
  /** Column whose value becomes the Lunora `_id`. Defaults to `"id"`. */
52
79
  idColumn?: string;
53
80
  /** Transform an external row into the stored document body. Omit ⇒ every selected column except `idColumn` is copied. */
54
81
  map?: (row: Record<string, unknown>) => Record<string, unknown>;
55
- /** Delete-detection mode. Defaults to `"full-pull"`. */
82
+ /** Delete-detection mode. `"full-pull"` (the default) diffs the whole membership; `"incremental"` pulls past a `cursor` watermark. */
56
83
  mode?: ExternalSourceMode;
57
84
  /** The full tenant-membership query, with driver-native placeholders (`$1` / `?`). `tenantBy` binds its params. */
58
85
  query: string;
59
- /** `"incremental"` only: run a full-pull reconcile this often to garbage-collect tombstones the incremental path can't see. */
86
+ /**
87
+ * **Incremental delete-visibility (plan 136)**: run a full-pull sweep at most
88
+ * this often (millis) to GC upstream deletes an incremental slice can't see.
89
+ * One of `reconcileEveryMs` / `softDeleteColumn` is required for incremental;
90
+ * rejected on a `"full-pull"` source.
91
+ */
60
92
  reconcileEveryMs?: number;
61
93
  /** Poll cadence, or `"manual"`. Omit ⇒ the runtime's size-scaled default. */
62
94
  refresh?: ExternalSourceRefresh;
63
95
  /**
96
+ * **Incremental delete-visibility (plan 136)**: the upstream soft-delete
97
+ * tombstone column (e.g. `"deleted_at"`). When set, the incremental pull must
98
+ * return tombstoned rows and the ingest turns each into a local delete — an
99
+ * alternative to `reconcileEveryMs`. Rejected on a `"full-pull"` source.
100
+ */
101
+ softDeleteColumn?: string;
102
+ /**
64
103
  * **Mandatory under `.shardBy()`**: map this DO's shard key → the query's bound
65
104
  * params, so a tenant DO can only ever pull its own rows. An unscoped sourced +
66
105
  * sharded table replicates the whole multitenant table into every shard — the
@@ -1293,4 +1332,4 @@ interface ActionCtx {
1293
1332
  */
1294
1333
  type AnyApi = Record<string, Record<string, RegisteredFunction<ArgsValidator, unknown, FunctionKind>>>;
1295
1334
  declare const anyApi: AnyApi;
1296
- export { type ActionCtx, type AggregateIndexDefinition, type AggregateOp, type AnyApi, type ArgsValidator, type AuthState, type CachePurge, type DatabaseReader, type DatabaseWriter, type DurableObjectJurisdiction, type ExternalSourceDefinition, type ExternalSourceMode, type ExternalSourceRefresh, type FunctionKind, type FunctionVisibility, type GlobalBackend, type IndexDefinition, type IndexRangeBuilder, type InferArgs, type LifecycleEvent, type LifecycleEventKind, type LunoraLogger, type MutationCtx, type OnDeleteAction, type PaginationOptions, type PaginationResult, type QueryCtx, type RankIndexDefinition, type RankSortKey, type ReadOnlyStorage, type RegisteredAction, type RegisteredFunction, type RegisteredLifecycleHook, type RegisteredMutation, type RegisteredQuery, type RegisteredStream, type RelationDefinition, type ScheduledFunctionDoc, type ScheduledJob, type Scheduler, type Schema, type SearchFilterBuilder, type SearchIndexDefinition, type Secrets, type SecretsStoreSecretLike, type ShardMode, type Storage, type StorageMetadata, type SystemDatabaseReader, type SystemDoc, type SystemQuery, type SystemTableName, type TableDefinition, type TableReader, type TableVectorIndex, type TriggerAggregateOptions, type TriggerBuilder, type TriggerCtx, type TriggerDatabase, type TriggerDefinition, type TriggerDeleteEvent, type TriggerEvent, type TriggerGroupByEntry, type TriggerGroupByOptions, type TriggerHandler, type TriggerInsertEvent, type TriggerOp, type TriggerQueryArgs, type TriggerQueryPage, type TriggerRankOptions, type TriggerRankPageOptions, type TriggerRankResult, type TriggerRow, type TriggerTiming, type TriggerUpdateEvent, type VectorEmbedder, type VectorIndexDefinition, type VectorMatch, type VectorMatches, type VectorMetric, type VectorQueryInput, type VectorRecord, type VectorSearch, type VectorSearchReader, type VectorUpsertInput, type WorkflowCreateOptions, type WorkflowHandle, type WorkflowInstance, type WorkflowInstanceStatus, type WorkflowStatusResult, type Workflows, type X402ProcedureConfig, anyApi };
1335
+ export { type ActionCtx, type AggregateIndexDefinition, type AggregateOp, type AnyApi, type ArgsValidator, type AuthState, type CachePurge, type DatabaseReader, type DatabaseWriter, type DurableObjectJurisdiction, type ExternalSourceCursor, type ExternalSourceDefinition, type ExternalSourceMode, type ExternalSourceRefresh, type FunctionKind, type FunctionVisibility, type GlobalBackend, type IndexDefinition, type IndexRangeBuilder, type InferArgs, type LifecycleEvent, type LifecycleEventKind, type LunoraLogger, type MutationCtx, type OnDeleteAction, type PaginationOptions, type PaginationResult, type QueryCtx, type RankIndexDefinition, type RankSortKey, type ReadOnlyStorage, type RegisteredAction, type RegisteredFunction, type RegisteredLifecycleHook, type RegisteredMutation, type RegisteredQuery, type RegisteredStream, type RelationDefinition, type ScheduledFunctionDoc, type ScheduledJob, type Scheduler, type Schema, type SearchFilterBuilder, type SearchIndexDefinition, type Secrets, type SecretsStoreSecretLike, type ShardMode, type Storage, type StorageMetadata, type SystemDatabaseReader, type SystemDoc, type SystemQuery, type SystemTableName, type TableDefinition, type TableReader, type TableVectorIndex, type TriggerAggregateOptions, type TriggerBuilder, type TriggerCtx, type TriggerDatabase, type TriggerDefinition, type TriggerDeleteEvent, type TriggerEvent, type TriggerGroupByEntry, type TriggerGroupByOptions, type TriggerHandler, type TriggerInsertEvent, type TriggerOp, type TriggerQueryArgs, type TriggerQueryPage, type TriggerRankOptions, type TriggerRankPageOptions, type TriggerRankResult, type TriggerRow, type TriggerTiming, type TriggerUpdateEvent, type VectorEmbedder, type VectorIndexDefinition, type VectorMatch, type VectorMatches, type VectorMetric, type VectorQueryInput, type VectorRecord, type VectorSearch, type VectorSearchReader, type VectorUpsertInput, type WorkflowCreateOptions, type WorkflowHandle, type WorkflowInstance, type WorkflowInstanceStatus, type WorkflowStatusResult, type Workflows, type X402ProcedureConfig, anyApi };
package/dist/types.d.ts CHANGED
@@ -28,13 +28,38 @@ type ExternalSourceRefresh = "manual" | {
28
28
  everyMs: number;
29
29
  };
30
30
  /**
31
- * Delete-detection mode for external-source ingest. `"full-pull"` (default) reads
32
- * the whole tenant membership each tick and diffs it, so it observes upstream
33
- * deletes; `"incremental"` pulls only changed rows (cheap) and is blind to deletes
34
- * unless paired with a soft-delete column or a `reconcileEveryMs` full-pull sweep.
31
+ * Delete-detection mode for external-source ingest (plan 077 / 136).
32
+ *
33
+ * `"full-pull"` (the default) reads the **whole** tenant membership each tick and
34
+ * diffs it, so it observes upstream deletes for free — but costs a full read per tick
35
+ * (the Phase-0 bench put the ceiling at ~10k rows).
36
+ *
37
+ * `"incremental"` pulls **only rows past a durable watermark** (`cursor`), cheap for
38
+ * large low-churn tables above the full-pull cap. Because an absent row then means
39
+ * "unchanged", not "deleted", incremental requires a delete-visibility path: either a
40
+ * `reconcileEveryMs` periodic full-pull sweep, or a `softDeleteColumn` whose
41
+ * tombstones the pull returns. `defineSchema` throws (and the
42
+ * `external_source_incremental_no_delete_path` advisor lint fails the build) when an
43
+ * incremental source declares neither.
35
44
  */
36
45
  type ExternalSourceMode = "full-pull" | "incremental";
37
46
  /**
47
+ * Incremental-ingest cursor (plan 136): the monotonic watermark column plus the
48
+ * watermark-parameterized pull query. `column` names the field in the pulled rows
49
+ * whose max becomes the next watermark (e.g. `"updated_at"`). `query` is a second
50
+ * SQL that returns only rows changed since the watermark — the watermark binds as
51
+ * the parameter AFTER `tenantBy`'s params (e.g. Postgres
52
+ * `... WHERE tenant_id = $1 AND updated_at >= $2 ORDER BY updated_at`). Prefer `>=`
53
+ * with the idempotent upsert apply so rows sharing the boundary timestamp are never
54
+ * skipped (re-pulling them is a no-op).
55
+ */
56
+ interface ExternalSourceCursor {
57
+ /** The monotonic watermark column in the pulled rows; its max advances the stored watermark. */
58
+ column: string;
59
+ /** The incremental pull SQL. `tenantBy`'s params bind first, then the watermark as the trailing param. */
60
+ query: string;
61
+ }
62
+ /**
38
63
  * Config for `.source(...)` (plan 077): declares a table as **materialized from an
39
64
  * external Postgres/MySQL behind Cloudflare Hyperdrive**, not written by user
40
65
  * mutations. A system-driven poll loop reads the tenant slice and lands it in the
@@ -48,19 +73,33 @@ interface ExternalSourceDefinition {
48
73
  binding: string;
49
74
  /** Project the materialized rows to these columns (passed to the membership diff). Omit ⇒ the full mapped document. */
50
75
  columns?: ReadonlyArray<string>;
76
+ /** **Required for `mode: "incremental"`**: the watermark column + watermark-parameterized pull query (plan 136). Rejected on a `"full-pull"` source. */
77
+ cursor?: ExternalSourceCursor;
51
78
  /** Column whose value becomes the Lunora `_id`. Defaults to `"id"`. */
52
79
  idColumn?: string;
53
80
  /** Transform an external row into the stored document body. Omit ⇒ every selected column except `idColumn` is copied. */
54
81
  map?: (row: Record<string, unknown>) => Record<string, unknown>;
55
- /** Delete-detection mode. Defaults to `"full-pull"`. */
82
+ /** Delete-detection mode. `"full-pull"` (the default) diffs the whole membership; `"incremental"` pulls past a `cursor` watermark. */
56
83
  mode?: ExternalSourceMode;
57
84
  /** The full tenant-membership query, with driver-native placeholders (`$1` / `?`). `tenantBy` binds its params. */
58
85
  query: string;
59
- /** `"incremental"` only: run a full-pull reconcile this often to garbage-collect tombstones the incremental path can't see. */
86
+ /**
87
+ * **Incremental delete-visibility (plan 136)**: run a full-pull sweep at most
88
+ * this often (millis) to GC upstream deletes an incremental slice can't see.
89
+ * One of `reconcileEveryMs` / `softDeleteColumn` is required for incremental;
90
+ * rejected on a `"full-pull"` source.
91
+ */
60
92
  reconcileEveryMs?: number;
61
93
  /** Poll cadence, or `"manual"`. Omit ⇒ the runtime's size-scaled default. */
62
94
  refresh?: ExternalSourceRefresh;
63
95
  /**
96
+ * **Incremental delete-visibility (plan 136)**: the upstream soft-delete
97
+ * tombstone column (e.g. `"deleted_at"`). When set, the incremental pull must
98
+ * return tombstoned rows and the ingest turns each into a local delete — an
99
+ * alternative to `reconcileEveryMs`. Rejected on a `"full-pull"` source.
100
+ */
101
+ softDeleteColumn?: string;
102
+ /**
64
103
  * **Mandatory under `.shardBy()`**: map this DO's shard key → the query's bound
65
104
  * params, so a tenant DO can only ever pull its own rows. An unscoped sourced +
66
105
  * sharded table replicates the whole multitenant table into every shard — the
@@ -1293,4 +1332,4 @@ interface ActionCtx {
1293
1332
  */
1294
1333
  type AnyApi = Record<string, Record<string, RegisteredFunction<ArgsValidator, unknown, FunctionKind>>>;
1295
1334
  declare const anyApi: AnyApi;
1296
- export { type ActionCtx, type AggregateIndexDefinition, type AggregateOp, type AnyApi, type ArgsValidator, type AuthState, type CachePurge, type DatabaseReader, type DatabaseWriter, type DurableObjectJurisdiction, type ExternalSourceDefinition, type ExternalSourceMode, type ExternalSourceRefresh, type FunctionKind, type FunctionVisibility, type GlobalBackend, type IndexDefinition, type IndexRangeBuilder, type InferArgs, type LifecycleEvent, type LifecycleEventKind, type LunoraLogger, type MutationCtx, type OnDeleteAction, type PaginationOptions, type PaginationResult, type QueryCtx, type RankIndexDefinition, type RankSortKey, type ReadOnlyStorage, type RegisteredAction, type RegisteredFunction, type RegisteredLifecycleHook, type RegisteredMutation, type RegisteredQuery, type RegisteredStream, type RelationDefinition, type ScheduledFunctionDoc, type ScheduledJob, type Scheduler, type Schema, type SearchFilterBuilder, type SearchIndexDefinition, type Secrets, type SecretsStoreSecretLike, type ShardMode, type Storage, type StorageMetadata, type SystemDatabaseReader, type SystemDoc, type SystemQuery, type SystemTableName, type TableDefinition, type TableReader, type TableVectorIndex, type TriggerAggregateOptions, type TriggerBuilder, type TriggerCtx, type TriggerDatabase, type TriggerDefinition, type TriggerDeleteEvent, type TriggerEvent, type TriggerGroupByEntry, type TriggerGroupByOptions, type TriggerHandler, type TriggerInsertEvent, type TriggerOp, type TriggerQueryArgs, type TriggerQueryPage, type TriggerRankOptions, type TriggerRankPageOptions, type TriggerRankResult, type TriggerRow, type TriggerTiming, type TriggerUpdateEvent, type VectorEmbedder, type VectorIndexDefinition, type VectorMatch, type VectorMatches, type VectorMetric, type VectorQueryInput, type VectorRecord, type VectorSearch, type VectorSearchReader, type VectorUpsertInput, type WorkflowCreateOptions, type WorkflowHandle, type WorkflowInstance, type WorkflowInstanceStatus, type WorkflowStatusResult, type Workflows, type X402ProcedureConfig, anyApi };
1335
+ export { type ActionCtx, type AggregateIndexDefinition, type AggregateOp, type AnyApi, type ArgsValidator, type AuthState, type CachePurge, type DatabaseReader, type DatabaseWriter, type DurableObjectJurisdiction, type ExternalSourceCursor, type ExternalSourceDefinition, type ExternalSourceMode, type ExternalSourceRefresh, type FunctionKind, type FunctionVisibility, type GlobalBackend, type IndexDefinition, type IndexRangeBuilder, type InferArgs, type LifecycleEvent, type LifecycleEventKind, type LunoraLogger, type MutationCtx, type OnDeleteAction, type PaginationOptions, type PaginationResult, type QueryCtx, type RankIndexDefinition, type RankSortKey, type ReadOnlyStorage, type RegisteredAction, type RegisteredFunction, type RegisteredLifecycleHook, type RegisteredMutation, type RegisteredQuery, type RegisteredStream, type RelationDefinition, type ScheduledFunctionDoc, type ScheduledJob, type Scheduler, type Schema, type SearchFilterBuilder, type SearchIndexDefinition, type Secrets, type SecretsStoreSecretLike, type ShardMode, type Storage, type StorageMetadata, type SystemDatabaseReader, type SystemDoc, type SystemQuery, type SystemTableName, type TableDefinition, type TableReader, type TableVectorIndex, type TriggerAggregateOptions, type TriggerBuilder, type TriggerCtx, type TriggerDatabase, type TriggerDefinition, type TriggerDeleteEvent, type TriggerEvent, type TriggerGroupByEntry, type TriggerGroupByOptions, type TriggerHandler, type TriggerInsertEvent, type TriggerOp, type TriggerQueryArgs, type TriggerQueryPage, type TriggerRankOptions, type TriggerRankPageOptions, type TriggerRankResult, type TriggerRow, type TriggerTiming, type TriggerUpdateEvent, type VectorEmbedder, type VectorIndexDefinition, type VectorMatch, type VectorMatches, type VectorMetric, type VectorQueryInput, type VectorRecord, type VectorSearch, type VectorSearchReader, type VectorUpsertInput, type WorkflowCreateOptions, type WorkflowHandle, type WorkflowInstance, type WorkflowInstanceStatus, type WorkflowStatusResult, type Workflows, type X402ProcedureConfig, anyApi };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/server",
3
- "version": "1.0.0-alpha.24",
3
+ "version": "1.0.0-alpha.26",
4
4
  "description": "Server primitives for Lunora: defineSchema, defineTable, query, mutation, and action",
5
5
  "keywords": [
6
6
  "backend",
@@ -62,9 +62,9 @@
62
62
  "access": "public"
63
63
  },
64
64
  "dependencies": {
65
- "@lunora/errors": "1.0.0-alpha.4",
66
- "@lunora/scheduler": "1.0.0-alpha.9",
67
- "@lunora/values": "1.0.0-alpha.7",
65
+ "@lunora/errors": "1.0.0-alpha.5",
66
+ "@lunora/scheduler": "1.0.0-alpha.10",
67
+ "@lunora/values": "1.0.0-alpha.8",
68
68
  "drizzle-orm": "^0.45.2",
69
69
  "hono": "^4.12.28"
70
70
  },