@lunora/do 1.0.0-alpha.98 → 1.0.0-alpha.99
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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { SchemaLike, DatabaseWriterLike, CrossShardReadArgs, CdcChange, FilterClause, ExportRow, MigrationDirection, ReactiveCache, ReactiveCacheOptions, ShardSocketLike, TransactionHeadroomTracker, LifecycleDispatchInfo, MigrationRunResult, TableIndexInfo, ColumnMeta, AdvisoryFinding, AdvisorProcedure, RlsPoliciesResult, MaskPoliciesResult, StorageRulesResult, StudioFeaturesResult, FlagsResult, SubscriptionIdentity, QueuesResult, WorkflowsResult, ImportShardResult, ShardRankPageResult, SubscriptionQuery, ShapeSubscriptionQuery, MutationDelta, KeyRange, ResolvedShape, ShapeRow, TtlSweepSpec, TransactionLimits, IndexKeyEntry, SqlExec } from '@lunora/shard-engine';
|
|
2
|
-
export { type AdvisorProcedure, type AdvisoryFinding, type AggregateIndexDefinitionLike, type DataMigrationLike, type DatabaseWriterLike, type ExportRow, type ExternalSourceLike, type FlagsResult, type ImportShardResult, type KeyRange, type MaskPoliciesResult, type MigrationRunResult, type MutationDelta, type QueuesResult, REPROJECTION_MIGRATION_PREFIX, type RankIndexDefinitionLike, type RlsPoliciesResult, type SchedulerLike, type SchemaLike, type ShardRankPageResult, type SourceClientLike, type SqlExec, type StorageRulesResult, type StudioFeaturesResult, type SystemReaderStorageLike, type TransactionHeadroomTracker, type ValidatorLike, type WhereInput, type WorkflowsResult, type WriteHook, applyCdcChanges, assertShapeShardable, buildReprojectionMigration, clearMemoryTables, countLegacyRows, createReadFootprint, createShardCtxDb, exportShardRows, importShardRows, isSourceDue, pullExternalSourceIncrementalTick, pullExternalSourceTick, reprojectionMigrationId, reprojectionTables, runDataMigration, runShardMigrations, subscriptionListDeltas } from '@lunora/shard-engine';
|
|
1
|
+
import { SchemaLike, DatabaseWriterLike, CrossShardReadArgs, CdcChange, FilterClause, ExportRow, MigrationDirection, ReactiveCache, ShapeProbeCounters, GlobalPollCounters, ReactiveCacheOptions, ShardSocketLike, TransactionHeadroomTracker, LifecycleDispatchInfo, MigrationRunResult, SearchBackfillProgress, TableIndexInfo, ColumnMeta, AdvisoryFinding, AdvisorProcedure, RlsPoliciesResult, MaskPoliciesResult, StorageRulesResult, StudioFeaturesResult, FlagsResult, SubscriptionIdentity, QueuesResult, WorkflowsResult, ImportShardResult, ShardRankPageResult, SubscriptionQuery, ShapeSubscriptionQuery, MutationDelta, KeyRange, ResolvedShape, ShapeRow, TtlSweepSpec, TransactionLimits, IndexKeyEntry, SqlExec, CdcChangeKey } from '@lunora/shard-engine';
|
|
2
|
+
export { type AdvisorProcedure, type AdvisoryFinding, type AggregateIndexDefinitionLike, type DataMigrationLike, type DatabaseWriterLike, type ExportRow, type ExternalSourceLike, type FlagsResult, type ImportShardResult, type KeyRange, type MaskPoliciesResult, type MigrationRunResult, type MutationDelta, type QueuesResult, REPROJECTION_MIGRATION_PREFIX, type RankIndexDefinitionLike, type RlsPoliciesResult, type SchedulerLike, type SchemaLike, type SearchBackfillProgress, type ShardRankPageResult, type SourceClientLike, type SqlExec, type StorageRulesResult, type StudioFeaturesResult, type SystemReaderStorageLike, type TransactionHeadroomTracker, UNVOUCHABLE_DEP, type ValidatorLike, type WhereInput, type WorkflowsResult, type WriteHook, applyCdcChanges, assertShapeShardable, backfillSearchIndexes, buildReprojectionMigration, clearMemoryTables, countLegacyRows, createReadFootprint, createShardCtxDb, exportShardRows, importShardRows, isSourceDue, markUnvouchableReads, pullExternalSourceIncrementalTick, pullExternalSourceTick, reprojectionMigrationId, reprojectionTables, runDataMigration, runShardMigrations, subscriptionListDeltas } from '@lunora/shard-engine';
|
|
3
3
|
import { DatabaseInstrumentation, MetricHistoryOptions, LogEventInput, ContextLogLevel, TraceAnchor, ContextTracer, ContextFetch, ContextMetrics } from '@lunora/observability';
|
|
4
4
|
import { DrizzleSqliteDODatabase } from 'drizzle-orm/durable-sqlite';
|
|
5
5
|
export { type ShardPlatform, type WorkerPlatform, type WorkerPlatformOptions, createShardAlarms, createShardDirectory, createShardHost, createShardKvStore, createShardPlatform, createSocketHost, createWorkerPlatform } from '@lunora/platform-cloudflare';
|
|
@@ -842,6 +842,14 @@ declare abstract class ShardDO {
|
|
|
842
842
|
* never trip it.
|
|
843
843
|
*/
|
|
844
844
|
protected static readonly GLOBAL_SHAPE_MAX_ROWS = 5e4;
|
|
845
|
+
/**
|
|
846
|
+
* How long a `.global()` poll may go without an unconditional membership
|
|
847
|
+
* pass. Between resyncs the poll trusts the global changelog and skips
|
|
848
|
+
* unchanged tables; a resync re-reads regardless, so a write that left no
|
|
849
|
+
* changelog row (another system writing the same database) still surfaces
|
|
850
|
+
* within this window instead of never.
|
|
851
|
+
*/
|
|
852
|
+
protected static readonly GLOBAL_SHAPE_RESYNC_MS = 3e4;
|
|
845
853
|
/**
|
|
846
854
|
* Per-socket whisper-topic cap. Topic membership rides the same hibernation
|
|
847
855
|
* attachment as `subs`, so bound it for the same reason — a runaway
|
|
@@ -903,6 +911,28 @@ declare abstract class ShardDO {
|
|
|
903
911
|
* the query on the first call, just like it does today.
|
|
904
912
|
*/
|
|
905
913
|
protected readonly reactiveCache: ReactiveCache | undefined;
|
|
914
|
+
/**
|
|
915
|
+
* Running read tallies for the shape-poke path, surfaced next to
|
|
916
|
+
* {@link ShardDO.fanout} on `getFanoutMetrics`. `run` counts the reads this
|
|
917
|
+
* instance issued to SQLite; `served` counts the ones the per-flush
|
|
918
|
+
* {@link ShapeDiffCache} answered because another socket had already asked the
|
|
919
|
+
* identical question. BOTH halves of the diff are counted — the changed-key
|
|
920
|
+
* scan keyed by `(table, op range)` and the membership probe keyed by
|
|
921
|
+
* `(effectiveWhere, that same range)` — so the reported sharing rate covers
|
|
922
|
+
* the work the cache actually does rather than half of it.
|
|
923
|
+
*
|
|
924
|
+
* Without the split the sharing is invisible: a flush that collapsed a
|
|
925
|
+
* hundred reads into one looks exactly like a flush that only ever had one
|
|
926
|
+
* shape.
|
|
927
|
+
*/
|
|
928
|
+
protected shapeProbe: ShapeProbeCounters;
|
|
929
|
+
/**
|
|
930
|
+
* Running `.global()` poll tallies, reported alongside {@link ShardDO.shapeProbe}.
|
|
931
|
+
* `run` counts membership drains actually issued to the global backend;
|
|
932
|
+
* `served` counts the (socket, shape) pairs a tick skipped because the global
|
|
933
|
+
* changelog proved their table had not moved.
|
|
934
|
+
*/
|
|
935
|
+
protected globalPoll: GlobalPollCounters;
|
|
906
936
|
/**
|
|
907
937
|
* The host-neutral engine runner. `fetch` and `alarm` delegate through it, so
|
|
908
938
|
* the dispatch entry points name a platform contract rather than a Durable
|
|
@@ -1095,6 +1125,13 @@ declare abstract class ShardDO {
|
|
|
1095
1125
|
* hot path without needing a separate alarm/cron.
|
|
1096
1126
|
*/
|
|
1097
1127
|
private lastIdempotencyTrimAt;
|
|
1128
|
+
/**
|
|
1129
|
+
* Wall-clock millis of the last `__cdc_log` retention sweep on this warm
|
|
1130
|
+
* instance, throttling {@link ShardDO.sweepCdcRetention} to at most once per
|
|
1131
|
+
* {@link CDC_SWEEP_INTERVAL_MS}. In-memory like its `__idempotency` twin, so
|
|
1132
|
+
* a fresh instance sweeps on its first coalesced flush.
|
|
1133
|
+
*/
|
|
1134
|
+
private lastCdcSweepAt;
|
|
1098
1135
|
/**
|
|
1099
1136
|
* Per-request identity envelope forwarded from the runtime via the
|
|
1100
1137
|
* `x-lunora-identity` JSON header. Stores claims like `email`,
|
|
@@ -1235,6 +1272,32 @@ declare abstract class ShardDO {
|
|
|
1235
1272
|
* threshold is grounded in real numbers, with no behavior change.
|
|
1236
1273
|
*/
|
|
1237
1274
|
private readonly fanout;
|
|
1275
|
+
/**
|
|
1276
|
+
* The `.global()` changelog position the last poll tick observed, or
|
|
1277
|
+
* `undefined` on a cold instance. In-memory on purpose: losing it costs one
|
|
1278
|
+
* full re-read pass, which is the safe direction — a stale persisted cursor
|
|
1279
|
+
* would let a tick skip a table that HAD changed.
|
|
1280
|
+
*/
|
|
1281
|
+
private globalPollCursor;
|
|
1282
|
+
/**
|
|
1283
|
+
* Set when a shape failed to settle in the last poll tick, forcing the next one
|
|
1284
|
+
* to read every shape unconditionally. See {@link GlobalPollTick.resyncRequested}
|
|
1285
|
+
* for why a shared cursor leaves no cheaper recovery.
|
|
1286
|
+
*/
|
|
1287
|
+
private globalResyncRequested;
|
|
1288
|
+
/**
|
|
1289
|
+
* Whether this wake has already re-minted the epoch to seal a rolled-back
|
|
1290
|
+
* timeline. In-memory on purpose — see {@link ShardDO.sealForkedTimeline} for
|
|
1291
|
+
* why the seal is capped at one per wake, and why losing the flag on eviction
|
|
1292
|
+
* is the correct direction.
|
|
1293
|
+
*/
|
|
1294
|
+
private forkSealed;
|
|
1295
|
+
/**
|
|
1296
|
+
* Wall-clock millis of the last unconditional `.global()` membership pass.
|
|
1297
|
+
* Bounds how long a write this deployment cannot see in its own changelog —
|
|
1298
|
+
* an out-of-band writer against the global database — can go unnoticed.
|
|
1299
|
+
*/
|
|
1300
|
+
private lastGlobalResyncAt;
|
|
1238
1301
|
/**
|
|
1239
1302
|
* The runtime's Durable Object namespace binding name (e.g. `"SHARD"`),
|
|
1240
1303
|
* forwarded as `x-lunora-shard-binding` on every request so a DO can address
|
|
@@ -1384,6 +1447,18 @@ declare abstract class ShardDO {
|
|
|
1384
1447
|
* attribute only SELECT result sizes as `rowsRead`.
|
|
1385
1448
|
*/
|
|
1386
1449
|
private currentStmtSamples;
|
|
1450
|
+
/**
|
|
1451
|
+
* The instrumented `sql` proxy built for {@link ShardDO.currentStmtSamples},
|
|
1452
|
+
* and the samples map it was built for.
|
|
1453
|
+
*
|
|
1454
|
+
* `get sql()` is read on essentially every storage call, and a fresh `Proxy`
|
|
1455
|
+
* per read is not only an allocation: the resume path memoizes this shard's
|
|
1456
|
+
* table catalog in a `WeakMap` keyed by the handle, so a new object each read
|
|
1457
|
+
* meant that memo never once hit during a dispatch — it re-scanned
|
|
1458
|
+
* `sqlite_master` every time, which is the 18% of `evaluateResume` the memo
|
|
1459
|
+
* exists to remove.
|
|
1460
|
+
*/
|
|
1461
|
+
private instrumentedSql;
|
|
1387
1462
|
/**
|
|
1388
1463
|
* Set when `currentStmtSamples` hit {@link MAX_STMT_SAMPLES_PER_DISPATCH}
|
|
1389
1464
|
* distinct statements and a brand-new shape was dropped this dispatch.
|
|
@@ -1701,6 +1776,20 @@ declare abstract class ShardDO {
|
|
|
1701
1776
|
* look the migration up and invoke `runDataMigration`.
|
|
1702
1777
|
*/
|
|
1703
1778
|
protected runShardDataMigration(args: RunShardMigrationArgs): Promise<MigrationRunResult>;
|
|
1779
|
+
/**
|
|
1780
|
+
* Index the rows that predate a `.searchIndex()` into its companion, a
|
|
1781
|
+
* bounded number of pages per call, and report whether anything is left.
|
|
1782
|
+
*
|
|
1783
|
+
* This is the exit from `staged: true`. A staged index is skipped by every
|
|
1784
|
+
* migration pass by design — the option exists for tables too large to walk
|
|
1785
|
+
* during a cold start — so without an explicit run its pre-existing rows are
|
|
1786
|
+
* unsearchable forever. The base class can't reach the project's generated
|
|
1787
|
+
* `schema`, so it reports the op unsupported; the codegen subclass overrides
|
|
1788
|
+
* it to call `backfillSearchIndexes`.
|
|
1789
|
+
*/
|
|
1790
|
+
protected runShardSearchBackfill(_options: {
|
|
1791
|
+
maxPages?: number;
|
|
1792
|
+
}): SearchBackfillProgress;
|
|
1704
1793
|
/**
|
|
1705
1794
|
* Lazily provision the shard's physical tables before an operation that
|
|
1706
1795
|
* depends on them existing. The base class has no `schema.ts`, so it does
|
|
@@ -1968,19 +2057,64 @@ declare abstract class ShardDO {
|
|
|
1968
2057
|
* the wire byte-identical to the pre-epoch format for non-CDC apps.
|
|
1969
2058
|
*/
|
|
1970
2059
|
protected currentCdcEpoch(): string | undefined;
|
|
2060
|
+
/**
|
|
2061
|
+
* Mint a fresh CDC epoch because a resume claim just proved this shard's
|
|
2062
|
+
* changelog forked under it, and return the new value for the outgoing frame.
|
|
2063
|
+
*
|
|
2064
|
+
* The trigger is a client presenting `sinceEpoch === epoch` with `sinceSeq >
|
|
2065
|
+
* cursor`: it holds a cursor this shard once issued on THIS timeline and can
|
|
2066
|
+
* no longer account for. Only a rollback produces that — in practice a native
|
|
2067
|
+
* PITR restore, which is armed in {@link handlePitrAdminOp}.
|
|
2068
|
+
*
|
|
2069
|
+
* **Why the signal has to come from a client.** A restore reverts the whole
|
|
2070
|
+
* SQLite database, `__cdc_meta` included, so the proactive bump `pitrRestore`
|
|
2071
|
+
* performs is rolled back along with everything else — the epoch cannot
|
|
2072
|
+
* detect the one event it exists for. Nothing durable inside a SQLite-backed
|
|
2073
|
+
* Durable Object escapes that: the KV half of `state.storage` is the same
|
|
2074
|
+
* database, and an alarm is a row in it. The only record of the pre-restore
|
|
2075
|
+
* timeline that the restore cannot reach is the cursor each CLIENT cached, so
|
|
2076
|
+
* that is what this reads. Turning one client's refusal into a shard-wide
|
|
2077
|
+
* epoch bump is what extends the protection to clients that reconnect later,
|
|
2078
|
+
* after post-restore writes have climbed the AUTOINCREMENT back past their
|
|
2079
|
+
* own `sinceSeq` and the `sinceSeq > cursor` guard no longer fires for them.
|
|
2080
|
+
*
|
|
2081
|
+
* **What it detects.** Any rollback under a live subscriber base, promptly: a
|
|
2082
|
+
* restore restarts the object (`ctx.abort()`, or the eviction that lets a
|
|
2083
|
+
* deferred restore apply), which drops every hibernated socket, so the whole
|
|
2084
|
+
* subscriber set reconnects with pre-restore cursors while the restored
|
|
2085
|
+
* cursor is still low. The first of them seals the fork for the rest.
|
|
2086
|
+
*
|
|
2087
|
+
* **What it cannot detect.** A rollback on a shard whose clients ALL stay
|
|
2088
|
+
* offline until the cursor has climbed back past their cursors — nobody is
|
|
2089
|
+
* left to present the proof. A shard with no subscribers at all is the
|
|
2090
|
+
* degenerate case of that, and is also the case where nothing is stale.
|
|
2091
|
+
*
|
|
2092
|
+
* **On trusting `sinceSeq`.** It is client-supplied, so a caller that already
|
|
2093
|
+
* knows this shard's epoch can force a bump, and a bump is shard-wide: every
|
|
2094
|
+
* subscriber takes a full snapshot instead of a resume on its NEXT reconnect.
|
|
2095
|
+
* The cost is bounded (the once-per-wake latch below) and deferred — nothing
|
|
2096
|
+
* is pushed and no live subscription is interrupted — and it stays strictly
|
|
2097
|
+
* less than what the same caller can already spend by subscribing without a
|
|
2098
|
+
* `sinceSeq` at all.
|
|
2099
|
+
* @returns the freshly minted epoch, to stamp on the frame this verdict produces
|
|
2100
|
+
*/
|
|
2101
|
+
protected sealForkedTimeline(): string;
|
|
1971
2102
|
/**
|
|
1972
2103
|
* Decide whether a reconnecting subscription can resume from `sinceSeq`
|
|
1973
2104
|
* without a full snapshot. Returns the current high-watermark `cursor` plus
|
|
1974
2105
|
* a `resumable` verdict.
|
|
1975
2106
|
*
|
|
1976
|
-
* `resumable: true` means `sinceSeq` is within the CDC retention window
|
|
1977
|
-
*
|
|
1978
|
-
*
|
|
1979
|
-
*
|
|
2107
|
+
* `resumable: true` means `sinceSeq` is within the CDC retention window,
|
|
2108
|
+
* every entry in the query's `readSet` is one the changelog can speak for,
|
|
2109
|
+
* and none of them changed in `(sinceSeq, cursor]` — the client's cached
|
|
2110
|
+
* value is still current, so the caller emits a lightweight `resume` frame
|
|
2111
|
+
* instead of re-shipping the snapshot.
|
|
1980
2112
|
*
|
|
1981
|
-
* `resumable: false` means
|
|
2113
|
+
* `resumable: false` means the log was compacted past `sinceSeq` (a
|
|
1982
2114
|
* retention gap), a read table changed (the client needs the fresh value),
|
|
1983
|
-
*
|
|
2115
|
+
* the read-set contains something the changelog cannot vouch for (see
|
|
2116
|
+
* {@link cdcCanVouchFor}), or CDC is off — the caller falls back to the
|
|
2117
|
+
* full-snapshot seed.
|
|
1984
2118
|
*/
|
|
1985
2119
|
protected evaluateResume(sinceSeq: number, readSet: Set<string>, sinceEpoch?: string): {
|
|
1986
2120
|
cursor: number | undefined;
|
|
@@ -2989,8 +3123,8 @@ declare abstract class ShardDO {
|
|
|
2989
3123
|
private handleAdminRpc;
|
|
2990
3124
|
/**
|
|
2991
3125
|
* Dispatch the side-effecting / non-read admin ops that `handleAdminRpc`
|
|
2992
|
-
* doesn't handle inline: the auth-event + mail-capture writes
|
|
2993
|
-
* PITR ops. Returns the op's `Response`, or `undefined` when `functionPath`
|
|
3126
|
+
* doesn't handle inline: the auth-event + mail-capture writes, the search
|
|
3127
|
+
* backfill, and the native PITR ops. Returns the op's `Response`, or `undefined` when `functionPath`
|
|
2994
3128
|
* isn't one of these (so the caller answers 404). Kept out of
|
|
2995
3129
|
* `handleAdminRpc` to hold that dispatcher under the complexity budget,
|
|
2996
3130
|
* mirroring `handlePitrAdminOp`.
|
|
@@ -3031,6 +3165,18 @@ declare abstract class ShardDO {
|
|
|
3031
3165
|
* untag); a missing or malformed value is a 400 rather than a silent no-op.
|
|
3032
3166
|
*/
|
|
3033
3167
|
private parseIssueTriagePatch;
|
|
3168
|
+
/**
|
|
3169
|
+
* `__lunora_admin__:backfillSearch` — index the rows that predate this
|
|
3170
|
+
* shard's `.searchIndex()` declarations, `maxPages` pages at a time.
|
|
3171
|
+
*
|
|
3172
|
+
* Bounded rather than run-to-completion because a DO request has a CPU and
|
|
3173
|
+
* wall-clock budget, and the tables `staged: true` exists for are precisely
|
|
3174
|
+
* the ones a single unbounded walk cannot finish. Progress is durable, so an
|
|
3175
|
+
* operator drives it with repeated calls until `done` — `lunora run
|
|
3176
|
+
* '__lunora_admin__:backfillSearch' --args '{"maxPages":20}'` needs no new
|
|
3177
|
+
* CLI surface. Admin-gated by `handleAdminRpc`'s caller.
|
|
3178
|
+
*/
|
|
3179
|
+
private handleBackfillSearch;
|
|
3034
3180
|
/**
|
|
3035
3181
|
* Record one app-level auth attempt for the auth-failure SLO (PLAN3 §2.3).
|
|
3036
3182
|
* The worker calls this fire-and-forget (via `waitUntil`) after a top-level
|
|
@@ -3247,6 +3393,8 @@ declare abstract class ShardDO {
|
|
|
3247
3393
|
* is still an operator asking the database a question.
|
|
3248
3394
|
*/
|
|
3249
3395
|
private handleGenerateSql;
|
|
3396
|
+
/** The single-shape admin writes (decode args → Response), keyed by function path. */
|
|
3397
|
+
private simpleAdminHandlers;
|
|
3250
3398
|
/** The AI-assistant admin writes, keyed by function path. */
|
|
3251
3399
|
private aiAdminHandlers;
|
|
3252
3400
|
/**
|
|
@@ -3858,7 +4006,12 @@ declare abstract class ShardDO {
|
|
|
3858
4006
|
* epoch, its checkpoint doesn't run ahead of ours, and the log still covers it;
|
|
3859
4007
|
* else a full re-seed. A fully-compacted log only proves "nothing missed" when
|
|
3860
4008
|
* the client is already at `cursor`.
|
|
3861
|
-
*
|
|
4009
|
+
*
|
|
4010
|
+
* `reset` is the inverse of the resume decision and MUST ride the wire: the
|
|
4011
|
+
* re-seed branch returns the whole membership as inserts, which can only ever
|
|
4012
|
+
* add rows, so a client that splices it onto a stale view keeps every row that
|
|
4013
|
+
* left the shape while it was disconnected. Callers stamp it on the poke part.
|
|
4014
|
+
* @returns the cursor/epoch, the resume base (`baseCheckpoint`), whether this is a full re-seed, and the membership patch
|
|
3862
4015
|
*/
|
|
3863
4016
|
private computeOpLogShapeSeed;
|
|
3864
4017
|
/**
|
|
@@ -3872,6 +4025,95 @@ declare abstract class ShardDO {
|
|
|
3872
4025
|
* carrying a part per changed shape. No-op when no socket holds a shape.
|
|
3873
4026
|
*/
|
|
3874
4027
|
private pokeShapeSubscribers;
|
|
4028
|
+
/**
|
|
4029
|
+
* Enforce the configured `__cdc_log` retention, at most once per interval per
|
|
4030
|
+
* warm instance. Two independent, independently-configured levels:
|
|
4031
|
+
*
|
|
4032
|
+
* **Payload compaction** (`LUNORA_CDC_PAYLOAD_RETENTION`, in rows) nulls the
|
|
4033
|
+
* post-images of older entries while keeping their `(seq, table, id, op)`
|
|
4034
|
+
* keys. Post-images are essentially all of the log's bytes, and since the
|
|
4035
|
+
* shape diff reads its values from the table rather than the log
|
|
4036
|
+
* (`selectShapeMembers`), dropping them costs a live subscriber
|
|
4037
|
+
* nothing — a client past the payload floor still gets an exact key-level
|
|
4038
|
+
* delta instead of a full re-seed.
|
|
4039
|
+
*
|
|
4040
|
+
* **Row deletion** (`LUNORA_CDC_LOG_RETENTION`, in rows) drops the entries
|
|
4041
|
+
* outright. This is the level that ends resumability for anyone below it:
|
|
4042
|
+
* `minCdcSeq` then reports a floor above their cursor and they re-seed.
|
|
4043
|
+
*
|
|
4044
|
+
* Both levels are enforced on the READ paths, not merely intended by the
|
|
4045
|
+
* sweep — `evaluateResume` and `computeOpLogShapeSeed` gate on `minCdcSeq`,
|
|
4046
|
+
* and {@link ShardDO.runShardCdcSync} refuses a page below either floor
|
|
4047
|
+
* (`CDC_LOG_TRIMMED` / `CDC_PAYLOAD_COMPACTED`). That matters because the
|
|
4048
|
+
* failure this sweep can cause is silent by nature: a consumer handed the
|
|
4049
|
+
* surviving tail with an advanced cursor has no way to notice a range went
|
|
4050
|
+
* missing, so every path that can serve one has to refuse instead.
|
|
4051
|
+
*
|
|
4052
|
+
* **Both are opt-in, and that is a deliberate answer rather than caution.**
|
|
4053
|
+
* The log's in-shard consumers record durable cursors this sweep can read
|
|
4054
|
+
* ({@link ShardDO.retentionFloor}), but its out-of-shard consumers do not: a
|
|
4055
|
+
* warehouse connector holds an opaque cursor token issued by the Worker, and
|
|
4056
|
+
* nothing in this shard knows where it is. Trimming to a floor computed only
|
|
4057
|
+
* from what SQLite can see would silently drop rows a connector had not read
|
|
4058
|
+
* — so a deployment that wants retention states the window it can afford, and
|
|
4059
|
+
* gets a sweep that additionally never crosses the in-shard floor. A shard
|
|
4060
|
+
* that configures neither behaves exactly as before.
|
|
4061
|
+
*
|
|
4062
|
+
* Best-effort throughout: a stub `sql` handle or a shard without CDC is a
|
|
4063
|
+
* no-op, and a failure here must never surface on a write path whose data
|
|
4064
|
+
* already committed.
|
|
4065
|
+
*/
|
|
4066
|
+
private sweepCdcRetention;
|
|
4067
|
+
/**
|
|
4068
|
+
* The highest `seq` this sweep may compact or delete through without stranding
|
|
4069
|
+
* an in-shard consumer: the lowest cursor any durable local consumer has
|
|
4070
|
+
* durably reached, or the log's head when there is none.
|
|
4071
|
+
*
|
|
4072
|
+
* The direction is what matters. Every input can only pull the floor DOWN, and
|
|
4073
|
+
* a consumer whose cursor cannot be read contributes nothing rather than a
|
|
4074
|
+
* guess — a floor that is too low leaves rows around for one more sweep, while
|
|
4075
|
+
* a floor that is too high deletes a range a live subscription still has to be
|
|
4076
|
+
* told about.
|
|
4077
|
+
*
|
|
4078
|
+
* Two in-shard consumers, and they are tracked in different places. Local
|
|
4079
|
+
* sockets record `__shape_poke_cursor` per `(connection, subscription)` on
|
|
4080
|
+
* every delivered poke, so that table carries their position. **Relayed
|
|
4081
|
+
* subscribers are not in it**: a relay's cohort frontier and its per-socket
|
|
4082
|
+
* proxies are the owner's shape registry (`__lunora_relay_shapes`, read back
|
|
4083
|
+
* through `RelayLink.minShapeCursor`). Reading only the local table would
|
|
4084
|
+
* therefore see a fully relayed shard — the high-fan-out case, i.e. exactly
|
|
4085
|
+
* the shard an operator turns retention on for — as having no subscribers,
|
|
4086
|
+
* and delete the rows the next relayed diff had to read. Both are folded in
|
|
4087
|
+
* here.
|
|
4088
|
+
*
|
|
4089
|
+
* **The floor is each consumer's `cursor`, not its `delivered`** (see
|
|
4090
|
+
* {@link ShapeMemo.delivered}), and the two differ: a client's own position is
|
|
4091
|
+
* `delivered`, which can sit BELOW the `cursor` this floor is computed from.
|
|
4092
|
+
* Trimming to the higher of the two is nonetheless safe, and only for one
|
|
4093
|
+
* reason: `cursor` runs ahead of `delivered` exclusively across ranges where
|
|
4094
|
+
* that shape's table saw no change at all — that is what an empty diff means,
|
|
4095
|
+
* and empty diffs are the only thing that advances `cursor` alone. So the rows
|
|
4096
|
+
* this deletes in `(delivered, cursor]` are never rows that shape still owes
|
|
4097
|
+
* its client. Change what advances `cursor` and this stops holding.
|
|
4098
|
+
*
|
|
4099
|
+
* **Known ceiling: a relayed cohort on a quiet table pins this floor.** A
|
|
4100
|
+
* scalar MIN over every consumer is only as good as the slowest consumer's
|
|
4101
|
+
* ability to advance, and the relay tier's cannot: `OwnerRelay.buildShapePoke`
|
|
4102
|
+
* must leave a cohort's cursor where the last DELIVERED poke reached, because
|
|
4103
|
+
* advancing it without a poke puts every relay socket's memo below the next
|
|
4104
|
+
* poke's `fromCursor` and freezes them (the failure `relay-hub.test.ts` pins).
|
|
4105
|
+
* Local sockets have no such constraint — the shard computes their diffs from
|
|
4106
|
+
* its own memo — so `collectShapePokeParts` advances them on every flush. The
|
|
4107
|
+
* asymmetry means a shard whose shapes are all relayed can still see retention
|
|
4108
|
+
* do nothing.
|
|
4109
|
+
*
|
|
4110
|
+
* The upgrade is a floor PER TABLE rather than one scalar: `readCdcChangeKeys`
|
|
4111
|
+
* already filters by table, so a cohort watching a quiet table never needs the
|
|
4112
|
+
* busy table's rows and should not be holding them. That is a real change to
|
|
4113
|
+
* how the sweep is expressed, not a tweak here, so it waits for a deployment
|
|
4114
|
+
* that needs it.
|
|
4115
|
+
*/
|
|
4116
|
+
private retentionFloor;
|
|
3875
4117
|
/**
|
|
3876
4118
|
* Diff every op-log-backed shape a socket holds against this flush, splitting
|
|
3877
4119
|
* the results into the poke parts to send and the per-shape memo advances. A
|
|
@@ -3884,37 +4126,38 @@ declare abstract class ShardDO {
|
|
|
3884
4126
|
*/
|
|
3885
4127
|
private collectShapePokeParts;
|
|
3886
4128
|
/**
|
|
3887
|
-
*
|
|
3888
|
-
*
|
|
3889
|
-
*
|
|
3890
|
-
*
|
|
3891
|
-
*
|
|
3892
|
-
*
|
|
3893
|
-
*
|
|
3894
|
-
*
|
|
4129
|
+
* Read the changed row keys for a shape diff. A thin protected seam over
|
|
4130
|
+
* {@link readCdcChangeKeys}, kept for the one thing nothing else observes:
|
|
4131
|
+
* the `sinceSeq` each diff resumed FROM. Which baseline a poke picked — the
|
|
4132
|
+
* durable memo, the attachment's subscribe-time cursor, or a clamped one
|
|
4133
|
+
* after a PITR rollback — is the assertion in three tests, and it is not
|
|
4134
|
+
* recoverable from the outside.
|
|
4135
|
+
*
|
|
4136
|
+
* It is NOT the read counter. `ShapeDiffCache.probesRun`/`probesServed` count
|
|
4137
|
+
* the reads the per-flush memo collapses, and that is what the sharing tests
|
|
4138
|
+
* assert against.
|
|
3895
4139
|
*/
|
|
3896
|
-
|
|
4140
|
+
protected readShapeCdcKeys(sql: SqlExec, table: string, sinceSeq: number, upTo: number): CdcChangeKey[];
|
|
3897
4141
|
/**
|
|
3898
|
-
*
|
|
3899
|
-
*
|
|
3900
|
-
*
|
|
3901
|
-
*
|
|
4142
|
+
* One relayed shape's diff, on its own cache.
|
|
4143
|
+
*
|
|
4144
|
+
* The relay tier computes one delta per cohort or proxy rather than per
|
|
4145
|
+
* socket, so there is nothing to share WITHIN a call — the cache exists
|
|
4146
|
+
* because the pipeline requires one. Its counters are still folded back into
|
|
4147
|
+
* {@link ShardDO.shapeProbe}, because they are reads this instance issued:
|
|
4148
|
+
* discarding them would under-report exactly the shards the fan-out panel
|
|
4149
|
+
* exists for, since a shard is relayed precisely when its fan-out is large.
|
|
3902
4150
|
*/
|
|
3903
|
-
|
|
3904
|
-
changes: CdcChange[];
|
|
3905
|
-
cursor: number;
|
|
3906
|
-
};
|
|
4151
|
+
private diffRelayedShape;
|
|
3907
4152
|
/**
|
|
3908
|
-
*
|
|
3909
|
-
*
|
|
3910
|
-
*
|
|
3911
|
-
*
|
|
3912
|
-
*
|
|
3913
|
-
*
|
|
3914
|
-
* → `delete(key)` (a delete carries no post-image, so membership is
|
|
3915
|
-
* unknowable from the op alone — the client no-ops an unknown key).
|
|
4153
|
+
* This shard's shape diff: {@link buildShapeDiff} with the changelog read
|
|
4154
|
+
* routed through {@link ShardDO.readShapeCdcKeys}, so the DO's one seam over
|
|
4155
|
+
* that read stays the DO's.
|
|
4156
|
+
*
|
|
4157
|
+
* The pipeline itself is host-neutral and lives in `@lunora/shard-engine` —
|
|
4158
|
+
* it reads through the `sql` handle and touches no instance state.
|
|
3916
4159
|
*/
|
|
3917
|
-
private
|
|
4160
|
+
private diffShape;
|
|
3918
4161
|
/** Build the full insert-poke of a shape's current membership — the first-seed/full-reseed rowset. */
|
|
3919
4162
|
private buildShapeSeed;
|
|
3920
4163
|
/**
|
|
@@ -4007,6 +4250,88 @@ declare abstract class ShardDO {
|
|
|
4007
4250
|
* snapshotted per socket.
|
|
4008
4251
|
*/
|
|
4009
4252
|
private withinGlobalShapeBound;
|
|
4253
|
+
/**
|
|
4254
|
+
* The changelog settings the generated code hands to the `.global()` writer.
|
|
4255
|
+
*
|
|
4256
|
+
* `cdc` is the app's own opt-in, forwarded from the shard config so the two
|
|
4257
|
+
* logs are governed by ONE switch: a deployment that turns CDC on gets a
|
|
4258
|
+
* changelog on both tiers, and one that leaves it off pays for neither. Until
|
|
4259
|
+
* this was threaded, the global writer was built without it unconditionally —
|
|
4260
|
+
* so the global `__cdc_log` was never written, and the poll's
|
|
4261
|
+
* changed-tables fast path was unreachable in every generated app while
|
|
4262
|
+
* looking, from the shard side, exactly like a backend that had CDC disabled.
|
|
4263
|
+
*
|
|
4264
|
+
* `cdcRetentionMs` is read here rather than in the generated factory so every
|
|
4265
|
+
* deployment knob goes through one strict parser (see
|
|
4266
|
+
* {@link ShardDO.sweepCdcRetention} for why lenient parsing on a delete path
|
|
4267
|
+
* is a footgun). Absent means the global log is never trimmed.
|
|
4268
|
+
*/
|
|
4269
|
+
protected globalCdcOptions(cdc: boolean): {
|
|
4270
|
+
cdc: boolean;
|
|
4271
|
+
cdcRetentionMs?: number;
|
|
4272
|
+
};
|
|
4273
|
+
/**
|
|
4274
|
+
* Ask the `.global()` backend which tables it recorded a write to after
|
|
4275
|
+
* `sinceSeq`. The base class has no global backend, so it reports no
|
|
4276
|
+
* visibility (`undefined`) and every poll tick falls back to re-reading
|
|
4277
|
+
* membership; the codegen-generated subclass overrides this to forward the
|
|
4278
|
+
* question to the global store's changelog. Emitted only for a project that
|
|
4279
|
+
* has both shapes and `.global()` tables.
|
|
4280
|
+
*
|
|
4281
|
+
* `cursorOnly` says the caller has already committed to reading everything
|
|
4282
|
+
* this pass and wants only the cursor — see the contract on
|
|
4283
|
+
* `DatabaseWriterLike.cdcChangedTables`.
|
|
4284
|
+
*/
|
|
4285
|
+
protected readGlobalChangedTables(_sinceSeq: number, _cursorOnly?: boolean): Promise<{
|
|
4286
|
+
cursor: number;
|
|
4287
|
+
floor?: number;
|
|
4288
|
+
tables: string[];
|
|
4289
|
+
} | undefined>;
|
|
4290
|
+
/**
|
|
4291
|
+
* Open one `.global()` poll tick: ask the global changelog what moved since
|
|
4292
|
+
* the last tick, and decide whether this tick may use that answer to skip
|
|
4293
|
+
* membership reads.
|
|
4294
|
+
*
|
|
4295
|
+
* Two things force a full pass regardless of what the changelog says. The
|
|
4296
|
+
* first is having no cursor to compare against — a cold instance, or a
|
|
4297
|
+
* backend with CDC disabled, has no basis for "unchanged" and must read. The
|
|
4298
|
+
* second is the resync interval, and it is the honest part of this design: a
|
|
4299
|
+
* `.global()` table can be written by something that is not this deployment,
|
|
4300
|
+
* and such a write leaves no row in our changelog. Trusting the changelog
|
|
4301
|
+
* forever would mean a shape silently frozen against an out-of-band writer,
|
|
4302
|
+
* so the fast path is a bounded skip — at worst {@link ShardDO.GLOBAL_SHAPE_RESYNC_MS}
|
|
4303
|
+
* of staleness for a change we could not see, against the full membership
|
|
4304
|
+
* re-read of every shape on every socket every two seconds that it replaces.
|
|
4305
|
+
*
|
|
4306
|
+
* That same bound now covers a second case worth naming: a shape whose
|
|
4307
|
+
* membership moves WITHOUT a write — a predicate over wall-clock, like
|
|
4308
|
+
* `_creationTime > now - 1h`. It used to converge on the poll interval
|
|
4309
|
+
* because every tick re-read it; it now converges on the resync interval,
|
|
4310
|
+
* because no changelog row marks its table as having moved. Bounded and
|
|
4311
|
+
* intended, but a different guarantee than the one this path used to give.
|
|
4312
|
+
*/
|
|
4313
|
+
private openGlobalPollTick;
|
|
4314
|
+
/**
|
|
4315
|
+
* Read a `.global()` shape's membership through this tick's cache.
|
|
4316
|
+
*
|
|
4317
|
+
* The key is the resolved predicate **and the caller's identity**, and the
|
|
4318
|
+
* second half is not redundant with the first. The op-log path can share a
|
|
4319
|
+
* probe on the predicate alone because it reads this shard's own SQLite with
|
|
4320
|
+
* nothing but that predicate — identity has no other channel into the query.
|
|
4321
|
+
* A `.global()` read does not have that property: the backend writer is built
|
|
4322
|
+
* per request from `{ identity, userId }`, so an application's own `d1` /
|
|
4323
|
+
* `hyperdriveGlobal` factory may scope rows by the caller before this code
|
|
4324
|
+
* ever sees them. Two sockets with equal predicates and different identities
|
|
4325
|
+
* can therefore be entitled to different rows, and sharing one read between
|
|
4326
|
+
* them would hand one user the other's.
|
|
4327
|
+
*
|
|
4328
|
+
* What remains shareable is what is genuinely identical: every socket of the
|
|
4329
|
+
* same user (tabs, devices, reconnects), and every socket of an anonymous or
|
|
4330
|
+
* public shape — which is where the fan-out that made this path expensive
|
|
4331
|
+
* lives. An identity or predicate that cannot be stably keyed falls back to
|
|
4332
|
+
* an un-shared read.
|
|
4333
|
+
*/
|
|
4334
|
+
private readGlobalShapeRowsCached;
|
|
4010
4335
|
/**
|
|
4011
4336
|
* Refresh every `.global()`-table shape held across all live sockets, one
|
|
4012
4337
|
* diff-poke per (socket, shape). Returns the number of global shapes still
|