@lunora/do 1.0.0-alpha.32 → 1.0.0-alpha.33

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
@@ -2499,13 +2499,31 @@ interface ExternalSourceLike {
2499
2499
  softDeleteColumn?: string;
2500
2500
  tenantBy?: (shardKey: string) => ReadonlyArray<unknown>;
2501
2501
  }
2502
+ /**
2503
+ * Coerce a driver-native value that `stableStringify` can't represent (see
2504
+ * `shared/stable-key.ts`) into its JSON-safe form: a `Date` → its ISO string, a
2505
+ * `bigint` → its decimal string. Every other value passes through unchanged.
2506
+ *
2507
+ * node-pg / postgres-js / mysql2 return `timestamp`/`datetime` columns as JS
2508
+ * `Date` and `bigint`/`int8` columns as `bigint` — both throw a `TypeError` out of
2509
+ * `stableStringify` (used by the full-pull diff and the incremental content
2510
+ * short-circuit), which bricks ingest for any table with such a column (e.g. the
2511
+ * canonical `cursor: { column: "updated_at" }` incremental config). This is the
2512
+ * single boundary where driver-native types cross into DO SQLite JSON; a new
2513
+ * source driver (or a new non-JSON column type) must be normalized here too.
2514
+ * `shared/stable-key.ts`'s throw contract is intentionally left unchanged — this
2515
+ * normalizes the value *before* it can ever reach that encoder.
2516
+ */
2517
+
2502
2518
  /**
2503
2519
  * Lift an external row to a Lunora document: the `idColumn` value becomes a
2504
2520
  * stringified `_id`, then either `map` shapes the body or every other column is
2505
2521
  * copied verbatim. Throws on a missing/null id, and on a non-scalar id, so a
2506
2522
  * misconfigured query fails loudly instead of materializing rows under the literal
2507
2523
  * id `"undefined"` (or collapsing many rows onto one id). Shared with
2508
- * `@lunora/hyperdrive`'s `projectSourceRow`.
2524
+ * `@lunora/hyperdrive`'s `projectSourceRow`. The returned document's values are
2525
+ * normalized (see {@link normalizeSourceValue}) so a `Date`/`bigint` column never
2526
+ * reaches `stableStringify` un-normalized.
2509
2527
  */
2510
2528
  declare const liftSourceId: (row: Record<string, unknown>, options?: {
2511
2529
  idColumn?: string;
@@ -4402,6 +4420,19 @@ declare abstract class ShardDO {
4402
4420
  private static rootSizeWarned;
4403
4421
  /** Test-only: reset the static "warned once" flag. */
4404
4422
  static resetRootSizeWarning(): void;
4423
+ /**
4424
+ * Compute the shared poll alarm's next wake time from both tiers' signals.
4425
+ * Global shapes need the fixed `GLOBAL_SHAPE_POLL_INTERVAL_MS` floor whenever
4426
+ * any are subscribed (a global table has no per-DO op-log, so it can only be
4427
+ * polled). External-source ingest instead reports the earliest NEXT-DUE
4428
+ * timestamp across its non-manual sources (or `undefined` when none exist) —
4429
+ * a source with a large `refresh.everyMs` must sleep until it's actually due,
4430
+ * not spin at the global-shape floor. Returns `undefined` when NEITHER tier
4431
+ * has pending work, so the DO can go fully idle instead of re-arming for no
4432
+ * reason; otherwise the earlier of the two candidate times (never later than
4433
+ * `nowMs`, so a source that's already due arms essentially immediately).
4434
+ */
4435
+ private static nextPollAlarmTarget;
4405
4436
  protected state: ShardDOState;
4406
4437
  protected env: unknown;
4407
4438
  /**
@@ -4733,12 +4764,17 @@ declare abstract class ShardDO {
4733
4764
  /** Hibernation API: invoked on socket error. */
4734
4765
  webSocketError(_ws: WebSocket, _error: unknown): void;
4735
4766
  /**
4736
- * Durable Object alarm handler — the heartbeat for `.global()`-table shapes.
4737
- * The runtime wakes this when the poll alarm armed by `scheduleGlobalPoll`
4738
- * fires; it refreshes every subscribed global shape (diff-poke from the global
4739
- * backend) and re-arms while any remain. With no global subscribers left, the
4740
- * alarm is not re-armed and the DO goes idle. A base-only / global-free DO
4741
- * never arms it, so this stays dormant there.
4767
+ * Durable Object alarm handler — the heartbeat for `.global()`-table shapes
4768
+ * AND external-source (`.source(...)`) ingest, which share one alarm. The
4769
+ * runtime wakes this when the poll alarm armed by `scheduleGlobalPoll` fires;
4770
+ * it refreshes every subscribed global shape (diff-poke from the global
4771
+ * backend), materializes any due sourced tables, and re-arms at
4772
+ * {@link ShardDO.nextPollAlarmTarget} the fixed floor while global shapes
4773
+ * are subscribed, or the earliest source next-due time when only ingest
4774
+ * remains (so a 1-hour-`refresh` source sleeps ~1 hour instead of waking
4775
+ * every 2 s). With neither tier pending, the alarm is not re-armed and the DO
4776
+ * goes idle. A base-only / global-free / source-free DO never arms it, so
4777
+ * this stays dormant there.
4742
4778
  */
4743
4779
  alarm(): Promise<void>;
4744
4780
  /** Subclasses implement function dispatch. */
@@ -5405,16 +5441,23 @@ declare abstract class ShardDO {
5405
5441
  /**
5406
5442
  * Poll external-source (`.source(...)`) tables once (plan 077): materialize
5407
5443
  * each sourced table's freshly-pulled tenant slice into this DO's SQLite. The
5408
- * base `ShardDO` has no sourced tables, so it returns `0` and the ingest tier
5409
- * stays dormant — zero behavior change for every existing DO. The codegen
5410
- * subclass overrides it to, per sourced table, build a `createShardCtxDb`
5411
- * writer, read the tenant slice from Hyperdrive under this DO's shard key, and
5412
- * run `runExternalSourceTick` (read local baseline → diff → apply via the
5413
- * validated CDC writer). Returns the number of sourced tables still being
5414
- * polled, so the shared poll alarm ({@link ShardDO.alarm}) re-arms while ingest
5415
- * is active.
5444
+ * base `ShardDO` has no sourced tables, so it returns `undefined` and the
5445
+ * ingest tier stays dormant — zero behavior change for every existing DO. The
5446
+ * codegen subclass overrides it to, per sourced table, build a
5447
+ * `createShardCtxDb` writer, read the tenant slice from Hyperdrive under this
5448
+ * DO's shard key, and run `runExternalSourceTick` (read local baseline → diff
5449
+ * → apply via the validated CDC writer).
5450
+ *
5451
+ * Returns the EARLIEST next-due timestamp (absolute epoch ms) across every
5452
+ * non-manual sourced table — `polledAt.get(table) ?? now` plus that source's
5453
+ * `refresh.everyMs` — or `undefined` when every sourced table is
5454
+ * `refresh: "manual"` (or there are none). NOT a bare active count: the
5455
+ * shared poll alarm ({@link ShardDO.alarm}) uses this to re-arm at the exact
5456
+ * next time ingest needs to run, instead of spinning at the fixed
5457
+ * `GLOBAL_SHAPE_POLL_INTERVAL_MS` floor for a source whose `refresh.everyMs`
5458
+ * is, say, an hour away.
5416
5459
  */
5417
- protected pollExternalSources(): Promise<number>;
5460
+ protected pollExternalSources(): Promise<number | undefined>;
5418
5461
  /**
5419
5462
  * Arm the shared poll alarm for external-source ingest (plan 077). The alarm is
5420
5463
  * shared with the global-shape poll tier; the codegen subclass calls this once
@@ -6414,11 +6457,18 @@ declare abstract class ShardDO {
6414
6457
  */
6415
6458
  private saveGlobalSnapshot;
6416
6459
  /**
6417
- * Arm the poll alarm for `.global()` shapes if one isn't already pending.
6418
- * Idempotent — every global-shape seed calls it, but only the first arms the
6419
- * alarm. Degrades to a no-op when the runtime exposes no `setAlarm` (the unit
6420
- * harness): a global shape is then seed-only, which the poll-loop tests assert
6421
- * by driving {@link ShardDO.alarm} directly.
6460
+ * Arm the poll alarm if one isn't already pending. Idempotent — every
6461
+ * global-shape seed calls it, but only the first arms the alarm. Degrades to
6462
+ * a no-op when the runtime exposes no `setAlarm` (the unit harness): a global
6463
+ * shape is then seed-only, which the poll-loop tests assert by driving
6464
+ * {@link ShardDO.alarm} directly.
6465
+ *
6466
+ * `atMs` lets {@link ShardDO.alarm} re-arm at a computed target — the
6467
+ * earlier of the fixed global-shape floor and the earliest external-source
6468
+ * next-due time — instead of always the fixed floor. Every OTHER caller
6469
+ * (a fresh global-shape seed, {@link ShardDO.scheduleSourcePoll}'s initial
6470
+ * kick) omits it and gets the original `GLOBAL_SHAPE_POLL_INTERVAL_MS`
6471
+ * default, since neither knows a more precise due time yet.
6422
6472
  */
6423
6473
  private scheduleGlobalPoll;
6424
6474
  /**
package/dist/index.d.ts CHANGED
@@ -2499,13 +2499,31 @@ interface ExternalSourceLike {
2499
2499
  softDeleteColumn?: string;
2500
2500
  tenantBy?: (shardKey: string) => ReadonlyArray<unknown>;
2501
2501
  }
2502
+ /**
2503
+ * Coerce a driver-native value that `stableStringify` can't represent (see
2504
+ * `shared/stable-key.ts`) into its JSON-safe form: a `Date` → its ISO string, a
2505
+ * `bigint` → its decimal string. Every other value passes through unchanged.
2506
+ *
2507
+ * node-pg / postgres-js / mysql2 return `timestamp`/`datetime` columns as JS
2508
+ * `Date` and `bigint`/`int8` columns as `bigint` — both throw a `TypeError` out of
2509
+ * `stableStringify` (used by the full-pull diff and the incremental content
2510
+ * short-circuit), which bricks ingest for any table with such a column (e.g. the
2511
+ * canonical `cursor: { column: "updated_at" }` incremental config). This is the
2512
+ * single boundary where driver-native types cross into DO SQLite JSON; a new
2513
+ * source driver (or a new non-JSON column type) must be normalized here too.
2514
+ * `shared/stable-key.ts`'s throw contract is intentionally left unchanged — this
2515
+ * normalizes the value *before* it can ever reach that encoder.
2516
+ */
2517
+
2502
2518
  /**
2503
2519
  * Lift an external row to a Lunora document: the `idColumn` value becomes a
2504
2520
  * stringified `_id`, then either `map` shapes the body or every other column is
2505
2521
  * copied verbatim. Throws on a missing/null id, and on a non-scalar id, so a
2506
2522
  * misconfigured query fails loudly instead of materializing rows under the literal
2507
2523
  * id `"undefined"` (or collapsing many rows onto one id). Shared with
2508
- * `@lunora/hyperdrive`'s `projectSourceRow`.
2524
+ * `@lunora/hyperdrive`'s `projectSourceRow`. The returned document's values are
2525
+ * normalized (see {@link normalizeSourceValue}) so a `Date`/`bigint` column never
2526
+ * reaches `stableStringify` un-normalized.
2509
2527
  */
2510
2528
  declare const liftSourceId: (row: Record<string, unknown>, options?: {
2511
2529
  idColumn?: string;
@@ -4402,6 +4420,19 @@ declare abstract class ShardDO {
4402
4420
  private static rootSizeWarned;
4403
4421
  /** Test-only: reset the static "warned once" flag. */
4404
4422
  static resetRootSizeWarning(): void;
4423
+ /**
4424
+ * Compute the shared poll alarm's next wake time from both tiers' signals.
4425
+ * Global shapes need the fixed `GLOBAL_SHAPE_POLL_INTERVAL_MS` floor whenever
4426
+ * any are subscribed (a global table has no per-DO op-log, so it can only be
4427
+ * polled). External-source ingest instead reports the earliest NEXT-DUE
4428
+ * timestamp across its non-manual sources (or `undefined` when none exist) —
4429
+ * a source with a large `refresh.everyMs` must sleep until it's actually due,
4430
+ * not spin at the global-shape floor. Returns `undefined` when NEITHER tier
4431
+ * has pending work, so the DO can go fully idle instead of re-arming for no
4432
+ * reason; otherwise the earlier of the two candidate times (never later than
4433
+ * `nowMs`, so a source that's already due arms essentially immediately).
4434
+ */
4435
+ private static nextPollAlarmTarget;
4405
4436
  protected state: ShardDOState;
4406
4437
  protected env: unknown;
4407
4438
  /**
@@ -4733,12 +4764,17 @@ declare abstract class ShardDO {
4733
4764
  /** Hibernation API: invoked on socket error. */
4734
4765
  webSocketError(_ws: WebSocket, _error: unknown): void;
4735
4766
  /**
4736
- * Durable Object alarm handler — the heartbeat for `.global()`-table shapes.
4737
- * The runtime wakes this when the poll alarm armed by `scheduleGlobalPoll`
4738
- * fires; it refreshes every subscribed global shape (diff-poke from the global
4739
- * backend) and re-arms while any remain. With no global subscribers left, the
4740
- * alarm is not re-armed and the DO goes idle. A base-only / global-free DO
4741
- * never arms it, so this stays dormant there.
4767
+ * Durable Object alarm handler — the heartbeat for `.global()`-table shapes
4768
+ * AND external-source (`.source(...)`) ingest, which share one alarm. The
4769
+ * runtime wakes this when the poll alarm armed by `scheduleGlobalPoll` fires;
4770
+ * it refreshes every subscribed global shape (diff-poke from the global
4771
+ * backend), materializes any due sourced tables, and re-arms at
4772
+ * {@link ShardDO.nextPollAlarmTarget} the fixed floor while global shapes
4773
+ * are subscribed, or the earliest source next-due time when only ingest
4774
+ * remains (so a 1-hour-`refresh` source sleeps ~1 hour instead of waking
4775
+ * every 2 s). With neither tier pending, the alarm is not re-armed and the DO
4776
+ * goes idle. A base-only / global-free / source-free DO never arms it, so
4777
+ * this stays dormant there.
4742
4778
  */
4743
4779
  alarm(): Promise<void>;
4744
4780
  /** Subclasses implement function dispatch. */
@@ -5405,16 +5441,23 @@ declare abstract class ShardDO {
5405
5441
  /**
5406
5442
  * Poll external-source (`.source(...)`) tables once (plan 077): materialize
5407
5443
  * each sourced table's freshly-pulled tenant slice into this DO's SQLite. The
5408
- * base `ShardDO` has no sourced tables, so it returns `0` and the ingest tier
5409
- * stays dormant — zero behavior change for every existing DO. The codegen
5410
- * subclass overrides it to, per sourced table, build a `createShardCtxDb`
5411
- * writer, read the tenant slice from Hyperdrive under this DO's shard key, and
5412
- * run `runExternalSourceTick` (read local baseline → diff → apply via the
5413
- * validated CDC writer). Returns the number of sourced tables still being
5414
- * polled, so the shared poll alarm ({@link ShardDO.alarm}) re-arms while ingest
5415
- * is active.
5444
+ * base `ShardDO` has no sourced tables, so it returns `undefined` and the
5445
+ * ingest tier stays dormant — zero behavior change for every existing DO. The
5446
+ * codegen subclass overrides it to, per sourced table, build a
5447
+ * `createShardCtxDb` writer, read the tenant slice from Hyperdrive under this
5448
+ * DO's shard key, and run `runExternalSourceTick` (read local baseline → diff
5449
+ * → apply via the validated CDC writer).
5450
+ *
5451
+ * Returns the EARLIEST next-due timestamp (absolute epoch ms) across every
5452
+ * non-manual sourced table — `polledAt.get(table) ?? now` plus that source's
5453
+ * `refresh.everyMs` — or `undefined` when every sourced table is
5454
+ * `refresh: "manual"` (or there are none). NOT a bare active count: the
5455
+ * shared poll alarm ({@link ShardDO.alarm}) uses this to re-arm at the exact
5456
+ * next time ingest needs to run, instead of spinning at the fixed
5457
+ * `GLOBAL_SHAPE_POLL_INTERVAL_MS` floor for a source whose `refresh.everyMs`
5458
+ * is, say, an hour away.
5416
5459
  */
5417
- protected pollExternalSources(): Promise<number>;
5460
+ protected pollExternalSources(): Promise<number | undefined>;
5418
5461
  /**
5419
5462
  * Arm the shared poll alarm for external-source ingest (plan 077). The alarm is
5420
5463
  * shared with the global-shape poll tier; the codegen subclass calls this once
@@ -6414,11 +6457,18 @@ declare abstract class ShardDO {
6414
6457
  */
6415
6458
  private saveGlobalSnapshot;
6416
6459
  /**
6417
- * Arm the poll alarm for `.global()` shapes if one isn't already pending.
6418
- * Idempotent — every global-shape seed calls it, but only the first arms the
6419
- * alarm. Degrades to a no-op when the runtime exposes no `setAlarm` (the unit
6420
- * harness): a global shape is then seed-only, which the poll-loop tests assert
6421
- * by driving {@link ShardDO.alarm} directly.
6460
+ * Arm the poll alarm if one isn't already pending. Idempotent — every
6461
+ * global-shape seed calls it, but only the first arms the alarm. Degrades to
6462
+ * a no-op when the runtime exposes no `setAlarm` (the unit harness): a global
6463
+ * shape is then seed-only, which the poll-loop tests assert by driving
6464
+ * {@link ShardDO.alarm} directly.
6465
+ *
6466
+ * `atMs` lets {@link ShardDO.alarm} re-arm at a computed target — the
6467
+ * earlier of the fixed global-shape floor and the earliest external-source
6468
+ * next-due time — instead of always the fixed floor. Every OTHER caller
6469
+ * (a fresh global-shape seed, {@link ShardDO.scheduleSourcePoll}'s initial
6470
+ * kick) omits it and gets the original `GLOBAL_SHAPE_POLL_INTERVAL_MS`
6471
+ * default, since neither knows a more precise due time yet.
6422
6472
  */
6423
6473
  private scheduleGlobalPoll;
6424
6474
  /**
package/dist/index.mjs CHANGED
@@ -3,13 +3,13 @@ export { AGGREGATE_SQL_FUNCTION, aggregateSqlFunction, matchesStaticWhere, norma
3
3
  export { aggregateTableName, coerceAggregateNumber, encodeAggregateKey, foldAggregateTally, readAggregateValue } from './packem_shared/aggregateTableName-CxNqY1Sl.mjs';
4
4
  export { CountRlsUnsupportedError, mergeWhere, planAggregateLookup, selectIndexForAggregate, selectIndexForCount, selectIndexForGroupBy } from './packem_shared/CountRlsUnsupportedError-BGxj0pgS.mjs';
5
5
  export { AUTH_METRICS_BUCKETS_TABLE, AUTH_METRICS_BUCKET_MS, AUTH_METRICS_BUCKET_RETENTION, AUTH_METRICS_TABLE, ensureAuthMetricsTables, readAuthMetrics, recordAuthEvent } from './packem_shared/AUTH_METRICS_BUCKETS_TABLE-CiHHYeJi.mjs';
6
- export { NotUniqueError, assertValidClientId, createShardCtxDb, normalizeIdStructurally } from './packem_shared/NotUniqueError-BigrdT_W.mjs';
6
+ export { NotUniqueError, assertValidClientId, createShardCtxDb, normalizeIdStructurally } from './packem_shared/NotUniqueError-Ca21uDuU.mjs';
7
7
  export { DATA_MIGRATION_STATE_TABLE, readMigrationStatus, runDataMigration } from './packem_shared/DATA_MIGRATION_STATE_TABLE-CYwBpyTr.mjs';
8
8
  export { SCAN_DEP, createDependencyTracker, depKey } from './packem_shared/SCAN_DEP-DLJF8dsj.mjs';
9
9
  export { renderSql } from './packem_shared/renderSql-D6eUcn2N.mjs';
10
10
  export { diffExternalSource } from './packem_shared/diffExternalSource-CovHfdyo.mjs';
11
- export { materializeExternalRows, materializeExternalRowsIncremental, readExternalSourceBaseline, runExternalSourceTick } from './packem_shared/materializeExternalRows-FyA5Rwn4.mjs';
12
- export { isSoftDeleted, isSourceDue, liftSourceId, pullExternalSourceIncrementalTick, pullExternalSourceTick } from './packem_shared/isSoftDeleted-i5kKh6Up.mjs';
11
+ export { materializeExternalRows, materializeExternalRowsIncremental, readExternalSourceBaseline, runExternalSourceTick } from './packem_shared/materializeExternalRows-DlQWMlw_.mjs';
12
+ export { isSoftDeleted, isSourceDue, liftSourceId, pullExternalSourceIncrementalTick, pullExternalSourceTick } from './packem_shared/isSoftDeleted-YLKR6JYw.mjs';
13
13
  export { FUNCTION_METRICS_BUCKETS_TABLE, FUNCTION_METRICS_BUCKET_MS, FUNCTION_METRICS_BUCKET_RETENTION, FUNCTION_METRICS_INDEX_TABLE, FUNCTION_METRICS_TABLE, ensureFunctionMetricsTables, readFunctionMetricBuckets, readFunctionMetricIndexHits, readFunctionMetrics, readFunctionMetricsTotals, recordFunctionMetric } from './packem_shared/FUNCTION_METRICS_BUCKETS_TABLE-UDNVD7FS.mjs';
14
14
  export { ADMIN_FUNCTIONS, ADMIN_FUNCTION_PREFIX, FLAGS_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, facetColumn, listTables, readTablePage, selectMatchingIds } from './packem_shared/ADMIN_FUNCTIONS-CAHLZMj8.mjs';
15
15
  export { LogBuffer } from './packem_shared/LogBuffer-B_Ezju_N.mjs';
@@ -26,16 +26,16 @@ export { RLS_UNWRAP_SYMBOL, RlsRequiredError, guardWriter } from './packem_share
26
26
  export { buildFtsMatch, ftsTableName, scoreDocument, stringifySearchText, tokenizeSearch } from './packem_shared/buildFtsMatch-BLEMawrp.mjs';
27
27
  export { M as MIN_ADMIN_TOKEN_LENGTH, a as MIN_AUTH_SECRET_LENGTH, b as buildSecurityAudit } from './packem_shared/security-audit-CucgBice.mjs';
28
28
  export { SESSION_DO_TTL_DEFAULT, SessionDO } from './packem_shared/SESSION_DO_TTL_DEFAULT-BnSKgVO4.mjs';
29
- export { ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, ShardDO } from './packem_shared/ROOT_DO_SIZE_WARN_BYTES-BWz51mpB.mjs';
29
+ export { ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, ShardDO } from './packem_shared/ROOT_DO_SIZE_WARN_BYTES-afQHUhyA.mjs';
30
30
  export { SHARD_REGISTRY_DO_NAME, ShardRegistryDO } from './packem_shared/SHARD_REGISTRY_DO_NAME-D99roc-r.mjs';
31
31
  export { MAX_SQL_ROWS, assertReadonly, runReadonlySql } from './packem_shared/MAX_SQL_ROWS-iFAA8FbD.mjs';
32
32
  export { createSystemReader } from './packem_shared/createSystemReader-D12eNH13.mjs';
33
33
  export { ConflictError } from './packem_shared/ConflictError-CLoq37xH.mjs';
34
34
  export { hasTrigger, runTriggers } from './packem_shared/hasTrigger-5N6_Fx0A.mjs';
35
35
  export { compileWhereSql } from './packem_shared/compileWhereSql-DE6yfRcQ.mjs';
36
- export { CDC_LOG_TABLE, applyCdcChanges, readCdcChanges, trimCdcChanges } from './packem_shared/CDC_LOG_TABLE-DjJEHiM2.mjs';
36
+ export { CDC_LOG_TABLE, applyCdcChanges, readCdcChanges, trimCdcChanges } from './packem_shared/CDC_LOG_TABLE-uwOJxJJZ.mjs';
37
37
  export { backfillAggregateIndexes, backfillRankIndexes } from './packem_shared/backfillAggregateIndexes-DDoT-UUI.mjs';
38
- export { runShardMigrations } from './packem_shared/runShardMigrations-BGx4v2B6.mjs';
38
+ export { runShardMigrations } from './packem_shared/runShardMigrations-DFzx6Qld.mjs';
39
39
  export { stableStringify } from './packem_shared/stableStringify-mC40mZts.mjs';
40
40
  export { stableWireKey } from './packem_shared/stableWireKey-DKuXO7T5.mjs';
41
41
  export { subscriptionListDeltas } from './packem_shared/subscriptionListDeltas-CT76bYny.mjs';
@@ -79,7 +79,7 @@ const bumpCdcEpoch = (sql$1) => {
79
79
  };
80
80
  const applyCdcChange = async (writer, change) => {
81
81
  if (change.op === "delete") {
82
- await writer.delete(change.id);
82
+ await writer.delete(change.id, change.table);
83
83
  return;
84
84
  }
85
85
  const document = change.doc ?? {};
@@ -3,8 +3,8 @@ import { sql } from 'drizzle-orm';
3
3
  import { matchesStaticWhere, aggregateSqlFunction, normalizeCountArgument, throwingScheduler } from './AGGREGATE_SQL_FUNCTION-CQsu2Xga.mjs';
4
4
  import { encodeAggregateKey, foldAggregateTally, aggregateTableName, coerceAggregateNumber, readAggregateValue } from './aggregateTableName-CxNqY1Sl.mjs';
5
5
  import { mergeWhere, CountRlsUnsupportedError, selectIndexForGroupBy, selectIndexForCount, selectIndexForAggregate } from './CountRlsUnsupportedError-BGxj0pgS.mjs';
6
- import { appendCdcChange } from './CDC_LOG_TABLE-DjJEHiM2.mjs';
7
- export { CDC_LOG_TABLE, applyCdcChanges, bumpCdcEpoch, minCdcSeq, readCdcChanges, readCdcCursor, readCdcEpoch, trimCdcChanges } from './CDC_LOG_TABLE-DjJEHiM2.mjs';
6
+ import { appendCdcChange } from './CDC_LOG_TABLE-uwOJxJJZ.mjs';
7
+ export { CDC_LOG_TABLE, applyCdcChanges, bumpCdcEpoch, minCdcSeq, readCdcChanges, readCdcCursor, readCdcEpoch, trimCdcChanges } from './CDC_LOG_TABLE-uwOJxJJZ.mjs';
8
8
  import { r as runDrizzle } from './do-exec-5eQy5cEi.mjs';
9
9
  import { i as isFtsAvailable, D as DOC_COLUMN$1, r as rowToDocument, A as AGG_KEY, a as AGG_VALUE, b as AGG_COUNT, d as aggUpsertSql, j as jsonPathSql, q as quoteIdentifier, t as tableColumns, e as qualifiedJsonPathSql } from './do-sql-BCHCWtrD.mjs';
10
10
  import { param } from './renderSql-D6eUcn2N.mjs';
@@ -23,7 +23,7 @@ import { runTriggers } from './hasTrigger-5N6_Fx0A.mjs';
23
23
  import { compileWhereSql } from './compileWhereSql-DE6yfRcQ.mjs';
24
24
  export { backfillAggregateIndexes, backfillRankIndexes } from './backfillAggregateIndexes-DDoT-UUI.mjs';
25
25
  export { C as CLIENT_WATERMARK_TABLE, G as GLOBAL_SHAPE_SNAPSHOT_TABLE, I as IDEMPOTENCY_TABLE, c as advanceClientWatermark, d as deleteGlobalShapeSnapshot, e as deleteGlobalShapeSnapshotsForConnection, m as migrateClientWatermark, b as migrateGlobalShapeSnapshot, r as readClientWatermark, f as readGlobalShapeSnapshot, g as readIdempotent, t as trimIdempotent, w as writeGlobalShapeSnapshot, h as writeIdempotent } from './ctx-db-idempotency-BdcNpvY4.mjs';
26
- export { runShardMigrations } from './runShardMigrations-BGx4v2B6.mjs';
26
+ export { runShardMigrations } from './runShardMigrations-DFzx6Qld.mjs';
27
27
  export { a as selectShapeMemberIds, s as selectShapeRows } from './ctx-db-shapes-Cz9dHyh1.mjs';
28
28
 
29
29
  const rankIndexFieldsUnchanged = (index, previous, next) => {
@@ -21,7 +21,7 @@ import { i as isDevEnvironment, c as buildSettings, b as buildSecurityAudit } fr
21
21
  import { runReadonlySql } from './MAX_SQL_ROWS-iFAA8FbD.mjs';
22
22
  import { ConflictError } from './ConflictError-CLoq37xH.mjs';
23
23
  import { e as deleteGlobalShapeSnapshotsForConnection, g as readIdempotent, h as writeIdempotent, t as trimIdempotent, r as readClientWatermark, m as migrateClientWatermark, c as advanceClientWatermark, d as deleteGlobalShapeSnapshot, f as readGlobalShapeSnapshot, w as writeGlobalShapeSnapshot } from './ctx-db-idempotency-BdcNpvY4.mjs';
24
- import { CDC_LOG_TABLE, readCdcChanges, readCdcCursor, readCdcEpoch, minCdcSeq, bumpCdcEpoch } from './CDC_LOG_TABLE-DjJEHiM2.mjs';
24
+ import { CDC_LOG_TABLE, readCdcChanges, readCdcCursor, readCdcEpoch, minCdcSeq, bumpCdcEpoch } from './CDC_LOG_TABLE-uwOJxJJZ.mjs';
25
25
  import { stableStringify } from './stableStringify-mC40mZts.mjs';
26
26
  import { a as selectShapeMemberIds, s as selectShapeRows } from './ctx-db-shapes-Cz9dHyh1.mjs';
27
27
 
@@ -2088,6 +2088,25 @@ class ShardDO {
2088
2088
  static resetRootSizeWarning() {
2089
2089
  ShardDO.rootSizeWarned = false;
2090
2090
  }
2091
+ /**
2092
+ * Compute the shared poll alarm's next wake time from both tiers' signals.
2093
+ * Global shapes need the fixed `GLOBAL_SHAPE_POLL_INTERVAL_MS` floor whenever
2094
+ * any are subscribed (a global table has no per-DO op-log, so it can only be
2095
+ * polled). External-source ingest instead reports the earliest NEXT-DUE
2096
+ * timestamp across its non-manual sources (or `undefined` when none exist) —
2097
+ * a source with a large `refresh.everyMs` must sleep until it's actually due,
2098
+ * not spin at the global-shape floor. Returns `undefined` when NEITHER tier
2099
+ * has pending work, so the DO can go fully idle instead of re-arming for no
2100
+ * reason; otherwise the earlier of the two candidate times (never later than
2101
+ * `nowMs`, so a source that's already due arms essentially immediately).
2102
+ */
2103
+ static nextPollAlarmTarget(globalShapesRemaining, nextSourceDueAt, nowMs) {
2104
+ const globalTarget = globalShapesRemaining > 0 ? nowMs + ShardDO.GLOBAL_SHAPE_POLL_INTERVAL_MS : void 0;
2105
+ if (globalTarget === void 0) {
2106
+ return nextSourceDueAt === void 0 ? void 0 : Math.max(nextSourceDueAt, nowMs);
2107
+ }
2108
+ return nextSourceDueAt === void 0 ? globalTarget : Math.min(globalTarget, nextSourceDueAt);
2109
+ }
2091
2110
  state;
2092
2111
  env;
2093
2112
  /**
@@ -2709,31 +2728,38 @@ class ShardDO {
2709
2728
  webSocketError(_ws, _error) {
2710
2729
  }
2711
2730
  /**
2712
- * Durable Object alarm handler — the heartbeat for `.global()`-table shapes.
2713
- * The runtime wakes this when the poll alarm armed by `scheduleGlobalPoll`
2714
- * fires; it refreshes every subscribed global shape (diff-poke from the global
2715
- * backend) and re-arms while any remain. With no global subscribers left, the
2716
- * alarm is not re-armed and the DO goes idle. A base-only / global-free DO
2717
- * never arms it, so this stays dormant there.
2731
+ * Durable Object alarm handler — the heartbeat for `.global()`-table shapes
2732
+ * AND external-source (`.source(...)`) ingest, which share one alarm. The
2733
+ * runtime wakes this when the poll alarm armed by `scheduleGlobalPoll` fires;
2734
+ * it refreshes every subscribed global shape (diff-poke from the global
2735
+ * backend), materializes any due sourced tables, and re-arms at
2736
+ * {@link ShardDO.nextPollAlarmTarget} the fixed floor while global shapes
2737
+ * are subscribed, or the earliest source next-due time when only ingest
2738
+ * remains (so a 1-hour-`refresh` source sleeps ~1 hour instead of waking
2739
+ * every 2 s). With neither tier pending, the alarm is not re-armed and the DO
2740
+ * goes idle. A base-only / global-free / source-free DO never arms it, so
2741
+ * this stays dormant there.
2718
2742
  */
2719
2743
  async alarm() {
2720
2744
  this.globalPollScheduled = false;
2721
- let remaining;
2745
+ let globalShapesRemaining;
2722
2746
  try {
2723
- remaining = await this.pollGlobalShapes();
2747
+ globalShapesRemaining = await this.pollGlobalShapes();
2724
2748
  } catch (error) {
2725
2749
  this.recordShapeError("shape:poll", error);
2726
- remaining = 1;
2750
+ globalShapesRemaining = 1;
2727
2751
  }
2752
+ let nextSourceDueAt;
2728
2753
  try {
2729
- remaining += await this.pollExternalSources();
2754
+ nextSourceDueAt = await this.pollExternalSources();
2730
2755
  } catch (error) {
2731
2756
  this.recordShapeError("source:poll", error);
2732
- remaining += 1;
2757
+ nextSourceDueAt = Date.now() + ShardDO.GLOBAL_SHAPE_POLL_INTERVAL_MS;
2733
2758
  }
2734
2759
  await this.flushChangedTables();
2735
- if (remaining > 0) {
2736
- await this.scheduleGlobalPoll();
2760
+ const nextAlarmAt = ShardDO.nextPollAlarmTarget(globalShapesRemaining, nextSourceDueAt, Date.now());
2761
+ if (nextAlarmAt !== void 0) {
2762
+ await this.scheduleGlobalPoll(nextAlarmAt);
2737
2763
  }
2738
2764
  }
2739
2765
  /**
@@ -3881,18 +3907,25 @@ class ShardDO {
3881
3907
  /**
3882
3908
  * Poll external-source (`.source(...)`) tables once (plan 077): materialize
3883
3909
  * each sourced table's freshly-pulled tenant slice into this DO's SQLite. The
3884
- * base `ShardDO` has no sourced tables, so it returns `0` and the ingest tier
3885
- * stays dormant — zero behavior change for every existing DO. The codegen
3886
- * subclass overrides it to, per sourced table, build a `createShardCtxDb`
3887
- * writer, read the tenant slice from Hyperdrive under this DO's shard key, and
3888
- * run `runExternalSourceTick` (read local baseline → diff → apply via the
3889
- * validated CDC writer). Returns the number of sourced tables still being
3890
- * polled, so the shared poll alarm ({@link ShardDO.alarm}) re-arms while ingest
3891
- * is active.
3910
+ * base `ShardDO` has no sourced tables, so it returns `undefined` and the
3911
+ * ingest tier stays dormant — zero behavior change for every existing DO. The
3912
+ * codegen subclass overrides it to, per sourced table, build a
3913
+ * `createShardCtxDb` writer, read the tenant slice from Hyperdrive under this
3914
+ * DO's shard key, and run `runExternalSourceTick` (read local baseline → diff
3915
+ * → apply via the validated CDC writer).
3916
+ *
3917
+ * Returns the EARLIEST next-due timestamp (absolute epoch ms) across every
3918
+ * non-manual sourced table — `polledAt.get(table) ?? now` plus that source's
3919
+ * `refresh.everyMs` — or `undefined` when every sourced table is
3920
+ * `refresh: "manual"` (or there are none). NOT a bare active count: the
3921
+ * shared poll alarm ({@link ShardDO.alarm}) uses this to re-arm at the exact
3922
+ * next time ingest needs to run, instead of spinning at the fixed
3923
+ * `GLOBAL_SHAPE_POLL_INTERVAL_MS` floor for a source whose `refresh.everyMs`
3924
+ * is, say, an hour away.
3892
3925
  */
3893
3926
  // eslint-disable-next-line class-methods-use-this -- base-class override hook: the codegen subclass implements the real Hyperdrive-backed poll
3894
3927
  pollExternalSources() {
3895
- return Promise.resolve(0);
3928
+ return Promise.resolve(void 0);
3896
3929
  }
3897
3930
  /**
3898
3931
  * Arm the shared poll alarm for external-source ingest (plan 077). The alarm is
@@ -6171,13 +6204,20 @@ class ShardDO {
6171
6204
  }
6172
6205
  }
6173
6206
  /**
6174
- * Arm the poll alarm for `.global()` shapes if one isn't already pending.
6175
- * Idempotent — every global-shape seed calls it, but only the first arms the
6176
- * alarm. Degrades to a no-op when the runtime exposes no `setAlarm` (the unit
6177
- * harness): a global shape is then seed-only, which the poll-loop tests assert
6178
- * by driving {@link ShardDO.alarm} directly.
6179
- */
6180
- async scheduleGlobalPoll() {
6207
+ * Arm the poll alarm if one isn't already pending. Idempotent — every
6208
+ * global-shape seed calls it, but only the first arms the alarm. Degrades to
6209
+ * a no-op when the runtime exposes no `setAlarm` (the unit harness): a global
6210
+ * shape is then seed-only, which the poll-loop tests assert by driving
6211
+ * {@link ShardDO.alarm} directly.
6212
+ *
6213
+ * `atMs` lets {@link ShardDO.alarm} re-arm at a computed target — the
6214
+ * earlier of the fixed global-shape floor and the earliest external-source
6215
+ * next-due time — instead of always the fixed floor. Every OTHER caller
6216
+ * (a fresh global-shape seed, {@link ShardDO.scheduleSourcePoll}'s initial
6217
+ * kick) omits it and gets the original `GLOBAL_SHAPE_POLL_INTERVAL_MS`
6218
+ * default, since neither knows a more precise due time yet.
6219
+ */
6220
+ async scheduleGlobalPoll(atMs) {
6181
6221
  if (this.globalPollScheduled) {
6182
6222
  return;
6183
6223
  }
@@ -6187,7 +6227,7 @@ class ShardDO {
6187
6227
  }
6188
6228
  this.globalPollScheduled = true;
6189
6229
  try {
6190
- await setAlarm.call(this.state.storage, Date.now() + ShardDO.GLOBAL_SHAPE_POLL_INTERVAL_MS);
6230
+ await setAlarm.call(this.state.storage, atMs ?? Date.now() + ShardDO.GLOBAL_SHAPE_POLL_INTERVAL_MS);
6191
6231
  } catch {
6192
6232
  this.globalPollScheduled = false;
6193
6233
  }
@@ -1,7 +1,7 @@
1
1
  import { LunoraError } from '@lunora/errors';
2
2
  import { sql } from 'drizzle-orm';
3
3
  import { r as runDrizzle } from './do-exec-5eQy5cEi.mjs';
4
- import { runExternalSourceTick, materializeExternalRowsIncremental } from './materializeExternalRows-FyA5Rwn4.mjs';
4
+ import { runExternalSourceTick, materializeExternalRowsIncremental } from './materializeExternalRows-DlQWMlw_.mjs';
5
5
 
6
6
  const SOURCE_CURSOR_TABLE = "__lunora_source_cursor";
7
7
  const serializeCursor = (value) => {
@@ -96,6 +96,28 @@ const writeSourceCursor = (sql$1, table, shardKey, state) => {
96
96
  );
97
97
  };
98
98
 
99
+ const normalizeSourceValue = (value) => {
100
+ if (value instanceof Date) {
101
+ return value.toISOString();
102
+ }
103
+ if (typeof value === "bigint") {
104
+ return String(value);
105
+ }
106
+ if (Array.isArray(value)) {
107
+ return value.map((element) => normalizeSourceValue(element));
108
+ }
109
+ if (value !== null && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
110
+ return normalizeSourceDocument(value);
111
+ }
112
+ return value;
113
+ };
114
+ const normalizeSourceDocument = (document) => {
115
+ const normalized = {};
116
+ for (const [key, value] of Object.entries(document)) {
117
+ normalized[key] = normalizeSourceValue(value);
118
+ }
119
+ return normalized;
120
+ };
99
121
  const liftSourceId = (row, options = {}) => {
100
122
  const { idColumn = "id", map } = options;
101
123
  const idValue = row[idColumn];
@@ -107,7 +129,7 @@ const liftSourceId = (row, options = {}) => {
107
129
  }
108
130
  const id = String(idValue);
109
131
  if (map) {
110
- return { ...map(row), _id: id };
132
+ return normalizeSourceDocument({ ...map(row), _id: id });
111
133
  }
112
134
  const body = {};
113
135
  for (const [key, value] of Object.entries(row)) {
@@ -115,7 +137,7 @@ const liftSourceId = (row, options = {}) => {
115
137
  body[key] = value;
116
138
  }
117
139
  }
118
- return { ...body, _id: id };
140
+ return normalizeSourceDocument({ ...body, _id: id });
119
141
  };
120
142
  const isSourceDue = (refresh, lastPolledMs, nowMs) => {
121
143
  if (refresh === "manual") {
@@ -157,7 +179,12 @@ const pullExternalSourceIncrementalTick = async (sql, writer, client, table, sou
157
179
  let applied;
158
180
  if (state.watermark === null || reconcileDue) {
159
181
  slice = await pullAndLift(client, source.query, tenantParameters, source);
160
- ({ applied } = await runExternalSourceTick(sql, writer, slice.documents, { columns: source.columns, table }));
182
+ const { softDeleteColumn } = source;
183
+ const documents = softDeleteColumn ? slice.documents.filter((_document, index) => {
184
+ const row = slice.rows[index];
185
+ return !(row && isSoftDeleted(row, softDeleteColumn));
186
+ }) : slice.documents;
187
+ ({ applied } = await runExternalSourceTick(sql, writer, documents, { columns: source.columns, table }));
161
188
  } else {
162
189
  slice = await pullAndLift(client, cursor.query, [...tenantParameters, deserializeCursor(state.watermark)], source);
163
190
  const { softDeleteColumn } = source;
@@ -176,8 +203,15 @@ const pullExternalSourceIncrementalTick = async (sql, writer, client, table, sou
176
203
  `external-source: table "${table}" (mode "incremental") pulled ${String(slice.rows.length)} rows but none carry the cursor column "${cursor.column}" — the seed \`query\` must project it (matching \`cursor.query\`'s alias), or the watermark can never advance.`
177
204
  );
178
205
  }
206
+ const noRowCarriesCursor = slice.rows.length > 0 && slice.rows.every((row) => row[cursor.column] === null || row[cursor.column] === void 0);
207
+ if (!fullPull && noRowCarriesCursor) {
208
+ throw new LunoraError(
209
+ "INTERNAL",
210
+ `external-source: table "${table}" (mode "incremental") pulled ${String(slice.rows.length)} rows but none carry the cursor column "${cursor.column}" — \`cursor.query\` must project it (matching the seed \`query\`'s alias), or the watermark can never advance and every tick re-pulls the same stranded slice.`
211
+ );
212
+ }
179
213
  writeSourceCursor(sql, table, shardKey, { lastReconcileMs: fullPull ? nowMs : state.lastReconcileMs, watermark });
180
214
  return { applied };
181
215
  };
182
216
 
183
- export { isSoftDeleted, isSourceDue, liftSourceId, pullExternalSourceIncrementalTick, pullExternalSourceTick };
217
+ export { isSoftDeleted, isSourceDue, liftSourceId, normalizeSourceValue, pullExternalSourceIncrementalTick, pullExternalSourceTick };
@@ -1,4 +1,4 @@
1
- import { applyCdcChanges } from './CDC_LOG_TABLE-DjJEHiM2.mjs';
1
+ import { applyCdcChanges } from './CDC_LOG_TABLE-uwOJxJJZ.mjs';
2
2
  import { s as selectShapeRows } from './ctx-db-shapes-Cz9dHyh1.mjs';
3
3
  import { diffExternalSource, projectExternalSourceRow } from './diffExternalSource-CovHfdyo.mjs';
4
4
  import { stableStringify } from './stableStringify-mC40mZts.mjs';
@@ -15,7 +15,10 @@ const materializeExternalRowsIncremental = async (writer, pulled, options) => {
15
15
  const value = projectExternalSourceRow(source, columns);
16
16
  const id = String(value._id);
17
17
  if (deletedIds?.has(id)) {
18
- changes.push({ id, op: "delete", seq: 0, table, ts: 0 });
18
+ const existing = await writer.get(id, table);
19
+ if (existing) {
20
+ changes.push({ id, op: "delete", seq: 0, table, ts: 0 });
21
+ }
19
22
  continue;
20
23
  }
21
24
  const stored = await writer.get(id, table);
@@ -1,6 +1,6 @@
1
1
  import { sql } from 'drizzle-orm';
2
2
  import { aggregateTableName } from './aggregateTableName-CxNqY1Sl.mjs';
3
- import { migrateCdcLog, migrateCdcMeta } from './CDC_LOG_TABLE-DjJEHiM2.mjs';
3
+ import { migrateCdcLog, migrateCdcMeta } from './CDC_LOG_TABLE-uwOJxJJZ.mjs';
4
4
  import { m as migrateClientWatermark, a as migrateIdempotency, b as migrateGlobalShapeSnapshot } from './ctx-db-idempotency-BdcNpvY4.mjs';
5
5
  import { r as runDrizzle } from './do-exec-5eQy5cEi.mjs';
6
6
  import { D as DOC_COLUMN, j as jsonPathSql, c as createIndexSql, t as tableColumns, i as isFtsAvailable, A as AGG_KEY, a as AGG_VALUE, b as AGG_COUNT } from './do-sql-BCHCWtrD.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/do",
3
- "version": "1.0.0-alpha.32",
3
+ "version": "1.0.0-alpha.33",
4
4
  "description": "Lunora Durable Objects: ShardDO (SQLite, OCC, hibernated WebSocket subscriptions) and SessionDO",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "@lunora/errors": "1.0.0-alpha.5",
50
- "@lunora/fingerprint": "1.0.0-alpha.2",
50
+ "@lunora/fingerprint": "1.0.0-alpha.3",
51
51
  "@visulima/redact": "3.0.0",
52
52
  "drizzle-orm": "^0.45.2"
53
53
  },