@lunora/codegen 1.0.0-alpha.35 → 1.0.0-alpha.350

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.
Files changed (124) hide show
  1. package/README.md +1 -1
  2. package/dist/index.d.mts +4354 -2174
  3. package/dist/index.d.ts +4354 -2174
  4. package/dist/index.mjs +1 -35
  5. package/dist/packem_shared/AGENTS_FILENAME-isdHqPIt.mjs +1 -0
  6. package/dist/packem_shared/CAPABILITY_PROBES-brcmKenl.mjs +25 -0
  7. package/dist/packem_shared/CONTAINERS_FILENAME-mK4cZpcz.mjs +1 -0
  8. package/dist/packem_shared/CodegenDiagnosticError-BUXlG3P-.mjs +1 -0
  9. package/dist/packem_shared/DEFAULT_TARGET-3OLdJhTj.mjs +1 -0
  10. package/dist/packem_shared/FLAGS_FILENAME-BQk3SX8D.mjs +1 -0
  11. package/dist/packem_shared/GENERATED_HEADER-BXK9A_wP.mjs +1 -0
  12. package/dist/packem_shared/LUNORA_ERROR_CODES-CIAJQeKB.mjs +1 -0
  13. package/dist/packem_shared/MUTATORS_FILENAME-Cy9h3EVW.mjs +1 -0
  14. package/dist/packem_shared/NOTIFY_FILENAME-9to_l_Kh.mjs +1 -0
  15. package/dist/packem_shared/OPENRPC_VERSION-DfSmacMR.mjs +3 -0
  16. package/dist/packem_shared/PROJECT_CONFIG_FILENAMES-CLfsyvjA.mjs +1 -0
  17. package/dist/packem_shared/QUEUES_FILENAME-Civ-7SiI.mjs +1 -0
  18. package/dist/packem_shared/SCHEMA_SNAPSHOT_FILENAME-DYEopqtc.mjs +31 -0
  19. package/dist/packem_shared/SCHEMA_SNAPSHOT_VERSION-C7Um0EnM.mjs +2 -0
  20. package/dist/packem_shared/SHAPES_FILENAME-DVJbfDZ8.mjs +1 -0
  21. package/dist/packem_shared/SchemaSnapshotParseError-H9eQX4ui.mjs +18 -0
  22. package/dist/packem_shared/WORKFLOWS_FILENAME-kXq07aGl.mjs +1 -0
  23. package/dist/packem_shared/attribution-CG8-kUcl.mjs +1 -0
  24. package/dist/packem_shared/buildOpenApiDocument-DWdj2jvW.mjs +3 -0
  25. package/dist/packem_shared/capabilitiesUsedBy-DjRHg-Zk.mjs +1 -0
  26. package/dist/packem_shared/chain-74gjwZMP.mjs +1 -0
  27. package/dist/packem_shared/classify-procedure-call-BkaeM9Ej.mjs +1 -0
  28. package/dist/packem_shared/context-property-calls-dFjpeIlP.mjs +1 -0
  29. package/dist/packem_shared/context-root-CNR6TAsP.mjs +1 -0
  30. package/dist/packem_shared/createCodegenProject-ClPr_MXM.mjs +1 -0
  31. package/dist/packem_shared/database-calls-UPlhi3Ks.mjs +1 -0
  32. package/dist/packem_shared/declared-output-Iaar469J.mjs +1 -0
  33. package/dist/packem_shared/describeErrorLevelFindings-vzaGS9KL.mjs +1 -0
  34. package/dist/packem_shared/discoverAuthApiCalls-CScPrUD-.mjs +1 -0
  35. package/dist/packem_shared/discoverCrons-B1_NUt19.mjs +1 -0
  36. package/dist/packem_shared/discoverFunctions-CJG4S6CK.mjs +1 -0
  37. package/dist/packem_shared/discoverHttpRoutes-DCDgDIYv.mjs +1 -0
  38. package/dist/packem_shared/discoverHyperdriveCalls-CL-d0IVo.mjs +1 -0
  39. package/dist/packem_shared/discoverInserts-BcoTjXqI.mjs +1 -0
  40. package/dist/packem_shared/discoverMaskProcedures-Bw-BOIbA.mjs +1 -0
  41. package/dist/packem_shared/discoverMigrations-BWxx8Wv9.mjs +1 -0
  42. package/dist/packem_shared/discoverNondeterministicCalls-mFJNU8qY.mjs +1 -0
  43. package/dist/packem_shared/discoverQueries-CxQRVuIV.mjs +1 -0
  44. package/dist/packem_shared/discoverR2sqlCalls-B4oeTYEM.mjs +1 -0
  45. package/dist/packem_shared/discoverRlsMetadata-D0B4nCmG.mjs +1 -0
  46. package/dist/packem_shared/discoverRlsProcedures-Cl6UKT7L.mjs +1 -0
  47. package/dist/packem_shared/discoverSandboxUsage--Z1vx7GM.mjs +1 -0
  48. package/dist/packem_shared/discoverSchema-Cwt-DQgh.mjs +1 -0
  49. package/dist/packem_shared/discoverStorageRulesMetadata-CitX6ziW.mjs +1 -0
  50. package/dist/packem_shared/dispatch-namespace-DwTjD7BU.mjs +1 -0
  51. package/dist/packem_shared/emitAgents--VDKoet4.mjs +1 -0
  52. package/dist/packem_shared/emitApi-zynTVppI.mjs +147 -0
  53. package/dist/packem_shared/emitApp-BcLO2eih.mjs +989 -0
  54. package/dist/packem_shared/emitCrons-lc-hnJYn.mjs +96 -0
  55. package/dist/packem_shared/emitDataModel-CuFqq97x.mjs +114 -0
  56. package/dist/packem_shared/emitDrizzleSchema-bct1OuH5.mjs +15 -0
  57. package/dist/packem_shared/emitFunctions-D7mopPiT.mjs +506 -0
  58. package/dist/packem_shared/emitServer-DqjvPPKJ.mjs +1 -0
  59. package/dist/packem_shared/emitShard-CFUBJcKw.mjs +1 -0
  60. package/dist/packem_shared/exported-procedure-chains-D1WlRfqJ.mjs +1 -0
  61. package/dist/packem_shared/fingerprintSchemaSources-BKXkgkj4.mjs +1 -0
  62. package/dist/packem_shared/formatAdvisories-BCVASH7N.mjs +2 -0
  63. package/dist/packem_shared/handler-reference-CClSR4bi.mjs +1 -0
  64. package/dist/packem_shared/index-CN8FVvr-.mjs +1 -0
  65. package/dist/packem_shared/isD1GlobalTable-udvHAAzD.mjs +1 -0
  66. package/dist/packem_shared/isTestPath-C3QumE88.mjs +1 -0
  67. package/dist/packem_shared/isTypedSchema-CDv3pJB6.mjs +4 -0
  68. package/dist/packem_shared/json-data-Chfs2Cz5.mjs +1 -0
  69. package/dist/packem_shared/loadProjectConfig-B7RmxIeD.mjs +1 -0
  70. package/dist/packem_shared/module-specifiers-BctA0m_p.mjs +1 -0
  71. package/dist/packem_shared/modules-l-Db0C4S.mjs +1 -0
  72. package/dist/packem_shared/parse-validator-iVTounLK.mjs +1 -0
  73. package/dist/packem_shared/platform-8oj7iXQy.mjs +1 -0
  74. package/dist/packem_shared/property-name-7cBlgK_O.mjs +1 -0
  75. package/dist/packem_shared/qualifiers-Bl5fSmav.mjs +1 -0
  76. package/dist/packem_shared/readPackageDependencies-RHhGiz0k.mjs +1 -0
  77. package/dist/packem_shared/readServiceBindings-BA49C6aN.mjs +1 -0
  78. package/dist/packem_shared/redact-Cy14LEnc.mjs +1 -0
  79. package/dist/packem_shared/reserved-words-CW1QFbTd.mjs +1 -0
  80. package/dist/packem_shared/schema-BgM8A3XV.mjs +1 -0
  81. package/dist/packem_shared/schemaFromIr-BWFnvCVg.mjs +1 -0
  82. package/dist/packem_shared/server-DlEPJi5i.mjs +369 -0
  83. package/dist/packem_shared/shard-6Xk0XLNG.mjs +1795 -0
  84. package/dist/packem_shared/shard-runtime-ByYXjCKY.mjs +333 -0
  85. package/dist/packem_shared/shared-8vTRkL2-.mjs +13 -0
  86. package/dist/packem_shared/unwrap-handler-return-CGdJGvGh.mjs +1 -0
  87. package/dist/sdk/index.d.mts +288 -0
  88. package/dist/sdk/index.d.ts +288 -0
  89. package/dist/sdk/index.mjs +372 -0
  90. package/package.json +21 -8
  91. package/dist/packem_shared/CONTAINERS_FILENAME-DjpXMqhp.mjs +0 -224
  92. package/dist/packem_shared/CodegenDiagnosticError-DyQ5FwkM.mjs +0 -24
  93. package/dist/packem_shared/FLAGS_FILENAME-BjpS5w08.mjs +0 -139
  94. package/dist/packem_shared/GENERATED_HEADER-CLwPlCQe.mjs +0 -3
  95. package/dist/packem_shared/LUNORA_ERROR_CODES-CySpQPD3.mjs +0 -61
  96. package/dist/packem_shared/MUTATORS_FILENAME-BZOfUhlY.mjs +0 -81
  97. package/dist/packem_shared/OPENRPC_VERSION-4YIeMkOI.mjs +0 -60
  98. package/dist/packem_shared/QUEUES_FILENAME-BF0iUmx7.mjs +0 -119
  99. package/dist/packem_shared/SCHEMA_SNAPSHOT_FILENAME-C6Q0h0O5.mjs +0 -3425
  100. package/dist/packem_shared/SCHEMA_SNAPSHOT_VERSION-D0ARY6rL.mjs +0 -249
  101. package/dist/packem_shared/SHAPES_FILENAME-DOhPGi-6.mjs +0 -94
  102. package/dist/packem_shared/WORKFLOWS_FILENAME-CCC_42o3.mjs +0 -124
  103. package/dist/packem_shared/buildOpenApiDocument-bp1pYSV6.mjs +0 -183
  104. package/dist/packem_shared/discover-ast-CT6BgBr4.mjs +0 -13
  105. package/dist/packem_shared/discoverAuthApiCalls-URDoTADN.mjs +0 -62
  106. package/dist/packem_shared/discoverCrons-DdZOeroL.mjs +0 -257
  107. package/dist/packem_shared/discoverFunctions-BJdF7lRs.mjs +0 -514
  108. package/dist/packem_shared/discoverHttpRoutes-CcwP-Rkr.mjs +0 -131
  109. package/dist/packem_shared/discoverInserts-BpFhM32L.mjs +0 -61
  110. package/dist/packem_shared/discoverMaskProcedures-DT-v8wvj.mjs +0 -273
  111. package/dist/packem_shared/discoverMigrations-cG1idBVM.mjs +0 -97
  112. package/dist/packem_shared/discoverNondeterministicCalls-CEOjmfk4.mjs +0 -122
  113. package/dist/packem_shared/discoverQueries-DeVBQVAG.mjs +0 -62
  114. package/dist/packem_shared/discoverR2sqlCalls-CWCAU-xJ.mjs +0 -80
  115. package/dist/packem_shared/discoverRlsMetadata-BL0oukaK.mjs +0 -280
  116. package/dist/packem_shared/discoverSchema-CeXJWVKV.mjs +0 -874
  117. package/dist/packem_shared/discoverStorageRulesMetadata-CFHByjIl.mjs +0 -97
  118. package/dist/packem_shared/emit-CBwEvCXQ.mjs +0 -3451
  119. package/dist/packem_shared/emitApp-nQ2oC-8G.mjs +0 -668
  120. package/dist/packem_shared/formatAdvisories-DLIqHq5l.mjs +0 -114
  121. package/dist/packem_shared/parse-validator-D6zI2i85.mjs +0 -134
  122. package/dist/packem_shared/paths-BRd6JHuF.mjs +0 -11
  123. package/dist/packem_shared/redact-oTmsol5A.mjs +0 -33
  124. package/dist/packem_shared/schemaFromIr-DTYsLBaA.mjs +0 -57
@@ -0,0 +1,1795 @@
1
+ import{LunoraError as he}from"@lunora/errors";import{serializeSchemaSnapshot as Jt,hashSchemaSnapshot as Yt}from"./SCHEMA_SNAPSHOT_VERSION-C7Um0EnM.mjs";import{isHyperdriveGlobalTable as zt,isD1GlobalTable as Xt}from"./isD1GlobalTable-udvHAAzD.mjs";import{r as Zt}from"./json-data-Chfs2Cz5.mjs";import{r as $,e as er,a as tr,b as rr,c as nr,d as ar,f as sr,g as or,h as ir,i as dr,j as cr,k as lr,l as ur,m as hr,n as pr,o as mr,p as fr,q as gr}from"./shard-runtime-ByYXjCKY.mjs";import{u as re,h as br,s as yr,a as wr,I as vr,c as kr,G as Sr,d as Rr}from"./shared-8vTRkL2-.mjs";import{p as xr}from"./platform-8oj7iXQy.mjs";import{i as Ar}from"./schema-BgM8A3XV.mjs";const u=(n,t)=>`((): (() => ${t}) => { let cached: ${t} | undefined; return () => (cached ??= JSON.parse(${JSON.stringify(JSON.stringify(n))}) as ${t}); })()`,pe=["categories","description","facing","level","remediation","title"],Or=n=>{const t={};return{advisoryRows:n.map(a=>{let r=t[a.name];return r||(r=Object.fromEntries(pe.filter(o=>o in a).map(o=>[o,a[o]])),t[a.name]=r),Object.fromEntries(Object.entries(a).filter(([o,d])=>!pe.includes(o)||JSON.stringify(d)!==JSON.stringify(r[o])))}),advisoryRules:t}},Dr=` /**
2
+ * The bare writer every admin/maintenance entry point writes through.
3
+ *
4
+ * Admin and maintenance writes go through the SAME reactive-cache hooks as a
5
+ * user mutation. Without this, a studio row edit, a TTL sweep, an admin
6
+ * import, a CDC apply or a data-migration backfill writes without
7
+ * invalidating, and the next query answers from the pre-write snapshot.
8
+ *
9
+ * \`headroom\` meters the transaction. An admin \`/rpc\` caller passes nothing
10
+ * and falls back to \`this.transactionHeadroom()\`, which for an admin RPC is
11
+ * \`undefined\` — \`handleAdminRpc\` answers before \`beginDispatch\`, so no
12
+ * per-dispatch meter is in flight and a bulk loop is bounded by its per-call
13
+ * row cap alone. The TTL sweep (an alarm work item, no dispatch) passes its
14
+ * own by-value tracker explicitly instead.
15
+ */
16
+ private adminWriter(headroom?: TransactionHeadroomTracker): DatabaseWriterLike {
17
+ const env = (this.env ?? {}) as Record<string, unknown>;
18
+ const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
19
+
20
+ return createShardCtxDb({
21
+ ...this.ctxDbTuning(),
22
+ broadcast: (delta) => {
23
+ this.recordChangedTable(delta.table, delta.indexKeys);
24
+ },
25
+ cdc: config.cdc ?? false,
26
+ ...(headroom === undefined ? {} : { headroom }),
27
+ // Live predicate, same as the user-facing ctx — see \`databaseOptions\`.
28
+ inTransaction: () => this.isInTransaction(),
29
+ scheduler,
30
+ schema: schema as unknown as SchemaLike,
31
+ sql: this.sql as SqlExec,
32
+ });
33
+ }`,y=(n="")=>` const writer = this.adminWriter(${n});`,Tr=`${Dr}
34
+
35
+ protected override async runShardDataMigration(args: RunShardMigrationArgs): Promise<MigrationRunResult> {
36
+ this.ensureMigrated();
37
+
38
+ // Falls back to the framework's reserved re-projection backfill
39
+ // (\`__lunora_reproject__<table>\`), which rewrites rows whose
40
+ // v.bigint()/v.bytes() columns are still stored in the pre-projection
41
+ // tagged form. Built here rather than at module scope because it
42
+ // needs this shard's \`sql\` handle to tell a legacy row from a
43
+ // current one.
44
+ const migration = LUNORA_MIGRATIONS[args.id] ?? buildReprojectionMigration(args.id, schema as unknown as SchemaLike, this.sql as SqlExec);
45
+
46
+ if (!migration) {
47
+ throw new LunoraError("MIGRATION_NOT_FOUND", \`data migration "\${args.id}" is not registered\`, { status: 404 });
48
+ }
49
+
50
+ ${y()}
51
+
52
+ try {
53
+ return await runDataMigration({
54
+ ...(args.batchSize === undefined ? {} : { batchSize: args.batchSize }),
55
+ ...(args.direction === undefined ? {} : { direction: args.direction }),
56
+ ...(args.dryRun === undefined ? {} : { dryRun: args.dryRun }),
57
+ ...(args.maxBatches === undefined ? {} : { maxBatches: args.maxBatches }),
58
+ migration: migration as unknown as DataMigrationLike,
59
+ // Flush after each batch so live migrationStatus subscribers see
60
+ // progress mid-run (the base method records the reserved state
61
+ // table the raw-SQL progress write is invisible to the tracker).
62
+ onBatch: () => this.flushMigrationProgress(),
63
+ sql: this.sql as SqlExec,
64
+ writer,
65
+ });
66
+ } finally {
67
+ // The de-dup that unblocks a deferred UNIQUE index runs through here;
68
+ // let the next call rebuild it now instead of after the retry interval.
69
+ this.migrationRetryAt = 0;
70
+ }
71
+ }
72
+
73
+ protected override async runShardWrite(args: RunShardWriteArgs, headroom?: TransactionHeadroomTracker): Promise<RunShardWriteResult> {
74
+ const definition = (schema as unknown as SchemaLike).tables[args.table];
75
+
76
+ if (!definition) {
77
+ throw new LunoraError("UNKNOWN_TABLE", \`unknown table: \${args.table}\`, { status: 404 });
78
+ }
79
+
80
+ // \`.global()\` tables live in D1, not this DO's SQLite — editing them
81
+ // here would corrupt nothing but would fail confusingly, so reject up
82
+ // front with a clear code the studio can surface.
83
+ if (definition.shardMode?.kind === "global") {
84
+ throw new LunoraError("GLOBAL_TABLE_NOT_EDITABLE", \`table "\${args.table}" is global; edit it through D1, not the shard\`, { status: 400 });
85
+ }
86
+
87
+ this.ensureMigrated();
88
+
89
+ ${y("headroom ?? this.transactionHeadroom()")}
90
+
91
+ if (args.op === "insert") {
92
+ const id = await writer.insert(args.table, args.doc ?? {});
93
+
94
+ return { id, op: "insert" };
95
+ }
96
+
97
+ // Every by-id op PINS \`args.table\`. Unpinned, \`locateRowById\` probes
98
+ // every non-global table for the id, so a request naming table A with an
99
+ // id belonging to table B locates and mutates B's row — the by-id IDOR
100
+ // the per-table \`ctx.db.<table>\` facade pins against. Pinning also stops
101
+ // an absent row falling through to the \`.global()\` D1 twin, though that
102
+ // branch is already unreachable here: \`adminWriter\` is built without a
103
+ // \`globalDb\`, so a miss throws \`NOT_FOUND\` either way.
104
+ // \`hard\` is set only by the BULK delete arm, which needs the row gone
105
+ // from the physical table for its next batch's scan to make progress.
106
+ // A single-row \`writeRow\` delete never carries it, so it keeps the
107
+ // table's declared \`.softDelete()\` behaviour.
108
+ if (args.op === "delete") {
109
+ await writer.delete(args.id ?? "", args.table, { hard: args.hard === true });
110
+
111
+ return { id: args.id ?? null, op: "delete" };
112
+ }
113
+
114
+ if (args.op === "replace") {
115
+ await writer.replace(args.id ?? "", args.doc ?? {}, args.table);
116
+
117
+ return { id: args.id ?? null, op: "replace" };
118
+ }
119
+
120
+ await writer.patch(args.id ?? "", args.doc ?? {}, args.table);
121
+
122
+ return { id: args.id ?? null, op: "patch" };
123
+ }
124
+
125
+ // The base \`ShardDO\` cannot build a schema-aware writer, so its
126
+ // \`runShardExport\`/\`runShardImport\` return \`[]\` and \`{inserted:{}}\`.
127
+ // Those are success-shaped: an empty export is what a correct export of
128
+ // an empty shard looks like, so a backup of a populated sharded schema
129
+ // reported success and wrote nothing, and an import reported success and
130
+ // dropped every row. Overriding both here is what makes the two admin
131
+ // RPCs real for a \`.shardBy()\` table.
132
+ //
133
+ // NOTE: the admin export RPC is not paginated — \`parseExportShardArgs\`
134
+ // takes \`tables\`/\`batchSize\` but no cursor, and the coordinator issues
135
+ // one call per shard — so the rows are collected into a single response.
136
+ // \`batchSize\` bounds the SQLite scan page, not the reply. A shard holding
137
+ // more rows than fit in the DO's memory budget will therefore fail the
138
+ // export rather than stream it. That is a loud failure where the bug this
139
+ // replaces was a silent empty one, but a cursor on the RPC is what makes
140
+ // this a complete backup path for large shards.
141
+ protected override async runShardExport(args: RunShardExportArgs): Promise<ExportRow[]> {
142
+ this.ensureMigrated();
143
+
144
+ ${y()}
145
+
146
+ const rows: ExportRow[] = [];
147
+
148
+ // \`exportShardRows\` walks in keyset batches so a large table does not
149
+ // materialize at once inside the generator; the admin RPC's response
150
+ // is a single array, so it is collected here rather than streamed.
151
+ for await (const row of exportShardRows(writer, schema as unknown as SchemaLike, args)) {
152
+ rows.push(row);
153
+ }
154
+
155
+ return rows;
156
+ }
157
+
158
+ protected override async runShardImport(args: RunShardImportArgs): Promise<ImportShardResult> {
159
+ this.ensureMigrated();
160
+
161
+ ${y()}
162
+
163
+ // \`importShardRows\` inserts with \`allowExplicitId\`, so a source
164
+ // database's \`_id\`s carry across verbatim and every foreign key
165
+ // referencing them stays valid without a second remapping pass.
166
+ return importShardRows(writer, schema as unknown as SchemaLike, args);
167
+ }
168
+
169
+ protected override async runShardRankBefore(args: RunShardRankBeforeArgs): Promise<{ before: number; total: number }> {
170
+ this.ensureMigrated();
171
+
172
+ ${y()}
173
+
174
+ // \`rankBefore\` is optional on \`DatabaseWriterLike\` (the D1 twin omits it),
175
+ // but the shard writer from \`createShardCtxDb\` always defines it.
176
+ if (!writer.rankBefore) {
177
+ throw new LunoraError("NOT_IMPLEMENTED", "rankBefore is unavailable on the shard writer", { status: 500 });
178
+ }
179
+
180
+ return writer.rankBefore(args.table, args.index, {
181
+ partitionKey: args.partitionKey,
182
+ rowId: args.rowId,
183
+ sortValues: args.sortValues,
184
+ });
185
+ }
186
+
187
+ protected override async runShardFindRelated(args: RunShardFindRelatedArgs): Promise<RelatedPage> {
188
+ this.ensureMigrated();
189
+
190
+ ${y()}
191
+
192
+ // \`related\` is optional on \`DatabaseWriterLike\` (the D1 twin omits it),
193
+ // but the shard writer from \`createShardCtxDb\` always defines it — it
194
+ // derives the edge set from this app's schema.
195
+ if (!writer.related) {
196
+ throw new LunoraError("NOT_IMPLEMENTED", "findRelated is unavailable on the shard writer", { status: 500 });
197
+ }
198
+
199
+ return writer.related(
200
+ { id: args.id, table: args.table },
201
+ args,
202
+ );
203
+ }
204
+
205
+ protected override async runShardRankPage(args: RunShardRankPageArgs): Promise<ShardRankPageResult> {
206
+ this.ensureMigrated();
207
+
208
+ ${y()}
209
+
210
+ // \`rankPageRows\` is optional on \`DatabaseWriterLike\` (the D1 twin omits it),
211
+ // but the shard writer from \`createShardCtxDb\` always defines it. The sort
212
+ // directions live in the schema's rankIndex, so the shard reads them itself;
213
+ // \`args.directions\` is only the coordinator's comparator hint and isn't forwarded.
214
+ if (!writer.rankPageRows) {
215
+ throw new LunoraError("NOT_IMPLEMENTED", "rankPage is unavailable on the shard writer", { status: 500 });
216
+ }
217
+
218
+ return writer.rankPageRows(args.table, args.index, {
219
+ ...(args.after === undefined ? {} : { after: args.after }),
220
+ ...(args.cursor === undefined ? {} : { cursor: args.cursor }),
221
+ ...(args.partitionKey === undefined ? {} : { partitionKey: args.partitionKey }),
222
+ ...(args.take === undefined ? {} : { take: args.take }),
223
+ });
224
+ }
225
+
226
+ protected override async runShardApplyCdc(args: RunShardApplyCdcArgs): Promise<{ applied: number }> {
227
+ this.ensureMigrated();
228
+
229
+ ${y()}
230
+
231
+ await applyCdcChanges(writer, args.changes);
232
+
233
+ return { applied: args.changes.length };
234
+ }`,_r=({actionOnlyBuild:n,actionOnlyFields:t,agentsBuild:e,agentsContextField:a,containersBuild:r,containersContextField:o,databaseOptions:d,everyContextBuild:m,everyContextField:f,facadeBlock:h,globalDatabaseLine:x,hasSensitiveArgs:S=!1,lazyFunctions:g=!1,notifyBuild:c,ormContextField:w,paymentsBuild:M,paymentsContextField:A,queuesBuild:q,queuesContextField:F,topicsBuild:R,topicsContextField:U,vectorsBuild:i,vectorsContextField:v,workflowsBuild:E,workflowsContextField:B})=>{const I=t.length>0,C=w===""?"{ db }":"{ db, orm: bindOrm(facade) }",j=w===""?"":`
235
+ orm: databaseContext.orm,`,W=w===""?"":`
236
+ orm: { configurable: true, enumerable: true, value: mutationDatabase.orm, writable: true },`,O=I?`
237
+ // ActionCtx-only helpers (external, non-deterministic I/O): constructed
238
+ // and attached only for an \`action\` so query/mutation ctx never carry them.
239
+ if (isAction) {
240
+ ${n.replaceAll(/^(?=.)/gmu," ")}${t.map(p=>` ctx.${p} = ${p};`).join(`
241
+ `)}
242
+ }
243
+ `:"";return` private buildCtx(options: { bookmarks?: DispatchBookmark | undefined; functionPath?: string | undefined; headroom?: TransactionHeadroomTracker | undefined; identity?: SubscriptionIdentity | undefined; kind?: "query" | undefined; onRead?: ((table: string, idOrScan?: string) => void) | undefined; onReadRange?: ((range: KeyRange) => void) | undefined; replayKey?: boolean | undefined; scope?: QueryReadScope | undefined; sensitive?: SensitiveScope | undefined; trusted?: boolean | undefined } = {}): unknown {
244
+ const env = (this.env ?? {}) as Record<string, unknown>;
245
+ // The caller context this ctx runs under, resolved ONCE on one
246
+ // discriminant. When the caller threads an explicit identity
247
+ // (subscription seed / refresh / stream pull / shape resolve — all
248
+ // deferred or interleaved), it is used by value and the shared
249
+ // per-request fields are never read, because a concurrent RPC owns
250
+ // them: a refresh in particular runs inside the writing dispatch's
251
+ // \`flushChangedTables\`, BEFORE its \`endDispatch\`, so \`getCurrentIp()\`
252
+ // there is the MUTATING caller's address. Otherwise (the synchronous
253
+ // RPC dispatch path) it falls back to those fields as before.
254
+ //
255
+ // One expression, not one per field: three parallel ternaries on the
256
+ // same discriminant is how \`ip\` came to be the only one still reading
257
+ // the shared field.
258
+ const caller: SubscriptionIdentity = options.identity ?? (() => {
259
+ const identity = this.getCurrentIdentity();
260
+ const ip = this.getCurrentIp();
261
+ const userId = this.getCurrentUserId();
262
+
263
+ return {
264
+ ...(identity === undefined ? {} : { identity }),
265
+ ...(ip === undefined ? {} : { ip }),
266
+ ...(userId === undefined ? {} : { userId }),
267
+ };
268
+ })();
269
+ const { identity, ip, userId } = caller;
270
+ ${i}${m}${r}${E}${q}${R}${e}
271
+ // Which dispatch this ctx belongs to. Drives the two deferral facades
272
+ // below and the \`ctx.run*\` caller guard; a ctx built for an
273
+ // admin/lifecycle path has no registered function, and so no kind. An
274
+ // untracked \`ctx.runQuery\` overrides it with \`"query"\`: the sub-query
275
+ // keeps the caller's \`functionPath\` for attribution but runs as a query.
276
+ const contextKind = options.kind ?? LUNORA_FUNCTIONS[options.functionPath ?? ""]?.kind;
277
+ // \`list\`/\`get\` are the two methods \`ctx.db.system.query("_scheduled_functions")\`
278
+ // reaches through, and pending jobs live in the SchedulerDO — nothing the
279
+ // CDC changelog records — so reading them must forfeit a delta resume.
280
+ // The scheduler's own \`runAfter\`/\`runAt\`/\`cancel\` are writes and stay unstamped.
281
+ const schedulerBase = markUnvouchableReads((config.scheduler?.(env) ?? schedulerStub) as SchedulerLike, options.onRead, ["get", "list"]);
282
+ // \`ctx.scheduler.runAfter(0, ...)\` is documented as the deterministic
283
+ // equivalent of an \`afterCommit\` hook, and the SchedulerDO persists the
284
+ // job the moment it is called — so inside a mutation the call is BUFFERED
285
+ // and replayed after the transaction commits (a rollback drops it).
286
+ // Composed mutations get their own facade when the caller is outside
287
+ // a transaction. True nested calls share the enclosing facade and open
288
+ // a nested window. The action's own schedules stay immediate.
289
+ //
290
+ // Every kind but \`query\` is wrapped, because \`runMutationTransaction\`
291
+ // is installed on \`ctx.runMutation\` for every kind but \`query\`: a
292
+ // STREAM ctx, and an admin/lifecycle ctx with no registered kind at all,
293
+ // both get the BEGIN/COMMIT span for a mutation they compose, and used
294
+ // to get it with the deferral missing — so that mutation's job reached
295
+ // the SchedulerDO while its transaction was still open, and survived the
296
+ // rollback. A query cannot host a mutation handler (\`dispatchRun\`
297
+ // refuses it) and its ctx is built on the hot subscription path, so it
298
+ // stays unwrapped.
299
+ //
300
+ // Wrapped OUTSIDE the read-stamping facade so \`get\`/\`list\` stay stamped.
301
+ //
302
+ // \`this.scheduleOutbox()\` is what keeps the buffer honest. Everything
303
+ // between the COMMIT and the scheduler's acknowledgement is outside the
304
+ // transaction, so a failure there leaves writes that are durable and a
305
+ // job that exists nowhere — and the mutation's replay-dedup row committed
306
+ // inside the span, so the client's retry is answered from cache and told
307
+ // it succeeded. The outbox takes custody of each buffered call inside the
308
+ // transaction and releases it once the scheduler has the job; what
309
+ // survives is retried by \`pollScheduleOutbox\` on the shared poll alarm.
310
+ const scheduler = contextKind === "query" ? schedulerBase : withDeferredSchedules(schedulerBase, this.scheduleOutbox());
311
+ // Build the storage adapter once and share it between \`ctx.storage\`
312
+ // and \`ctx.db.system._storage\` so both read the same R2 binding. The
313
+ // \`storageStub\` fallback satisfies SystemReaderStorageLike structurally
314
+ // (its \`list\`/\`getMetadata\` throw the "no storage configured" error).
315
+ //
316
+ // Wrapped BEFORE the split so both consumers share one stamping facade.
317
+ // Only the methods that actually reach R2 are stamped: \`getUrl\` and
318
+ // \`getSignedUrl\` build a URL from the configured base (and an HMAC) and
319
+ // read nothing, and they are what handlers overwhelmingly call — stamping
320
+ // them would forfeit resumes for a dependency that cannot move the result.
321
+ // \`bucket\` IS stamped: it hands back a sub-facade this wrapper does not
322
+ // reach, so the selection is the last point at which the read can be seen.
323
+ //
324
+ // The request origin is the storage base fallback for mutations and
325
+ // actions on the synchronous dispatch only. A deferred caller
326
+ // (\`options.identity\` set) runs outside the dispatch that owns the
327
+ // field, the same hazard \`getCurrentIp\` has. A query never gets it: a
328
+ // live query re-runs with no request behind it, and the reactive
329
+ // cache does not key on the host, so a query that signs URLs needs a
330
+ // declared \`publicBaseUrl\` either way.
331
+ const requestOrigin = contextKind !== "query" && options.identity === undefined ? this.getCurrentOrigin() : undefined;
332
+ const makeStorage = (origin?: string): SystemReaderStorageLike =>
333
+ markUnvouchableReads(asBucketStorage(config.storage?.(env, origin) ?? storageStub) as SystemReaderStorageLike, options.onRead, [
334
+ "bucket",
335
+ "download",
336
+ "getMetadata",
337
+ "list",
338
+ ]);
339
+ const storage = makeStorage(requestOrigin);
340
+ // \`ctx.storage.deleteAfterCommit(key)\`, on every dispatch that can host
341
+ // a MUTATION handler — which is not only a mutation dispatch. An
342
+ // action's composed mutation gets a child ctx with its own deferred
343
+ // delete queue, while mutations reuse the admitted ctx. Queries stay
344
+ // unwrapped: they cannot host one, and their ctx is built on the hot
345
+ // subscription path.
346
+ //
347
+ // Every dispatch wrapped here also flushes (see \`handleRpc\` and
348
+ // \`runReactor\`) — a queue nothing drains leaks silently, which is worse
349
+ // than not having the method at all.
350
+ //
351
+ // Wrapped OUTSIDE the read-stamping facade so \`bucket(name)\` still
352
+ // resolves through it and stays stamped, and applied only to
353
+ // \`ctx.storage\` so \`ctx.db.system._storage\` (which shares the adapter
354
+ // above) is untouched.
355
+ const contextStorage = contextKind === "mutation" || contextKind === "action" ? withDeferredDeletes(storage) : storage;
356
+ // \`ctx.log\`: the DO base builds the attributed logger (structured
357
+ // fields + \`.with(...)\` child + trace correlation) and routes each call
358
+ // to the optional \`observability\` sink. It also buffers the line (studio
359
+ // Logs panel) and emits a structured console event the dev-server formats.
360
+ //
361
+ // Resolved BEFORE \`ctx.db\` below, because the database is wrapped in
362
+ // auto-instrumentation that needs the sink, the trace anchor, and the
363
+ // dispatch's wide-event handle.
364
+ const observability = config.observability?.(env);
365
+ const logFunctionPath = options.functionPath ?? "";
366
+ const log = this.makeLogger(logFunctionPath, observability, undefined, options.sensitive);
367
+ const traceAnchor = this.resolveDispatchAnchor(Boolean(options.identity));
368
+
369
+ // \`ctx.span\`: the handle onto the DISPATCH's own span — the wide-event
370
+ // surface. Attributes attached here accumulate for the whole request
371
+ // and are folded into the one span it already emits, instead of
372
+ // becoming N separate log lines. Shares \`traceAnchor\` with \`ctx.trace\`
373
+ // so both write to the same trace.
374
+ const span = this.makeDispatchSpan(traceAnchor, observability, options.sensitive);
375
+
376
+ ${x} // \`ctx.db\`, wrapped in automatic instrumentation: by default this
377
+ // adds aggregate counters (call count, total time, per-operation
378
+ // breakdown) to the wide event rather than a span per call, so a
379
+ // handler making hundreds of queries stays readable. See
380
+ // \`instrumentDatabase\` for the \`"spans"\` / \`"off"\` levels.
381
+ // Schema triggers close over this DB's scheduler, so a composed
382
+ // mutation must rebuild its DB and table/ORM views with its own queue.
383
+ const makeDatabaseContext = (scheduler: SchedulerLike) => {
384
+ const db: DatabaseWriterLike = this.instrumentDb(createShardCtxDb(${d}), logFunctionPath, traceAnchor, observability, options.sensitive);
385
+ ${h} return ${C};
386
+ };
387
+ const databaseContext = makeDatabaseContext(scheduler);
388
+ const db = databaseContext.db;
389
+ ${M}
390
+
391
+ // \`ctx.trace\` / \`ctx.metrics\`: spans and measurements to the same sink.
392
+ // The trace anchor is threaded explicitly for the same reason \`identity\`
393
+ // is — a deferred caller (a subscription re-run) must not inherit the
394
+ // writing mutation's trace from the shared per-request field.
395
+ const trace = this.makeTracer(logFunctionPath, observability, traceAnchor, options.sensitive);
396
+ const metrics = this.makeMetrics(logFunctionPath, observability, options.sensitive);
397
+ ${c}
398
+ // \`ctx.now\`: the wall-clock instant (epoch ms) this function began,
399
+ // captured ONCE so the whole handler body sees a single stable value.
400
+ // A \`query\` handler is re-run by every live subscription that reads it,
401
+ // so \`Date.now()\` there flickers between re-evaluations. A \`mutation\`
402
+ // handler does not replay under ordinary dispatch (an OCC conflict throws
403
+ // to the caller rather than retrying internally), but one called from a
404
+ // workflow step or queue consumer runs again when that step replays. Both
405
+ // read time through \`ctx.now\` — the \`nondeterministic_query_mutation\`
406
+ // advisor flags \`Date.now()\`. Actions may still use ambient \`Date.now()\`.
407
+ const now = Date.now();
408
+
409
+ // \`ctx.fetch\` is action-only. A query is re-run by every live subscription and a
410
+ // mutation may be replayed, so a network call in either returns a different answer
411
+ // on each run. Refused at runtime, not only linted: the advisor sees just the call
412
+ // sites it can read, and a call inside a dependency is invisible to it.
413
+ // Synchronous on purpose: an async refusal only rejects a promise the handler may
414
+ // discard, and the handler would then return normally. A sync throw surfaces at the call.
415
+ const refuseFetch = (): never => {
416
+ throw new LunoraError("FORBIDDEN", "ctx.fetch is action-only: network I/O belongs in an action, not a query or mutation");
417
+ };
418
+
419
+ // A reactor run is driven by the writer's inline refresh, not replayed by the writer, so it must not
420
+ // inherit the writer's key: every run would restart the same id and random sequence.
421
+ const stableKey = options.replayKey !== false && options.identity === undefined && this.idempotencyNamespace() !== undefined ? this.getCurrentMutationId() : undefined;
422
+ const stableScope = JSON.stringify([this.currentShardKey(), options.functionPath ?? "", this.idempotencyNamespace() ?? "", userId ?? ""]);
423
+ // One sequence for every mutation this run composes: their ids share one counter (the copied \`newId\`), so their draws must too.
424
+ const composedRandom = createStableRandomFactory(stableKey, stableScope);
425
+
426
+ const ctx: Record<string, unknown> = {
427
+ auth: {
428
+ getIdentity: async () => identity ?? null,
429
+ userId: userId ?? null,
430
+ },
431
+ db,
432
+ // \`ctx.newId()\`: a replay of the same top-level mutation gets the same ids. A nested or
433
+ // subscription-driven build (\`options.identity\`) is not a replay.
434
+ // A replay key only means something inside the caller's dedup namespace; without one the call is not
435
+ // deduplicated, so its ids stay random.
436
+ // \`ctx.random()\` shares the replay key and scope, so a replay reproduces both sequences.
437
+ newId: createStableIdFactory(stableKey, stableScope),
438
+ // Only a mutation is a replay the key reproduces; a query or action carrying the header draws Math.random.
439
+ random: createStableRandomFactory(contextKind === "mutation" ? stableKey : undefined, stableScope),
440
+ // Instrumented \`fetch\`: a CLIENT span per outbound call plus W3C
441
+ // \`traceparent\` propagation, so time spent in a downstream service
442
+ // is visible and its spans join this trace. Degrades to the bare
443
+ // global when no sink is configured. Not built for a query or a
444
+ // mutation: those refuse network I/O (see \`refuseFetch\`).
445
+ fetch: contextKind === "query" || contextKind === "mutation" ? refuseFetch : this.makeFetch(logFunctionPath, traceAnchor, observability, options.sensitive),
446
+ // A GETTER, not a plain field, so the reactive cache can tell a
447
+ // handler that reads the caller's address from the majority that
448
+ // never do. \`ctx.ip\` is per-request state the memo must be keyed
449
+ // by — otherwise two anonymous callers share one entry and the
450
+ // second is served the first's address — but keying every entry
451
+ // by address would shard the cache per client for every query.
452
+ // Reading it here marks the dispatch's scope; \`runCachedQuery\`
453
+ // folds the address into the key for this function from then on.
454
+ get ip() {
455
+ options.scope?.markIpRead();
456
+
457
+ return ip;
458
+ },
459
+ log,
460
+ metrics,
461
+ now,
462
+ // The same request origin the storage fallback above uses, handed
463
+ // to the handler so code that signs its own URLs (a copy-in
464
+ // \`createStorage\`) can fall back to it too. \`undefined\` for a
465
+ // query and for a deferred dispatch, for the reasons given there.
466
+ origin: requestOrigin,${j}
467
+ scheduler,
468
+ span,
469
+ storage: contextStorage,
470
+ trace,${v}${f}${A}${o}${B}${F}${U}${a}${S?`
471
+ [Symbol.for("lunora.admitSensitiveArgs")]: (args: Record<string, unknown>, names: ReadonlySet<string>): void => this.admitSensitiveValues(options.sensitive, args, names),`:""}
472
+ };
473
+ ${I?` const isAction = contextKind === "action";
474
+ `:""}${O}
475
+ ${g?` const captureScope = (): (() => void) => {
476
+ const requestScope = this.captureRequestScope();
477
+ return () => this.restoreRequestScope(requestScope);
478
+ };
479
+
480
+ `:""} const installRun = (target: Record<string, unknown>, kind: typeof contextKind): void => {
481
+ target.runAction = (reference: FunctionReference, fnArgs: Record<string, unknown>) => {
482
+ // The callee runs inside this invocation, so its marked arguments are scrubbed here too.
483
+ this.admitSensitiveArgs(options.sensitive, reference.__lunoraRef, fnArgs);
484
+
485
+ return dispatchRun("action", reference.__lunoraRef, fnArgs, target, kind${g?", undefined, captureScope":""});
486
+ };
487
+ // The composed mutation runs under the SAME wrapper the top-level RPC
488
+ // uses, so "do the transactional work in a mutation and call it from the
489
+ // action" — the recipe the docs give for atomicity — actually is atomic.
490
+ target.runMutation = (reference: FunctionReference, fnArgs: Record<string, unknown>) => {
491
+ this.admitSensitiveArgs(options.sensitive, reference.__lunoraRef, fnArgs);
492
+ // An action can start sibling mutations concurrently. Each owns its
493
+ // context and deferral queues; only calls from an admitted mutation
494
+ // inherit that mutation's transaction.
495
+ const mutationCtx: Record<string, unknown> = this.mutationContexts.has(target)
496
+ ? target
497
+ : (() => {
498
+ const mutationScheduler = withDeferredSchedules(schedulerBase, this.scheduleOutbox());
499
+ const mutationDatabase = makeDatabaseContext(mutationScheduler);
500
+
501
+ // Copied from an action's ctx, which carries the real \`fetch\`; a mutation
502
+ // refuses network I/O wherever it runs, so its \`fetch\` is overridden here.
503
+ return Object.defineProperties({}, {
504
+ ...Object.getOwnPropertyDescriptors(target),
505
+ db: { configurable: true, enumerable: true, value: mutationDatabase.db, writable: true },${W}
506
+ fetch: { configurable: true, enumerable: true, value: refuseFetch, writable: true },
507
+ // The composed mutation's ids come from the action's replay key and counter, so its draws do too; a
508
+ // fresh factory per call would repeat the same draws across sibling mutations.
509
+ random: { configurable: true, enumerable: true, value: composedRandom, writable: true },
510
+ scheduler: { configurable: true, enumerable: true, value: mutationScheduler, writable: true },
511
+ storage: { configurable: true, enumerable: true, value: withDeferredDeletes(storage), writable: true },
512
+ });
513
+ })();
514
+
515
+ if (mutationCtx !== target) {
516
+ installRun(mutationCtx, kind);
517
+ }
518
+
519
+ return dispatchRun("mutation", reference.__lunoraRef, fnArgs, mutationCtx, kind, async (work) => this.runMutationTransaction(mutationCtx, work)${g?", captureScope":""});
520
+ };
521
+ // \`ctx.runQuery(ref, args, { untracked: true })\` runs the sub-query on
522
+ // its OWN query context, built without the subscription's read-footprint
523
+ // hooks — so its reads never enter this subscription's footprint and a
524
+ // write to the tables it touched does not re-run us. Everything else is
525
+ // inherited: \`functionPath\` (log/metric attribution), \`headroom\` (the
526
+ // sub-query must not escape this dispatch's resource ceiling),
527
+ // \`scope\` (the reactive-cache capture — an untracked sub-query's reads
528
+ // must still be deps of the entry the OUTER query is memoized as, or
529
+ // the memo goes stale), and — load-bearing — the resolved \`caller\` BY VALUE
530
+ // (identity, userId AND ip). Omitting it would let \`buildCtx\` fall back
531
+ // to the shared per-request fields, which a concurrent RPC may have
532
+ // re-set, and an RLS-scoped sub-query would then read as the wrong user
533
+ // from the wrong address. A tracked call runs on \`queryContext()\` below.
534
+ target.runQuery = (reference: FunctionReference, fnArgs: Record<string, unknown>, runOptions?: { untracked?: boolean }) => {
535
+ this.admitSensitiveArgs(options.sensitive, reference.__lunoraRef, fnArgs);
536
+
537
+ return dispatchRun(
538
+ "query",
539
+ reference.__lunoraRef,
540
+ fnArgs,
541
+ runOptions?.untracked === true
542
+ ? this.buildCtx({ bookmarks: options.bookmarks, functionPath: options.functionPath, headroom: options.headroom, identity: caller, kind: "query", scope: options.scope, sensitive: options.sensitive })
543
+ : queryContext(),
544
+ kind,${g?`
545
+ undefined,
546
+ captureScope,`:""}
547
+ );
548
+ };
549
+ };
550
+
551
+ // A query composed from a mutation or action runs on a QUERY view of
552
+ // this ctx, so it behaves exactly as it does when called directly or
553
+ // live: same \`db\`, read hooks, trace and \`now\`, but every field that
554
+ // depends on the dispatch kind or the request origin is re-derived for a
555
+ // query — no \`ctx.origin\`, storage with no origin fallback (and no
556
+ // \`deleteAfterCommit\`), the unwrapped scheduler, no ActionCtx-only
557
+ // helpers, no \`fetch\` (a query is re-run, see \`refuseFetch\`), and a \`run*\` guard
558
+ // that refuses a mutation or action. A new kind- or origin-dependent ctx field must
559
+ // be overridden here too.
560
+ // Copied by descriptor so the \`ip\` getter is carried over, not read.
561
+ // Built on the first composed call only; a query ctx is its own view.
562
+ let queryView: Record<string, unknown> | undefined;
563
+ const queryContext = (): Record<string, unknown> => {
564
+ if (contextKind === "query") {
565
+ return ctx;
566
+ }
567
+
568
+ if (queryView === undefined) {
569
+ const descriptors: PropertyDescriptorMap = Object.getOwnPropertyDescriptors(ctx);
570
+ ${t.map(p=>` delete descriptors.${p};
571
+ `).join("")} // Writable and configurable, like the plain fields they replace.
572
+ const field = (value: unknown): PropertyDescriptor => ({ configurable: true, enumerable: true, value, writable: true });
573
+
574
+ queryView = Object.defineProperties({}, {
575
+ ...descriptors,
576
+ fetch: field(refuseFetch),
577
+ origin: field(undefined),
578
+ // A composed query is not a mutation replay, so it must not advance the mutation's random sequence.
579
+ random: field(createStableRandomFactory(undefined, stableScope)),
580
+ scheduler: field(schedulerBase),
581
+ storage: field(makeStorage()),
582
+ }) as Record<string, unknown>;
583
+ installRun(queryView, "query");
584
+ }
585
+
586
+ return queryView;
587
+ };
588
+
589
+ installRun(ctx, contextKind);
590
+
591
+ return ctx;
592
+ }`},me=`// Bound in-process \`ctx.run*\` composition depth so a self- or cyclically-
593
+ // referencing call fails loudly with a clear error instead of overflowing the
594
+ // stack. Tracked across the awaited handler chain (one DO invocation is
595
+ // single-threaded), so the counter reflects true nesting depth.
596
+ const MAX_RUN_DEPTH = 32;
597
+ let runDepth = 0;
598
+
599
+ const dispatchRun = async (
600
+ expected: FunctionKind,
601
+ functionPath: string,
602
+ args: Record<string, unknown>,
603
+ ctx: unknown,
604
+ // The kind of the context making the call, and — for a mutation target — the
605
+ // transaction wrapper to run it under. Both are supplied by \`buildCtx\`, which
606
+ // is the only place that knows which dispatch this \`ctx\` belongs to. Widened
607
+ // past \`FunctionKind\` because the registry also carries \`"stream"\`, and
608
+ // \`undefined\` for a ctx built outside a registered function (admin, migration).
609
+ callerKind?: "stream" | FunctionKind,
610
+ runTransactional?: (work: () => Promise<unknown>) => Promise<unknown>,
611
+ ): Promise<unknown> => {
612
+ // A query and a stream are read-only: their declared ctx is the query ctx. The
613
+ // ctx object installs \`run*\` on every kind (one shape, built once), so the
614
+ // TYPE is all that stops them writing — and a cast walks straight past it,
615
+ // inside a subscription re-run that can execute many times per write. Checked
616
+ // on the CALLER, before the registry lookup, so \`ctx.runMutation\` from a
617
+ // query or stream fails identically whatever it aimed at.
618
+ if ((callerKind === "query" || callerKind === "stream") && expected !== "query") {
619
+ throw new LunoraError(
620
+ "RUN_KIND_FORBIDDEN",
621
+ \`ctx.run\${expected[0]!.toUpperCase()}\${expected.slice(1)}: a \${callerKind} may only compose other queries (tried to run "\${functionPath}")\`,
622
+ );
623
+ }
624
+
625
+ const registered = LUNORA_FUNCTIONS[functionPath];
626
+
627
+ if (!registered) {
628
+ throw new Error(\`unknown function: \${functionPath}\`);
629
+ }
630
+
631
+ if (registered.kind !== expected) {
632
+ throw new Error(\`ctx.run\${expected[0]!.toUpperCase()}\${expected.slice(1)}: "\${functionPath}" is registered as a \${registered.kind}, not a \${expected}\`);
633
+ }
634
+
635
+ if (runDepth >= MAX_RUN_DEPTH) {
636
+ throw new LunoraError("RUN_DEPTH_EXCEEDED", \`ctx.run*: composition depth limit (\${MAX_RUN_DEPTH}) exceeded — likely a cyclic runQuery/runMutation\`);
637
+ }
638
+
639
+ runDepth += 1;
640
+
641
+ try {
642
+ // A mutation composed from an action (or an http action) is the shape the
643
+ // docs prescribe for atomicity — "do the transactional reads and writes in
644
+ // a mutation and call it from the action". It only holds if the composed
645
+ // dispatch gets the same BEGIN/COMMIT span and single-writer gate the
646
+ // top-level mutation path gets; without it every write autocommits and a
647
+ // mid-handler throw leaves the earlier ones durable. A \`runMutation\` from
648
+ // inside a mutation rides the enclosing span instead of opening a second
649
+ // one (see \`runMutationTransaction\`).
650
+ if (runTransactional) {
651
+ return await runTransactional(async () => registered.handler(ctx, args));
652
+ }
653
+
654
+ return await registered.handler(ctx, args);
655
+ } finally {
656
+ runDepth -= 1;
657
+ }
658
+ };`,Er=n=>n?me.replace(" runTransactional?: (work: () => Promise<unknown>) => Promise<unknown>,",` runTransactional?: (work: () => Promise<unknown>) => Promise<unknown>,
659
+ captureScope?: () => () => void,`).replace(` try {
660
+ // A mutation composed from an action`,` try {
661
+ let loaded = getLoadedFunction(functionPath);
662
+
663
+ if (loaded === undefined) {
664
+ const restoreScope = captureScope?.();
665
+
666
+ try {
667
+ loaded = await loadFunction(functionPath);
668
+ } finally {
669
+ restoreScope?.();
670
+ }
671
+ }
672
+
673
+ if (!loaded) {
674
+ throw new Error(\`unknown function: \${functionPath}\`);
675
+ }
676
+
677
+ const loadedFunction = loaded;
678
+
679
+ // A mutation composed from an action`).replaceAll("registered.handler","loadedFunction.handler"):me,Ir=` public override async handleRpc(functionPath: string, args: Record<string, unknown>, headroom?: TransactionHeadroomTracker, scope?: QueryReadScope, bookmarks?: DispatchBookmark, sensitive?: SensitiveScope): Promise<unknown> {
680
+ // A dispatch that did not come through the shard's \`/rpc\` (a scheduled call,
681
+ // a lifecycle hook, an admin run) has no scope of its own, so it opens one here
682
+ // and releases it when the handler settles.
683
+ if (sensitive === undefined) {
684
+ const own = this.openSensitiveScope(functionPath, args);
685
+
686
+ try {
687
+ return await this.handleRpc(functionPath, args, headroom, scope, bookmarks, own);
688
+ } finally {
689
+ own.clear();
690
+ }
691
+ }
692
+
693
+ __PREPARE_FUNCTION__ const registered = LUNORA_FUNCTIONS[functionPath];
694
+
695
+ // Internal functions are reachable server-side only: via \`ctx.run*\`
696
+ // composition, or a trusted system dispatch (scheduler/cron, marked by
697
+ // \`isSystemDispatch()\`). A client RPC never carries that flag, so its
698
+ // internals stay not-found and never leak across the external boundary.
699
+ if (!registered || (registered.visibility === "internal" && !this.isSystemDispatch())) {
700
+ throw new LunoraError("FUNCTION_NOT_FOUND", \`function not registered: \${functionPath}\`);
701
+ }
702
+
703
+ // A stream yields chunks over its WebSocket procedure stream. Run here it
704
+ // would return an async iterator, which the wire encoder cannot carry as a
705
+ // single result, so refuse it before the handler runs.
706
+ if (registered.kind === "stream") {
707
+ throw new LunoraError("BAD_REQUEST", \`"\${functionPath}" is a stream: open it over the WebSocket, not RPC\`);
708
+ }
709
+
710
+ this.ensureServable();
711
+
712
+ // \`headroom\`, when the caller supplied one, is threaded straight
713
+ // through to \`buildCtx\` — bypassing its \`this.transactionHeadroom()\`
714
+ // fallback (the racy shared-field read) entirely for THIS dispatch.
715
+ // The main \`/rpc\` path always supplies one; \`dispatchLifecycle\` /
716
+ // \`handleRunAs\` don't mint their own and omit it, so they keep the
717
+ // prior fallback behavior unchanged.
718
+ //
719
+ // \`scope\` is the reactive cache's per-dispatch read capture, threaded
720
+ // the same way and for the same reason: the ctx-db read hooks resolve
721
+ // their tracker at READ time, so binding them to the scope THIS
722
+ // dispatch was handed is what keeps a concurrent query's reads out of
723
+ // our dep set — and ours out of theirs. A dispatch with no scope (a
724
+ // mutation, an action, a cache-less shard) builds unbound hooks that
725
+ // stamp no deps.
726
+ const ctx = this.buildCtx({ bookmarks, functionPath, headroom, scope, sensitive, trusted: registered.lifecycle === "init" });
727
+
728
+ // A mutation's writes must commit all-or-nothing, so its dispatch runs
729
+ // under \`runMutationTransaction\` — the ONE place that opens the span,
730
+ // settles the deferred schedules and flushes the deferred deletes. The
731
+ // same helper backs \`ctx.runMutation\` and \`runReactor\`, because a
732
+ // mutation reached by composition is promised exactly these guarantees
733
+ // and used to get none of them.
734
+ //
735
+ // The replay bookkeeping (idempotency dedup row + custom-mutator
736
+ // watermark advance) commits INSIDE the span via
737
+ // \`commitMutationBookkeeping\`, so the writes, the dedup row, and the
738
+ // watermark are atomic — a crash can't leave the writes durable without
739
+ // the replay guard. It is deliberately NOT in the shared helper: it is
740
+ // per-REQUEST bookkeeping, and a composed sub-mutation has no request of
741
+ // its own to record.
742
+ if (registered.kind === "mutation") {
743
+ return await this.runMutationTransaction(ctx, async () => {
744
+ const value = await registered.handler(ctx, args);
745
+
746
+ this.commitMutationBookkeeping(value);
747
+
748
+ return value;
749
+ });
750
+ }
751
+
752
+ const result = await registered.handler(ctx, args);
753
+
754
+ // Composed mutations settle their own delete queues. An action can
755
+ // also queue deletes outside those mutations; drain its own queue here.
756
+ await this.deferPastResponse(flushDeferredDeletes(ctx));
757
+
758
+ return result;
759
+ }
760
+
761
+ private readonly mutationContexts = new WeakSet<object>();
762
+
763
+ /**
764
+ * Run one mutation handler with the atomicity every caller of a mutation is
765
+ * promised: a BEGIN/COMMIT span (with the single-writer gate
766
+ * \`ShardRunner.runInTransaction\` composes in front of it), scheduled jobs
767
+ * held until that span commits, and the deferred object deletes flushed
768
+ * only once it has.
769
+ *
770
+ * Shared by the three entry points that dispatch a mutation handler — the
771
+ * top-level RPC, \`ctx.runMutation\` from an action or http action, and a
772
+ * reactor — because the guarantee is a property of the mutation, not of how
773
+ * it was reached. The docs tell users to put the transactional part in a
774
+ * mutation and call it from the action precisely so that composition is
775
+ * atomic.
776
+ *
777
+ * A \`ctx.runMutation\` issued from inside a mutation is NESTED: SQLite-in-DO
778
+ * has no savepoints and \`runInTransaction\` rejects a second open, so it
779
+ * rides the enclosing span, which also owns its commit and therefore its
780
+ * scheduler flush.
781
+ *
782
+ * Both deferral windows are opened here and settled against the SAME
783
+ * outcome. True nested calls share their caller's context, so each window
784
+ * must discard its own failed delete and schedule intents even when the
785
+ * outer handler catches the error and commits.
786
+ */
787
+ private async runMutationTransaction<T>(ctx: unknown, work: () => Promise<T>): Promise<T> {
788
+ const settleSchedules = beginDeferredSchedules(ctx as { scheduler?: unknown });
789
+ // Synchronous and non-throwing (it only moves queue entries), so it can
790
+ // settle first on every path and cannot mask the outcome.
791
+ const settleDeletes = beginDeferredDeletes(ctx);
792
+
793
+ if (this.mutationContexts.has(ctx as object)) {
794
+ try {
795
+ const nested = await work();
796
+
797
+ settleDeletes(true);
798
+ await settleSchedules(true);
799
+
800
+ return nested;
801
+ } catch (error) {
802
+ settleDeletes(false);
803
+ await settleSchedules(false);
804
+
805
+ throw error;
806
+ }
807
+ }
808
+
809
+ let result: T;
810
+
811
+ try {
812
+ result = await this.runInTransaction(async () => {
813
+ this.mutationContexts.add(ctx as object);
814
+
815
+ try {
816
+ return await work();
817
+ } finally {
818
+ this.mutationContexts.delete(ctx as object);
819
+ }
820
+ }, { independent: true });
821
+ } catch (error) {
822
+ // The span rolled back, so the rows are gone and the jobs that were
823
+ // to run "after I commit" must go with them. Dropping them is the
824
+ // whole point of buffering: a persisted job for a write that never
825
+ // landed fires against state that does not exist.
826
+ //
827
+ // The queued object deletes go the same way, and for a sharper
828
+ // reason: the rows they were to clean up after are still there, and
829
+ // an R2 delete cannot be undone.
830
+ settleDeletes(false);
831
+ await settleSchedules(false);
832
+
833
+ throw error;
834
+ }
835
+
836
+ // Committed, so this span's queued keys join whatever the flush below
837
+ // (or an enclosing span's) will drain.
838
+ settleDeletes(true);
839
+
840
+ // Committed. Schedules first and AWAITED: \`runAfter(0, ...)\` is
841
+ // documented as the deterministic equivalent of an \`afterCommit\` hook,
842
+ // so the job must be enqueued after the commit, and a failure to enqueue
843
+ // it must be visible rather than swallowed.
844
+ //
845
+ // In a \`finally\`, because both halves of this are post-commit cleanup
846
+ // and neither is allowed to cancel the other. \`settleSchedules\` drains
847
+ // its whole queue and then rethrows what failed, so a SchedulerDO that
848
+ // refused one job used to take the object cleanup down with it — the
849
+ // rows were durably gone and their objects leaked with nothing logged.
850
+ try {
851
+ await settleSchedules(true);
852
+ __COMMITTED_CATCH__ } finally {
853
+ // Then the objects whose rows are now durably gone. Deliberately AFTER
854
+ // the span, never inside it: an R2 delete cannot roll back, so a delete
855
+ // issued from within a transaction that later aborts destroys data the
856
+ // surviving row still points at.
857
+ //
858
+ // \`flushDeferredDeletes\` never rejects — the mutation has already
859
+ // succeeded, so a failed cleanup must not turn into a failed response.
860
+ // A leaked object is reported through \`ctx.log\` instead, with its key.
861
+ await this.deferPastResponse(flushDeferredDeletes(ctx));
862
+ }
863
+
864
+ return result;
865
+ }
866
+
867
+ // Only a \`mutation\` may enter the base class's single-writer gate for
868
+ // mutation-replay dedup — it is \`blockConcurrencyWhile\`, so gating an
869
+ // action would stall every other dispatch on the shard for the length of
870
+ // its outbound I/O, on nothing but a caller-supplied header. Unregistered
871
+ // paths answer \`false\`; \`handleRpc\` above rejects them anyway.
872
+ protected override isMutationFunction(functionPath: string): boolean {
873
+ return LUNORA_FUNCTIONS[functionPath]?.kind === "mutation";
874
+ }
875
+
876
+ // The scheduler the deferred-schedule outbox retries through. Only the
877
+ // CONFIGURED one: falling back to \`schedulerStub\` would have an app with no
878
+ // scheduler retry every entry against a thrower until the attempt ceiling
879
+ // parked it, and there is nothing to park — an app with no scheduler has no
880
+ // \`ctx.scheduler\` call that reached the buffer in the first place.
881
+ protected override scheduleOutboxScheduler(): SchedulerLike | undefined {
882
+ return config.scheduler?.((this.env ?? {}) as Record<string, unknown>) as SchedulerLike | undefined;
883
+ }`,Cr=` protected override async executeSubscription(functionPath: string, args: Record<string, unknown>, identity?: SubscriptionIdentity): Promise<{ ranges?: Map<string, KeyRange[]>; result: unknown; tables: Set<string> } | null> {
884
+ const registered = LUNORA_FUNCTIONS[functionPath];
885
+
886
+ if (!registered || registered.kind !== "query" || registered.visibility === "internal") {
887
+ return null;
888
+ }
889
+
890
+ __PREPARE_FUNCTION__ this.ensureServable();
891
+
892
+ const footprint = createReadFootprint();
893
+ // Identity is threaded EXPLICITLY from the (deferred/interleaved)
894
+ // subscription caller — never read from the shared per-request field
895
+ // here — so a concurrent RPC can't leak its identity into this re-run.
896
+ const ctx = this.buildCtx({ functionPath, headroom: this.subscriptionHeadroom(), identity, onRead: footprint.onRead, onReadRange: footprint.onReadRange });
897
+ const result = await registered.handler(ctx, args);
898
+
899
+ // Ranges come from THIS run's reads, never a shared field on the DO:
900
+ // a deferred re-run interleaves with unrelated dispatches.
901
+ const ranges = footprint.ranges();
902
+
903
+ return { ...(ranges === undefined ? {} : { ranges }), result, tables: footprint.tables };
904
+ }
905
+
906
+ protected override executeStream(functionPath: string, args: Record<string, unknown>, identity?: SubscriptionIdentity): null | { durable?: { ttlMs?: number }; iterator: (signal: AbortSignal) => AsyncIterable<unknown> } {
907
+ const registered = LUNORA_FUNCTIONS[functionPath];
908
+
909
+ if (!registered || registered.kind !== "stream" || registered.visibility === "internal") {
910
+ return null;
911
+ }
912
+
913
+ this.ensureServable();
914
+
915
+ return {
916
+ ...(registered.durable ? { durable: registered.durable as { ttlMs?: number } } : {}),
917
+ // Identity threaded EXPLICITLY from the socket, exactly as
918
+ // \`executeSubscription\` above: the iterator is pulled after this
919
+ // frame returned, interleaved with unrelated dispatches, so
920
+ // \`buildCtx\`'s per-request fallback would run an \`rls()\` /
921
+ // \`ctx.auth\` stream as nobody — or as a concurrent RPC's caller.
922
+ iterator: (signal) => (registered.handler as (context: unknown, args: Record<string, unknown>, signal: AbortSignal) => AsyncIterable<unknown>)(this.buildCtx({ functionPath, identity }), args, signal),
923
+ };
924
+ }`,Lr=` } catch (error) {
925
+ // The write committed before this failed. Mark the error so the caller can tell
926
+ // a durable write from a rollback: the rethrow still reaches the caller unchanged.
927
+ if (typeof error === "object" && error !== null) {
928
+ Object.assign(error, { committed: true });
929
+ }
930
+
931
+ throw error;
932
+ `,Pr=(n,t=!1)=>{const e=t?` const registeredBeforeLoad = LUNORA_FUNCTIONS[functionPath];
933
+
934
+ if (registeredBeforeLoad?.visibility === "internal" && !this.isSystemDispatch()) {
935
+ throw new LunoraError("FUNCTION_NOT_FOUND", "function not registered: " + functionPath);
936
+ }
937
+
938
+ `:"",a=t?` const requestScope = this.captureRequestScope();
939
+ const preparation = this.prepareFunction(functionPath, "rpc");
940
+
941
+ if (preparation !== undefined) {
942
+ try {
943
+ await preparation;
944
+ } finally {
945
+ this.restoreRequestScope(requestScope);
946
+ }
947
+ }
948
+
949
+ `:"";return Ir.replace("__COMMITTED_CATCH__",n?Lr:"").replace("__PREPARE_FUNCTION__",`${e}${a}`)},Nr=n=>{const t={};for(const e of n.tables){const a={};for(const[r,o]of Object.entries(e.shape)){const d=re(o);d.kind==="id"&&d.tableName!==void 0&&(a[r]=d.tableName)}Object.keys(a).length>0&&(t[e.name]=a)}return t},$r=n=>{const t={};for(const e of n.tables){const a=[];for(const[r,o]of Object.entries(e.shape))re(o).kind==="storage"&&a.push(r);a.length>0&&(t[e.name]=a)}return t},Mr=n=>{const t={};for(const e of n.tables){const a=[...e.indexes.map(r=>({fields:[...r.fields],name:r.name,type:"index",...r.unique===!0?{unique:!0}:{}})),...e.searchIndexes.map(r=>({fields:[r.field,...r.filterFields??[]],name:r.name,type:"search"})),...(e.geoIndexes??[]).map(r=>({fields:[r.field],name:r.name,type:"geo"})),...e.rankIndexes.map(r=>({fields:r.sortBy.map(o=>o.field),name:r.name,type:"rank"})),...e.vectorIndexes.map(r=>({fields:r.field===void 0?[]:[r.field],name:r.name,type:"vector"}))];a.length>0&&(t[e.name]=a)}return t},qr=n=>{const t=[];for(const e of n.tables)!e.ttl||e.shardMode==="global"||t.push({...e.ttl.after===void 0?{}:{after:e.ttl.after},field:e.ttl.field,...e.softDelete?{softDeleteField:e.softDelete.field}:{},table:e.name});return t},Fr=n=>{if(n.kind!=="union"||n.members===void 0||n.members.length===0)return;const t=[];for(const e of n.members){if(e.kind!=="literal"||e.literalValue===void 0)return;try{const a=JSON.parse(e.literalValue);if(typeof a!="string")return;t.push(a)}catch{return}}return t},Ur=(n,t,e)=>{const a=re(t),r={name:n,optional:br(t),type:a.kind};if(a.kind==="id"&&a.tableName!==void 0){r.ref=a.tableName;const d=e.get(n);d!==void 0&&(r.onDelete=d)}a.kind==="storage"&&(r.isStorage=!0,a.bucket!==void 0&&(r.bucket=a.bucket));const o=Fr(a);return o!==void 0&&(r.enumValues=o),a.column?.notNull===!1&&(r.nullable=!0),r},Br=n=>{const t={};for(const e of n.tables){const a=[{name:"_id",optional:!1,pk:!0,type:"id"},{name:"_creationTime",optional:!1,type:"number"},...e.commitOrdered===!0?[{name:"_commitSeq",optional:!1,type:"number"}]:[]],r=new Map(e.relations.flatMap(o=>o.kind==="one"?[[o.field,o.onDelete]]:[]));for(const[o,d]of Object.entries(e.shape))a.push(Ur(o,d,r));t[e.name]=a}return t},jr=({hasMutators:n,hasSensitiveShapes:t,hasShapes:e,rlsData:a})=>{const r="{ columns?: readonly string[]; effectiveWhere?: WhereInput; global?: boolean; table: string } | undefined",o=t?` protected override resolveShape(name: string, args: Record<string, unknown>, identity?: SubscriptionIdentity): ${r} {
950
+ const sensitive = this.openSensitiveScope(\`__shape__:\${name}\`, args);
951
+
952
+ try {
953
+ return this.resolveShapeWithScope(name, args, identity, sensitive);
954
+ } catch (error) {
955
+ throw this.maskSensitiveError(error, sensitive);
956
+ } finally {
957
+ sensitive.clear();
958
+ }
959
+ }
960
+
961
+ private resolveShapeWithScope(name: string, args: Record<string, unknown>, identity: SubscriptionIdentity | undefined, sensitive: SensitiveScope): ${r} {`:` protected override resolveShape(name: string, args: Record<string, unknown>, identity?: SubscriptionIdentity): ${r} {`,m=e?`
962
+ ${o}
963
+ const shape = LUNORA_SHAPES[name];
964
+
965
+ if (!shape) {
966
+ return undefined;
967
+ }
968
+
969
+ this.ensureServable();
970
+
971
+ // Trusted ctx from the socket's OWN verified identity — the client
972
+ // supplies only the shape name + args; \`compileWhere\` validates the
973
+ // args then runs the shape's \`where(ctx, args)\` under that identity,
974
+ // so which rows replicate is a server decision (reads-as-permissions).
975
+ const ctx = this.buildCtx({ functionPath: \`__shape__:\${name}\`, identity${t?", sensitive":""} });
976
+ // \`ownerField\` resolves an \`owner: true\` shape: only here is the table's
977
+ // \`.ownedBy(field)\` column in scope, so the DO hands it to the shape
978
+ // rather than every shape restating the ownership check in its \`where\`.
979
+ const ownerField = (schema as unknown as { tables: Record<string, { ownerField?: string }> }).tables[shape.table]?.ownerField;
980
+ const shapeWhere = shape.compileWhere(ctx, args, ownerField === undefined ? {} : { ownerField }) as unknown as WhereInput;
981
+
982
+ // AND-compose with the read policies this SHAPE declares
983
+ // (\`defineShape({ use: [guard] })\`, pre-indexed into
984
+ // \`shape.rlsRegistry\`). A shape runs no procedure, so the
985
+ // \`.use(rls(...))\` middleware never fires; without this merge its
986
+ // reads would bypass the read policies it opted into.
987
+ // \`composeShapeReadWhere\` evaluates them under this same trusted ctx
988
+ // and fails closed under a \`.rls("required")\` schema for a
989
+ // non-\`.public()\` table the shape declared no read policy for.
990
+ //
991
+ // The registry is the shape's OWN — never one folded from every
992
+ // registered function. A project-wide union let one admin-only
993
+ // procedure's allow-all read policy unrestrict every shape on the
994
+ // table; see the SCOPE note in \`shape-read-base.ts\`.
995
+ //
996
+ // It is NOT identical to the request-time path, and must not be
997
+ // described as such: roles come from the identity's \`roles\` claim
998
+ // only, because no middleware runs here to contribute
999
+ // \`ctx.auth.roles\`. Derive roles at the identity if a policy gates
1000
+ // on them — see \`shape-read-base.ts\`.
1001
+ const effectiveWhere = composeShapeReadWhere(shape.rlsRegistry, {
1002
+ ctx,
1003
+ identity: identity?.identity ?? null,
1004
+ rlsRequired: (schema as unknown as { rlsMode?: string }).rlsMode === "required",
1005
+ shapeWhere,
1006
+ table: shape.table,
1007
+ tablePublic: (schema as unknown as { tables: Record<string, { isPublic?: boolean }> }).tables[shape.table]?.isPublic === true,
1008
+ userId: identity?.userId ?? null,
1009
+ });
1010
+
1011
+ // A live shape pokes only from its OWN shard's op-log, so a \`where()\`
1012
+ // that joins to a \`.shardBy()\` table (rows in another DO) is rejected
1013
+ // here — the first point the compiled predicate + shard modes are both
1014
+ // known. Remedy: denormalize, or move the joined table to \`.global()\`.
1015
+ assertShapeShardable(effectiveWhere, schema as unknown as SchemaLike, shape.table);
1016
+
1017
+ // A \`.global()\` table lives in D1 (no per-DO op-log): flag it so the
1018
+ // base serves it through the latency-tiered poll path (\`readGlobalShapeRows\`)
1019
+ // instead of the CDC poke path.
1020
+ const isGlobal = (schema as unknown as SchemaLike).tables[shape.table]?.shardMode?.kind === "global";
1021
+
1022
+ return { ...(shape.columns === undefined ? {} : { columns: shape.columns }), effectiveWhere, global: isGlobal, table: shape.table };
1023
+ }
1024
+ `:"",f=e?[...new Set(a.policies.filter(c=>c.on==="read"&&c.table!=="").map(c=>c.table))].toSorted((c,w)=>c.localeCompare(w)):[],h=f.length>0?`
1025
+ /** Refuses to boot a shape that would replicate around the read policies its table is governed by (\`defineShape({ use })\` is how a shape opts in; \`use: []\` acknowledges an ungoverned one). */
1026
+ assertShapesDeclareReadPolicies(LUNORA_SHAPES, ${JSON.stringify(f)}, (schema as unknown as { rlsMode?: string }).rlsMode === "required");
1027
+ `:"";return{customMutatorOverride:n?`
1028
+ protected override isCustomMutator(functionPath: string): boolean {
1029
+ return LUNORA_MUTATOR_PATHS.has(functionPath);
1030
+ }
1031
+ `:"",shapeGuardImport:e?"assertShapeShardable, ":"",shapeReadPolicyAssertion:h,shapeReadPolicyImport:h===""?"":"assertShapesDeclareReadPolicies, ",shapeResolveOverride:m}},Wr=(n,t)=>n?`
1032
+ protected override async readGlobalShapeRows(resolved: { columns?: readonly string[]; effectiveWhere?: WhereInput; global?: boolean; table: string }, identity?: SubscriptionIdentity): Promise<Array<{ doc: Record<string, unknown>; id: string }>> {
1033
+ const env = this.env as Record<string, unknown>;
1034
+ // A shape read performs no writes, so the widened request only needs
1035
+ // the inbound bookmark (pin the membership drain to the caller's own
1036
+ // prior writes) — no \`onBookmark\` here.
1037
+ //
1038
+ // The two identity members are named rather than spread, matching the
1039
+ // dispatch-path request built in \`buildCtx\`. A spread would also hand
1040
+ // the global writer \`ip\`, and \`globalShapeReadKey\` — the per-flush
1041
+ // cache in front of this read — keys on identity + userId only. Equal
1042
+ // keys must mean equal rows, so a field the writer can scope by has to
1043
+ // be either in the key or out of the request; it is out.
1044
+ const bookmark = this.getInboundBookmark();
1045
+ const globalRequest = {
1046
+ ...this.globalCdcOptions(config.cdc ?? false),
1047
+ ...(bookmark === undefined ? {} : { bookmark }),
1048
+ ...(identity?.identity === undefined ? {} : { identity: identity.identity }),
1049
+ ...(identity?.userId === undefined ? {} : { userId: identity.userId }),
1050
+ };
1051
+ const globalDb: DatabaseWriterLike = ${t}?.(env, globalRequest) ?? globalDbStub;
1052
+ const rows: Array<{ doc: Record<string, unknown>; id: string }> = [];
1053
+
1054
+ let cursor: null | string = null;
1055
+
1056
+ // Drain every page of the global membership so the seed/diff sees the
1057
+ // full rowset (D1 \`findMany\` is paginated). Stop one row past the cap:
1058
+ // a broad \`.global()\` shape would otherwise materialize an unbounded
1059
+ // array before the caller's \`withinGlobalShapeBound\` check rejects it,
1060
+ // so bail early and let the caller fail it closed.
1061
+ do {
1062
+ // eslint-disable-next-line no-await-in-loop -- sequential page drain to assemble the full membership
1063
+ const page = await globalDb.findMany(resolved.table, { cursor, ...(resolved.effectiveWhere === undefined ? {} : { where: resolved.effectiveWhere }) });
1064
+
1065
+ for (const doc of page.page) {
1066
+ rows.push({ doc, id: String((doc as { _id?: unknown })._id) });
1067
+
1068
+ if (rows.length > ShardDOBase.GLOBAL_SHAPE_MAX_ROWS) {
1069
+ return rows;
1070
+ }
1071
+ }
1072
+
1073
+ cursor = page.isDone ? null : page.continueCursor;
1074
+ } while (cursor !== null);
1075
+
1076
+ return rows;
1077
+ }
1078
+
1079
+ protected override async readGlobalChangedTables(sinceSeq: number, cursorOnly?: boolean): Promise<{ cursor: number; floor?: number; tables: string[] } | undefined> {
1080
+ const env = this.env as Record<string, unknown>;
1081
+ // Metadata-only: this asks the global changelog which TABLES moved,
1082
+ // never what changed in them, so it costs one small read per poll tick
1083
+ // for the whole shard — and a tick whose answer omits a shape's table
1084
+ // skips that shape's membership drain entirely.
1085
+ // Named local, not an inline object literal — same reason as the
1086
+ // dispatch path and \`readGlobalShapeRows\`: \`bookmark\` is not declared
1087
+ // on the narrower Hyperdrive thunk's \`request\` type, and an inline
1088
+ // literal trips an excess-property error (TS2353) that makes the
1089
+ // emitted \`shard.ts\` uncompilable for every Hyperdrive-global app with
1090
+ // a \`defineShape\`. The Hyperdrive factory simply never reads it.
1091
+ const bookmark = this.getInboundBookmark();
1092
+ const globalRequest = { ...this.globalCdcOptions(config.cdc ?? false), ...(bookmark === undefined ? {} : { bookmark }) };
1093
+ const globalDb: DatabaseWriterLike = ${t}?.(env, globalRequest) ?? globalDbStub;
1094
+
1095
+ return globalDb.cdcChangedTables?.(sinceSeq, cursorOnly === undefined ? {} : { cursorOnly });
1096
+ }
1097
+ `:"",Kr=n=>({externalSourceOverride:n?`
1098
+ protected override async pollExternalSources(trace?: TraceRefLike): Promise<number | undefined> {
1099
+ const env = (this.env ?? {}) as Record<string, unknown>;
1100
+ const sourced = Object.entries((schema as unknown as SchemaLike).tables)
1101
+ .map(([table, definition]) => [table, (definition as { externalSource?: ExternalSourceLike }).externalSource] as const)
1102
+ .filter((entry): entry is [string, ExternalSourceLike] => entry[1] !== undefined);
1103
+
1104
+ if (sourced.length === 0) {
1105
+ return undefined;
1106
+ }
1107
+
1108
+ const shardKey = this.currentShardKey();
1109
+ const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
1110
+
1111
+ let clients = sourceClientCache.get(this);
1112
+
1113
+ if (clients === undefined) {
1114
+ clients = new Map();
1115
+ sourceClientCache.set(this, clients);
1116
+ }
1117
+
1118
+ let polledAt = sourcePollAtCache.get(this);
1119
+
1120
+ if (polledAt === undefined) {
1121
+ polledAt = new Map();
1122
+ sourcePollAtCache.set(this, polledAt);
1123
+ }
1124
+
1125
+ const now = Date.now();
1126
+ // \`nextDueAt\` tracks the EARLIEST next-due timestamp across every
1127
+ // non-manual source; the shared alarm re-arms there instead of the
1128
+ // fixed 2 s global-shape floor, so a large \`refresh.everyMs\` actually
1129
+ // sleeps until it's due. Stays \`undefined\` when every source is
1130
+ // \`refresh: "manual"\`, so the shared alarm goes idle for this tier.
1131
+ let nextDueAt: number | undefined;
1132
+
1133
+ for (const [table, source] of sourced) {
1134
+ if (source.refresh === "manual") {
1135
+ continue;
1136
+ }
1137
+
1138
+ if (isSourceDue(source.refresh, polledAt.get(table), now)) {
1139
+ try {
1140
+ let client = clients.get(source.binding);
1141
+
1142
+ if (client === undefined) {
1143
+ client = config.sourceClient?.(env, source.binding);
1144
+
1145
+ if (client !== undefined) {
1146
+ clients.set(source.binding, client);
1147
+ }
1148
+ }
1149
+
1150
+ if (client === undefined) {
1151
+ // No SqlClient resolved for this binding (host never wired
1152
+ // \`config.sourceClient\`, or wired it wrong). Surface it in the
1153
+ // Logs panel and stamp \`polledAt\` so a persistent misconfig backs
1154
+ // off to \`refresh.everyMs\` instead of retrying every alarm tick.
1155
+ this.recordExternalSourceError(table, new Error(\`external-source: no sourceClient resolved for binding "\${source.binding}"\`), trace);
1156
+ } else {
1157
+ // A FRESH tracker per TABLE, not one shared across the whole
1158
+ // loop: one table's runaway pull must not spend a budget a
1159
+ // sibling table needs, and a limit hit here must not block
1160
+ // that sibling from getting its own full budget this tick.
1161
+ const writer = createShardCtxDb({
1162
+ // Admin and maintenance writes go through the SAME reactive-cache hooks as
1163
+ // a user mutation. Without this, a studio row edit, a TTL sweep, an admin
1164
+ // import, a CDC apply or a data-migration backfill writes without
1165
+ // invalidating, and the next query answers from the pre-write snapshot.
1166
+ ...this.ctxDbTuning(),
1167
+ broadcast: (delta) => {
1168
+ this.recordChangedTable(delta.table, delta.indexKeys);
1169
+ },
1170
+ cdc: config.cdc ?? false,
1171
+ headroom: this.alarmHeadroom(),
1172
+ scheduler,
1173
+ schema: schema as unknown as SchemaLike,
1174
+ sql: this.sql as SqlExec,
1175
+ });
1176
+
1177
+ if (source.mode === "incremental") {
1178
+ // Incremental (plan 136): pull only rows past the durable
1179
+ // watermark (or a full-pull seed/reconcile), upsert-only.
1180
+ // eslint-disable-next-line no-await-in-loop -- one sourced table at a time; sequential keeps the writer transaction simple
1181
+ await pullExternalSourceIncrementalTick(this.sql as SqlExec, writer, client, table, source, shardKey, now);
1182
+ } else {
1183
+ // eslint-disable-next-line no-await-in-loop -- one sourced table at a time; slices are independent but small and sequential keeps the writer transaction simple
1184
+ await pullExternalSourceTick(this.sql as SqlExec, writer, client, table, source, shardKey);
1185
+ }
1186
+ }
1187
+
1188
+ // Timestamp AFTER the poll finishes, not the batch-start \`now\` — a
1189
+ // poll that outruns \`everyMs\` must not make \`nextDueAt\` (below)
1190
+ // stale-immediate and re-arm the alarm in a hammering loop.
1191
+ polledAt.set(table, Date.now());
1192
+ } catch (error) {
1193
+ if (error instanceof LunoraError && error.code === "TRANSACTION_LIMIT_EXCEEDED") {
1194
+ // Batch full, not a genuine failure. Incremental mode only
1195
+ // persists its watermark AFTER a full apply (see
1196
+ // \`pullExternalSourceIncrementalTick\`), so this throw left it
1197
+ // untouched — the next tick safely re-pulls/re-applies the
1198
+ // SAME slice (idempotent upsert). Full-pull mode (and an
1199
+ // incremental source's own occasional full-pull/reconcile
1200
+ // sweep) has no resumable cursor at all — deliberately not
1201
+ // inventing one here; a retry just redoes the whole pull,
1202
+ // safe if wasteful. Either way: warn instead of
1203
+ // \`recordExternalSourceError\`, and leave \`polledAt\`
1204
+ // UNCHANGED (skip the stamp below via \`continue\`) so this
1205
+ // table stays "due" and \`nextPollAlarmTarget\`'s existing
1206
+ // due-now floor re-arms the shared alarm promptly — not a
1207
+ // fresh \`setAlarm(now)\`.
1208
+ this.recordExternalSourceWarning(
1209
+ table,
1210
+ \`external-source poll for "\${table}" hit the transaction limit mid-batch; resuming next tick: \${error.message}\`,
1211
+ trace,
1212
+ );
1213
+
1214
+ nextDueAt = nextDueAt === undefined ? now : Math.min(nextDueAt, now);
1215
+
1216
+ continue;
1217
+ }
1218
+
1219
+ this.recordExternalSourceError(table, error, trace);
1220
+ // Stamp on failure too, so a persistently failing source throttles
1221
+ // to \`refresh.everyMs\` rather than being hammered every tick.
1222
+ polledAt.set(table, Date.now());
1223
+ }
1224
+ }
1225
+
1226
+ // This source's own next-due time, read AFTER the poll-or-skip above
1227
+ // so a just-polled source reports \`now + everyMs\` (its FRESH due
1228
+ // time), not the stale pre-poll one. An omitted \`refresh\` (poll
1229
+ // every tick) is due again immediately.
1230
+ const sourceNextDueAt = source.refresh === undefined ? now : (polledAt.get(table) ?? now) + source.refresh.everyMs;
1231
+
1232
+ nextDueAt = nextDueAt === undefined ? sourceNextDueAt : Math.min(nextDueAt, sourceNextDueAt);
1233
+ }
1234
+
1235
+ return nextDueAt;
1236
+ }
1237
+ `:"",sourceBootstrap:n?`
1238
+ const autoSourced = Object.values((schema as unknown as SchemaLike).tables).some((definition) => {
1239
+ const source = (definition as { externalSource?: ExternalSourceLike }).externalSource;
1240
+
1241
+ return source !== undefined && source.refresh !== "manual";
1242
+ });
1243
+
1244
+ if (autoSourced) {
1245
+ void this.scheduleSourcePoll();
1246
+ }
1247
+ `:"",sourceClientCacheConst:n?`
1248
+ const sourceClientCache = new WeakMap<object, Map<string, SourceClientLike>>();
1249
+ const sourcePollAtCache = new WeakMap<object, Map<string, number>>();
1250
+ `:""}),Hr=(n,t)=>{const e=t.length>0,r=n?$("vectorsStub: VectorSearchLike","ctx.vectors: no vectors configured. Pass `vectors` to createShardDO().",["deleteByIds","getByIds","query","upsert","upsertMany","upsertNow"]):"",o=e?` const vectorShardKey = this.currentShardKey();
1251
+ `:"",d=e?"namespace: vectorShardKey === ROOT_SHARD_NAME ? undefined : vectorShardKey, ":"",f=`, { ${["deferAfterCommit: (work) => this.deferAfterCommit(work)",...e?["namespace: vectorShardKey === ROOT_SHARD_NAME ? undefined : vectorShardKey",`shardedIndexNames: [${t.map(S=>JSON.stringify(S)).join(", ")}]`]:[]].join(", ")} }`,h=n?`
1252
+ const vectorSync = this.vectorSync(env);
1253
+ // Vectorize lives outside this shard's SQLite, so a query whose result
1254
+ // depends on a similarity search cannot be proven current on reconnect —
1255
+ // an unrelated upsert (or a re-index) moves the matches without touching
1256
+ // \`__cdc_log\`. Only \`ctx.vectors\` is wrapped: the write-through
1257
+ // vector-sync hook keeps calling the bare facade.
1258
+ const bareVectors = vectorSync?.vectors ?? vectorsStub;
1259
+ const vectors = markUnvouchableReads(bareVectors, options.onRead, ["getByIds", "query"]);
1260
+ const onWrite = vectorSync?.onWrite;
1261
+ `:"";return{vectorSyncMethod:n?`
1262
+ private vectorSync(env: Record<string, unknown>): { backfill: VectorBackfillSync; onWrite: WriteHook; vectors: VectorSearchLike } | undefined {
1263
+ if (!config.vectors) {
1264
+ return undefined;
1265
+ }
1266
+
1267
+ const lunora = createVectors({ indexes: config.vectors(env) });
1268
+ ${o} const vectors = createContextVectors(lunora${f});
1269
+
1270
+ return {
1271
+ backfill: createVectorBackfillSync({ ${d}schema: schema as unknown as VectorSchemaLike, upsertMany: lunora.upsertMany, vectors }),
1272
+ onWrite: createVectorSyncHook({ ${d}schema: schema as unknown as VectorSchemaLike, vectors }),
1273
+ vectors,
1274
+ };
1275
+ }
1276
+
1277
+ // \`__lunora_admin__:backfillVectors\`: the rows that predate a vector index,
1278
+ // a page at a time, each page ordered on the after-commit chain so it
1279
+ // never lands over a newer write's vector.
1280
+ protected override async runShardVectorBackfill(options: { maxPages?: number; restart?: boolean }): Promise<VectorBackfillProgress> {
1281
+ this.ensureMigrated();
1282
+
1283
+ const vectorSync = this.vectorSync((this.env ?? {}) as Record<string, unknown>);
1284
+
1285
+ if (!vectorSync) {
1286
+ throw new LunoraError("NOT_IMPLEMENTED", "vector backfill is unavailable: no vectors configured. Pass \`vectors\` to createShardDO().");
1287
+ }
1288
+
1289
+ return backfillVectorIndexes(this.sql as SqlExec, vectorBackfillTargets(schema as unknown as VectorSchemaLike), vectorSync.backfill, {
1290
+ ...options,
1291
+ ordered: async (read, work) => this.runOrderedAfterWrites(read, work),
1292
+ });
1293
+ }
1294
+ `:"",vectorsBuild:h,vectorsStub:r}},on=({advisories:n=[],advisorProcedures:t=[],agents:e=[],capabilities:a=new Set,containers:r=[],env:o,flagKeys:d=[],hasFlags:m=!1,hasNotify:f=!1,hasRooms:h=!1,hasSecretValues:x=!1,hasSensitiveArgs:S=!1,hasSockets:g=!1,lazyFunctions:c=!1,hasVectors:w=!0,liveStreamDeclarations:M=[],liveStreams:A=!1,maskMetadata:q,mutators:F=[],queues:R=[],rlsMetadata:U,schema:i,schemaSnapshot:v,shapes:E=[],storageRules:B,studioFeatures:I,services:C=[],topics:j=[],useUmbrella:W=!1,workflows:O=[]})=>{const l=Rr(W),K=F.length>0,p=E.length>0,D=E.some(s=>yr(s.args).length>0),H=S||D,fe=v===void 0?"":Jt(v),ge=v===void 0?"":`
1295
+ /** Structural schema snapshot + its content hash, recorded in the shard's \`__lunora_schema_history\` ledger on cold start so the studio can show a schema-version timeline and diff any two versions. */
1296
+ const LUNORA_SCHEMA_SNAPSHOT: { hash: string; json: string } = { hash: ${JSON.stringify(Yt(v))}, json: ${JSON.stringify(fe)} };
1297
+ `,be=v===void 0?"":", schemaSnapshot: LUNORA_SCHEMA_SNAPSHOT",k=er(a),L=tr(m,l.flags),V=rr(d,m,l.flags),G=gr(f),Q=nr(o),{build:ye,contextField:we,importLines:ve,specs:ke}=ar(xr(R)),{build:Se,contextField:Re,importLines:xe,specs:Ae}=sr(j,R),J=or(C,l.server),{build:Oe,contextField:De,importLines:Te,specs:_e}=ir(r,i.jurisdiction),{build:Ee,contextField:Ie,importLines:Ce,specs:Le}=dr(O),{build:Pe,contextField:Ne,importLines:$e,specs:Me}=cr(e,x&&e.length>0),{build:qe,configField:Fe,contextField:Ue,imports:Be,stub:je}=lr(a.has("payments")),We=n,{advisoryRules:Ke,advisoryRows:He}=Or(We),Ve=t,ne=U??{policies:[],roles:[]},Ge=q??{columns:[]},Qe=B??{rules:[]},Je=I??{analytics:!1,auth:!1,containers:!1,flags:!1,kv:!1,mail:!1,notifications:!1,payments:!1,privacy:!1,queues:!1,scheduler:!1,storage:!1,vectors:!1,workflows:!1},{constant:Ye,override:ze}=ur(O),{constant:Xe,override:Ze}=hr(R),b=w&&i.vectorIndexes.length>0,Y=new Set(i.tables.filter(s=>Ar(s)).map(s=>s.name)),ae=b&&i.vectorIndexes.some(s=>Y.has(s.table)),T=i.tables.some(s=>s.shardMode==="global"),z=pr(Y),X=i.tables.some(s=>zt(s)),se=i.tables.some(s=>Xt(s)),P=i.tables.some(s=>s.externalSource!==void 0),oe=i.tables.some(s=>s.memory===!0),Z=i.tables.some(s=>s.shardMode!=="global"&&s.searchIndexes.length>0);if(se&&X)throw new he("INTERNAL",'lunora codegen: mixing `.global()` (D1) and `.global({ backend: "hyperdrive" })` tables in one app is not supported yet — use a single global backend.');const ee=i.tables.length>0,et=Nr(i),tt=Mr(i),rt=Br(i),nt=$r(i),ie=qr(i),te=mr(b,O.length>0,R.length>0,m,Z);p&&te.push("WhereInput"),D&&te.push("SensitiveScope");const de=fr(T),{customMutatorOverride:at,shapeGuardImport:st,shapeReadPolicyAssertion:ot,shapeReadPolicyImport:it,shapeResolveOverride:dt}=jr({hasMutators:K,hasSensitiveShapes:D,hasShapes:p,rlsData:ne}),_=A?M:[];for(const s of _)if(wr(s.exportName,"live stream export name"),!vr.test(s.filePath))throw new he("INTERNAL",`@lunora/codegen: refusing to emit import for unsafe live stream file path: ${JSON.stringify(s.filePath)}`);const ct=c?[]:_.map(s=>`import { ${s.exportName} as liveStream_${s.exportName} } from "../${s.filePath}.js";`),lt=` protected override liveStreams() {
1298
+ return { ${_.map(s=>`${kr(s.id)}: liveStream_${s.exportName}`).join(", ")} };
1299
+ }
1300
+ `,ut=` private readonly liveStreamLoaders = new Map<string, () => Promise<{ validatePublish: (topic: string, payload: unknown) => unknown }>>([
1301
+ ${_.map(s=>` [${JSON.stringify(s.id)}, () => import("../${s.filePath}.js").then((module) => module.${s.exportName})],`).join(`
1302
+ `)}
1303
+ ]);
1304
+
1305
+ private readonly loadedLiveStreams = new Map<string, { validatePublish: (topic: string, payload: unknown) => unknown }>();
1306
+
1307
+ protected override loadLiveStream(stream: string): Promise<void> | undefined {
1308
+ const load = this.loadedLiveStreams.has(stream) ? undefined : this.liveStreamLoaders.get(stream);
1309
+
1310
+ return load === undefined
1311
+ ? undefined
1312
+ : load().then((declaration) => {
1313
+ this.loadedLiveStreams.set(stream, declaration);
1314
+ });
1315
+ }
1316
+
1317
+ // Only the streams a publish has already loaded: a stream not yet loaded was either never published to or is
1318
+ // unknown, and either way \`declaredLivePayload\` refuses what this does not hold.
1319
+ protected override liveStreams() {
1320
+ return Object.fromEntries(this.loadedLiveStreams);
1321
+ }
1322
+ `;let ce="";_.length>0&&(ce=c?ut:lt);const N=[`import type { ${te.join(", ")} } from "${l.do}";`,`import { applyCdcChanges, createStableIdFactory, createStableRandomFactory, ${Z?"backfillSearchIndexes, ":""}${b?"backfillVectorIndexes, ":""}buildReprojectionMigration, ${oe?"clearMemoryTables, ":""}${st}createReadFootprint, createShardCtxDb, exportShardRows, importShardRows, ${P?"isSourceDue, pullExternalSourceIncrementalTick, pullExternalSourceTick, ":""}markUnvouchableReads, ${ae?"ROOT_SHARD_NAME, ":""}runDataMigration, runShardMigrations, ${de.importFragment}ShardDO as ShardDOBase } from "${l.do}";`,...P?[`import type { ExternalSourceLike, SourceClientLike, TraceRefLike } from "${l.do}";`]:[],p?`import { asBucketStorage, ${it}beginDeferredDeletes, beginDeferredSchedules, composeShapeReadWhere, createSecrets, flushDeferredDeletes, LunoraError, withDeferredDeletes, withDeferredSchedules } from "${l.server}";`:`import { asBucketStorage, beginDeferredDeletes, beginDeferredSchedules, createSecrets, flushDeferredDeletes, LunoraError, withDeferredDeletes, withDeferredSchedules } from "${l.server}";`];ee&&N.push(`import { bindOrm, bindTableFacade } from "${l.server}";`),b&&N.push('import type { SchemaLike as VectorSchemaLike, VectorBackfillSync, VectorizeIndexLike, VectorSearchLike } from "@lunora/bindings/vectors";','import { createContextVectors, createVectorBackfillSync, createVectors, createVectorSyncHook, vectorBackfillTargets } from "@lunora/bindings/vectors";'),N.push(...k.importLines,...L.importLines,...G.importLines,...Q.importLines,...Te,...Ce,...ve,...xe,...J.importLines,...$e,...Be,...H?[`import { sensitiveArgumentNames } from "${l.values}";`]:[],"",'import schema from "../schema.js";',...ct,`import { ${["LUNORA_FUNCTIONS","LUNORA_LIFECYCLE_HOOKS","LUNORA_MIGRATIONS",...c?["getLoadedFunction","loadFunction"]:[],...K?["LUNORA_MUTATOR_PATHS"]:[],...h?["LUNORA_ROOMS"]:[],...g?["LUNORA_SOCKETS"]:[],...p?["LUNORA_SHAPES"]:[]].join(", ")} } from "./functions.js";`);const ht=b?`
1323
+ vectors?: (env: Record<string, unknown>) => Record<string, VectorizeIndexLike>;`:"",pt=se?`
1324
+ d1?: (env: Record<string, unknown>, request?: { bookmark?: string; cdc?: boolean; cdcRetentionMs?: number; identity?: Record<string, unknown>; onBookmark?: (bookmark: string | undefined) => void; userId?: string }) => DatabaseWriterLike | undefined;`:"",mt=X?`
1325
+ hyperdriveGlobal?: (env: Record<string, unknown>, request?: { cdc?: boolean; cdcRetentionMs?: number; identity?: Record<string, unknown>; userId?: string }) => DatabaseWriterLike | undefined;`:"",ft=P?`
1326
+ sourceClient?: (env: Record<string, unknown>, binding: string) => { query: <Row = Record<string, unknown>>(text: string, params?: readonly unknown[]) => Promise<Row[]> } | undefined;`:"",gt="ctx.db.<globalTable>: no global backend configured. Pass `d1` or `hyperdriveGlobal` to createShardDO().",bt="ctx.scheduler: no scheduler configured. Pass `scheduler` to createShardDO().",yt="ctx.storage: no storage configured. Pass `storage` to createShardDO().",wt=T?$("globalDbStub: DatabaseWriterLike",gt,["aggregate","count","delete","explain","findFirst","findFirstOrThrow","findMany","get","groupBy","insert","normalizeId","patch","query","rank","rankPage","replace"],{sync:["normalizeId","query"]}):"",{vectorSyncMethod:vt,vectorsBuild:kt,vectorsStub:St}=Hr(b,ae?i.vectorIndexes.filter(s=>Y.has(s.table)).map(s=>s.name):[]),Rt=`{
1327
+ ...this.ctxDbTuning(),
1328
+ auth: { identity: identity ?? null, userId: userId ?? null },
1329
+ broadcast: (delta) => {
1330
+ this.recordChangedTable(delta.table, delta.indexKeys);
1331
+ },
1332
+ // Read at call time, not captured: the baseline belongs to the
1333
+ // dispatch in flight, and a queued mutation admitted after a
1334
+ // sibling's prologue must be judged against ITS caller's cursor.
1335
+ baselineSeq: () => this.getCurrentBaselineSeq(),
1336
+ cdc: config.cdc ?? false,
1337
+ // A dispatch with NO caller identity runs system-trusted: RLS scopes
1338
+ // rows to a user, and these have no user to scope to. This is the tier
1339
+ // migrations, imports and the external-source poll loop already run in
1340
+ // (see \`adminWriterPrelude\`). Only \`onShardInit\` and \`onQueryChange\`
1341
+ // reach it; \`onConnect\`/\`onDisconnect\` carry a verified identity and
1342
+ // stay guarded like any request.
1343
+ enforceRls: options.trusted !== true,
1344
+ headroom: options.headroom ?? this.transactionHeadroom(),
1345
+ // A live predicate, not a flag: the transaction opens AFTER this ctx
1346
+ // is built (a mutation dispatch wraps the handler), so only a call-time
1347
+ // read reports the truth. \`_commitSeq\` reuses one sequence across
1348
+ // writes only while they commit together — an action is not wrapped, so
1349
+ // each of its writes allocates its own.
1350
+ inTransaction: () => this.isInTransaction(),
1351
+ onIndexUse: this.getCtxDbIndexUseHook(),
1352
+ onStalePatchDropped: (event) => {
1353
+ this.recordStalePatchDropped(event);
1354
+ },
1355
+ // Bound to THIS dispatch's reactive-cache read scope (\`handleRpc\`
1356
+ // threads it in), so two concurrent queries never stamp each
1357
+ // other's dep sets. \`executeSubscription\` overrides both with its
1358
+ // own \`ReadFootprint\` pair instead, and a dispatch with neither
1359
+ // gets an unbound hook that stamps no deps.
1360
+ onRead: options.onRead ?? this.getCtxDbReadHook(options.scope),
1361
+ // Omitted when there is no scope so \`createShardCtxDb\`
1362
+ // keeps its own "degrade a provable slice to a whole-table dep"
1363
+ // fallback rather than reporting into a no-op.
1364
+ ...(options.onReadRange !== undefined
1365
+ ? { onReadRange: options.onReadRange }
1366
+ : options.scope === undefined ? {} : { onReadRange: this.getCtxDbReadRangeHook(options.scope) }),${b?`
1367
+ // Vectorize is outside this shard's SQLite and cannot roll back, so
1368
+ // the sync is held until the mutation's transaction has COMMITTED
1369
+ // (\`deferAfterCommit\`) instead of running inline. Inline, a write
1370
+ // that later aborted left a vector pointing at a row that does not
1371
+ // exist — which a search then surfaces — and a rolled-back delete
1372
+ // left the row with its vector already purged. Outside a
1373
+ // transaction (an action) nothing is open and it runs at once.
1374
+ ...(onWrite === undefined ? {} : { onWrite: (event) => this.deferAfterCommit(() => onWrite(event)) }),`:""}
1375
+ scheduler,
1376
+ schema: schema as unknown as SchemaLike,
1377
+ sql: this.sql as SqlExec,
1378
+ storage,${T?`
1379
+ globalDb,`:""}
1380
+ }`,xt=b?`
1381
+ vectors,`:"",At=ee?`
1382
+ orm: bindOrm(facade),`:"",le=X?"config.hyperdriveGlobal":"config.d1",Ot=T?` const bookmark = this.getInboundBookmark();
1383
+ const globalRequest = {
1384
+ ...this.globalCdcOptions(config.cdc ?? false),
1385
+ ...(bookmark === undefined ? {} : { bookmark }),
1386
+ ...(identity === undefined ? {} : { identity }),
1387
+ onBookmark: (bookmarkValue: string | undefined) => { if (options.bookmarks !== undefined) { options.bookmarks.value = bookmarkValue; } },
1388
+ ...(userId === undefined ? {} : { userId }),
1389
+ };
1390
+ const globalDb: DatabaseWriterLike = ${le}?.(env, globalRequest) ?? globalDbStub;
1391
+ `:"",Dt=Wr(p&&T,le),Tt=h?` protected override roomHandlerPath(topic: string): string | undefined {
1392
+ // Own properties only: a plain object would resolve \`toString\` and the like to inherited functions.
1393
+ return Object.hasOwn(LUNORA_ROOMS, topic) ? LUNORA_ROOMS[topic] : undefined;
1394
+ }
1395
+ `:"",_t=g?` protected override socketRoute(path: string): { auth: boolean; functionPath: string } | undefined {
1396
+ return LUNORA_SOCKETS[path];
1397
+ }
1398
+ `:"",{externalSourceOverride:Et,sourceBootstrap:It,sourceClientCacheConst:Ct}=Kr(P),Lt=`
1399
+ private shardInitDeferred = false;
1400
+ private deferredShardInit?: Promise<void>;
1401
+
1402
+ protected override async runShardInit(): Promise<void> {
1403
+ this.ensureMigrated();
1404
+ ${oe?` clearMemoryTables(this.sql as SqlExec, schema as unknown as SchemaLike);
1405
+ `:""}
1406
+ if (this.blockedUniqueIndexes.length > 0) {
1407
+ this.shardInitDeferred = true;
1408
+
1409
+ return;
1410
+ }
1411
+
1412
+ await this.dispatchShardInit();
1413
+ }
1414
+
1415
+ protected override async ensureShardInit(): Promise<void> {
1416
+ await super.ensureShardInit();
1417
+
1418
+ if (!this.shardInitDeferred) {
1419
+ return;
1420
+ }
1421
+
1422
+ try {
1423
+ this.ensureMigrated();
1424
+ } catch (error: unknown) {
1425
+ this.recordShardInitError("__shard_init__", error);
1426
+
1427
+ return;
1428
+ }
1429
+
1430
+ if (this.blockedUniqueIndexes.length > 0) {
1431
+ return;
1432
+ }
1433
+
1434
+ this.deferredShardInit ??= (async (): Promise<void> => {
1435
+ try {
1436
+ await this.dispatchShardInit();
1437
+ } finally {
1438
+ this.shardInitDeferred = false;
1439
+ }
1440
+ })();
1441
+
1442
+ await this.deferredShardInit;
1443
+ }
1444
+ `,Pt=ie.length>0?`
1445
+ if (LUNORA_TTL_SWEEPS.length > 0) {
1446
+ void this.scheduleTtlSweep();
1447
+ }
1448
+ `:"",Nt=`
1449
+ public constructor(state: ShardDOState, env: unknown) {
1450
+ super(state, env, {
1451
+ // Every writer this file builds spreads the ctxDbTuning() slice —
1452
+ // the user-facing ctx and all three admin/maintenance writers — so
1453
+ // the base class can drop its coarse invalidation backstop. Only the
1454
+ // emitter can know that; a hand-written subclass leaves this unset
1455
+ // and keeps the backstop.
1456
+ ctxDbCacheWired: true,
1457
+ ...(config.deliveryAuthorization === undefined ? {} : { deliveryAuthorization: config.deliveryAuthorization((env ?? {}) as Record<string, unknown>) }),
1458
+ ...(config.maxRelationKeys === undefined ? {} : { maxRelationKeys: config.maxRelationKeys }),
1459
+ ...(config.reactiveCache ? { reactiveCache: config.reactiveCache === true ? {} : config.reactiveCache } : {}),
1460
+ ...(config.relationExistsPushDown === undefined ? {} : { relationExistsPushDown: config.relationExistsPushDown }),
1461
+ // The same sink \`buildCtx\` resolves, made reachable before any ctx
1462
+ // exists — a trigger that fails first still flushes and fuses.
1463
+ ...(config.observability === undefined ? {} : { telemetrySink: () => config.observability?.((env ?? {}) as Record<string, unknown>) }),
1464
+ });
1465
+ ${It}${Pt} }
1466
+
1467
+ protected override isCacheableQuery(functionPath: string): boolean {
1468
+ // NOT \`kind === "query"\` alone. A cache hit answers without running
1469
+ // \`handleRpc\`, so the \`internal\` refusal at the top of it is skipped —
1470
+ // and an \`internalQuery\` primed by a trusted system dispatch was then
1471
+ // served to an anonymous client that the refusal would have 404'd.
1472
+ //
1473
+ // \`perDispatch\` is the same argument one layer out: the procedure's
1474
+ // own \`.use()\` chain runs INSIDE \`handleRpc\`, so a hit skips it too.
1475
+ // A \`.use(rateLimit(...))\` query was therefore charged once and then
1476
+ // served from the memo to every later request — each of which still
1477
+ // reached this Durable Object and still cost it a dispatch, so only
1478
+ // the accounting was skipped. Metering and memoizing are mutually
1479
+ // exclusive; the author asking for the first is choosing against the
1480
+ // second.
1481
+ const registered = LUNORA_FUNCTIONS[functionPath];
1482
+
1483
+ return registered?.kind === "query" && registered.visibility !== "internal" && registered.perDispatch !== true;
1484
+ }
1485
+
1486
+ protected override isPaidFunction(functionPath: string): boolean {
1487
+ return LUNORA_FUNCTIONS[functionPath]?.x402 !== undefined;
1488
+ }
1489
+ `,ue=c?`
1490
+ protected override prepareFunction(functionPath: string, reason: "rpc" | "subscription" | "stream" | "reactor"): Promise<void> | undefined {
1491
+ const registered = LUNORA_FUNCTIONS[functionPath];
1492
+ const allowsInternal = reason === "reactor" || (reason === "rpc" && this.isSystemDispatch());
1493
+
1494
+ if (registered?.visibility === "internal" && !allowsInternal) {
1495
+ return;
1496
+ }
1497
+
1498
+ if (getLoadedFunction(functionPath) !== undefined) {
1499
+ return;
1500
+ }
1501
+
1502
+ return loadFunction(functionPath).then(() => undefined);
1503
+ }
1504
+ `:"",$t=ue===""?`
1505
+
1506
+ `:`${ue}
1507
+ `,Mt=H?`
1508
+ protected override sensitiveArgNames(functionPath: string): ReadonlySet<string> {
1509
+ const registered = LUNORA_FUNCTIONS[functionPath];${D?`
1510
+ const shape = functionPath.startsWith("__shape__:") ? LUNORA_SHAPES[functionPath.slice("__shape__:".length)] : undefined;`:""}
1511
+ const fromValidators = sensitiveArgumentNames(registered?.args ?? ${D?"shape?.args ?? ":""}{});
1512
+
1513
+ if (registered?.sensitiveArgs === undefined) {
1514
+ return fromValidators;
1515
+ }
1516
+
1517
+ return new Set([...registered.sensitiveArgs, ...fromValidators]);
1518
+ }
1519
+
1520
+ `:"",qt=Er(c),Ft=c?` const preparation = this.prepareFunction(functionPath, "subscription");
1521
+ if (preparation !== undefined) await preparation;
1522
+ `:"",Ut=Cr.replace("__PREPARE_FUNCTION__",Ft),Bt=Pr(h,c),jt=ee?`
1523
+ const facade = db as unknown as Record<string, ReturnType<typeof bindTableFacade>>;
1524
+ ${i.tables.map(s=>` facade[${JSON.stringify(s.name)}] = bindTableFacade(db, ${JSON.stringify(s.name)});`).join(`
1525
+ `)}
1526
+ `:"",Wt=`${k.everyBuild}${L.build}${Q.build}
1527
+ const secrets = createSecrets(env);
1528
+ `,Kt=`${k.everyFields}${L.contextField}${G.contextField}${Q.contextField}
1529
+ secrets,`,Ht=G.build,Vt=A?`
1530
+ const live = {
1531
+ publish: async (ref: { __lunoraLive: string; topic: string }, payload: unknown, publishOptions?: { shardKey?: string }): Promise<void> => {
1532
+ await this.publishLiveFromAction(ref.topic, payload, { ...publishOptions, stream: ref.__lunoraLive });
1533
+ },
1534
+ };
1535
+ `:"",Gt=[...k.actionFields,...C.length>0?["services"]:[],...A?["live"]:[]],Qt=`${k.actionBuild}${J.build}${Vt}`;return`${Sr}${N.join(`
1536
+ `)}
1537
+
1538
+ type FunctionKind = "action" | "mutation" | "query";
1539
+
1540
+ interface FunctionReference {
1541
+ __lunoraRef: string;
1542
+ }
1543
+
1544
+ /** Foreign-key columns per table (\`v.id("target")\` fields) for the data browser. */
1545
+ const LUNORA_TABLE_REFS = ${u(et,"Record<string, Record<string, string>>")};
1546
+
1547
+ /** Declared indexes per table (secondary, search, geo, rank, vector) for the schema viewer. */
1548
+ const LUNORA_TABLE_INDEXES = ${u(tt,'Record<string, Array<{ fields: string[]; name: string; type: "geo" | "index" | "rank" | "search" | "vector"; unique?: boolean }>>')};
1549
+
1550
+ /** Columns per table (typed, with PK/FK markers) for the studio's schema diagram, served via \`__lunora_admin__:describeTable\`. */
1551
+ const LUNORA_TABLE_COLUMNS = ${u(rt,`Record<
1552
+ string,
1553
+ Array<{
1554
+ bucket?: string;
1555
+ enumValues?: string[];
1556
+ isStorage?: boolean;
1557
+ name: string;
1558
+ nullable?: boolean;
1559
+ onDelete?: "cascade" | "restrict" | "set null";
1560
+ optional: boolean;
1561
+ pk?: boolean;
1562
+ ref?: string;
1563
+ type: string;
1564
+ }>
1565
+ >`)};
1566
+
1567
+ /** Storage-key columns per table (\`v.storage(...)\` fields) for the file browser's records↔files join. */
1568
+ const LUNORA_STORAGE_COLUMNS = ${u(nt,"Record<string, string[]>")};
1569
+
1570
+ /** Declarative TTL policies (\`.ttl(field, { after? })\`) the DO alarm sweep auto-expires rows for. */
1571
+ const LUNORA_TTL_SWEEPS = ${Zt(ie,"Array<{ after?: number; field: string; softDeleteField?: string; table: string }>")};
1572
+
1573
+ /** Static schema advisories (computed by @lunora/advisor at codegen time) served via \`__lunora_admin__:getAdvisories\`. */
1574
+ const LUNORA_ADVISORY_RULES = ${u(Ke,"Record<string, Partial<AdvisoryFinding>>")};
1575
+
1576
+ /** Per-finding rows; rule-level text lives once in \`LUNORA_ADVISORY_RULES\` and is merged back on first read. */
1577
+ const LUNORA_ADVISORY_ROWS = ${u(He,"Array<Partial<AdvisoryFinding> & { name: string }>")};
1578
+
1579
+ const LUNORA_ADVISORIES = ((): (() => AdvisoryFinding[]) => {
1580
+ let cached: AdvisoryFinding[] | undefined;
1581
+ return () => (cached ??= LUNORA_ADVISORY_ROWS().map((row) => ({ ...LUNORA_ADVISORY_RULES()[row.name], ...row }) as AdvisoryFinding));
1582
+ })();
1583
+
1584
+ /** Every declared procedure (discovered by @lunora/codegen) served via \`__lunora_admin__:getAdvisorProcedures\` — the health map's denominator. */
1585
+ const LUNORA_ADVISOR_PROCEDURES = ${u(Ve,"AdvisorProcedure[]")};
1586
+
1587
+ /** Read-only RLS metadata (policies + roles discovered from \`.use(rls(...))\` chains) served via \`__lunora_admin__:rlsPolicies\` for the studio's RLS inspector. */
1588
+ const LUNORA_RLS_METADATA = ${u(ne,"RlsPoliciesResult")};
1589
+ ${ot}
1590
+ /** Read-only masking metadata (table + column + strategy discovered from \`.use(mask(...))\` chains) served via \`__lunora_admin__:maskPolicies\` for the studio's data-browser mask preview. */
1591
+ const LUNORA_MASK_METADATA = ${u(Ge,"MaskPoliciesResult")};
1592
+
1593
+ /** Read-only storage access-rule metadata (discovered from \`.use(storageRules(...))\` chains) served via \`__lunora_admin__:storageRules\` for the studio's access-rules view. */
1594
+ const LUNORA_STORAGE_RULES = ${u(Qe,"StorageRulesResult")};
1595
+
1596
+ /** Which optional package-backed features this app wires up (discovered from imports / \`ctx.*\` reads / schema signals) served via \`__lunora_admin__:studioFeatures\` so the studio hides nav pages whose package isn't enabled. */
1597
+ const LUNORA_STUDIO_FEATURES = ${u(Je,"StudioFeaturesResult")};
1598
+
1599
+ /** How long a shard with a UNIQUE index blocked by duplicate rows serves the cached diagnostic before re-attempting the build. */
1600
+ const BLOCKED_MIGRATION_RETRY_MS = 5000;
1601
+ ${ge}${z.constant}${V.constant}${Ye}${Xe}${_e}${Le}${ke}${Ae}${J.specs}${Me}
1602
+ export interface ShardDOConfig {
1603
+ /** Opt into change-data-capture: records a post-image to \`__cdc_log\` on every write (backs streaming export + replay-PITR). */
1604
+ cdc?: boolean;
1605
+ /** Current authority for RPC and ordinary subscription deliveries; other live delivery modes are refused. */
1606
+ deliveryAuthorization?: (env: Record<string, unknown>) => DeliveryAuthorization;
1607
+ /** Ceiling on the join keys one relation-crossing \`where\` predicate may pre-resolve via semijoin before failing closed. Omit for the engine default. */
1608
+ maxRelationKeys?: number;
1609
+ /** Enable the per-shard reactive query cache: \`true\` for the defaults, or an options object to tune the caps. Query results are memoized by \`(functionPath, args, identity)\` and invalidated by the ctx-db write hooks before the subscription broadcast, so subscribers never observe a pre-write value. Omitted (or \`false\`) keeps every dispatch re-running its handler. */
1610
+ reactiveCache?: boolean | { maxBytes?: number; maxEntries?: number };
1611
+ /** Resolution policy for a relation-crossing \`where\` whose child is co-located in this shard: \`"auto"\` (cost-based, the engine default), \`"always"\` (inline correlated EXISTS) or \`"never"\` (universal semijoin). All three return identical rows. */
1612
+ relationExistsPushDown?: "always" | "auto" | "never";
1613
+ /** Optional telemetry sink. When supplied, each \`ctx.log.*\` call is forwarded to \`sink.onLog\`. Pass the SAME sink you give \`createWorker({ observability })\` (which drives \`onRpc\`) to route both RPC and log events. */
1614
+ observability?: (env: Record<string, unknown>) => TelemetrySink | undefined;
1615
+ /** \`unknown\` because \`@lunora/scheduler\`'s \`Scheduler\` is not assignable to \`SchedulerLike\`; the shard casts it. */
1616
+ scheduler?: (env: Record<string, unknown>) => unknown;
1617
+ /** \`origin\` is the origin the current \`/rpc\` request reached the worker on — the fallback base for signed object URLs when no \`publicBaseUrl\` is configured. \`undefined\` off the synchronous dispatch path. */
1618
+ storage?: (env: Record<string, unknown>, origin?: string) => unknown;${ht}${k.configFields}${L.configField}${Fe}${pt}${mt}${ft}${z.configField}
1619
+ }
1620
+ ${$("schedulerStub",bt,["cancel","runAfter","runAt"])}${$("storageStub",yt,["createMultipartUpload","delete","download","generateUploadUrl","getMetadata","getPresignedUrl","getSignedUrl","getUrl","head","list","resumeMultipartUpload","store","upload"],{sync:["getUrl","resumeMultipartUpload"]})}${wt}${Ct}${St}${k.stubs}${je}
1621
+ ${qt}
1622
+
1623
+ /**
1624
+ * Build the project's shard Durable Object. Export the result as \`ShardDO\`
1625
+ * from the worker entry so wrangler binds it by name.
1626
+ */
1627
+ export const createShardDO = (config: ShardDOConfig = {}): new (state: ShardDOState, env: unknown) => ShardDOBase =>
1628
+ class extends ShardDOBase {${Nt}
1629
+ private blockedUniqueIndexes: readonly string[] = [];
1630
+ private migrated = false;
1631
+ private migrationRetryAt = 0;${$t}${Mt}${Bt}
1632
+ ${de.override}
1633
+ ${Ut}
1634
+ ${at}${dt}${Dt}${Et}
1635
+ protected override lifecycleHookPaths(event: "connect" | "disconnect" | "init" | "reactor" | "whisper"): readonly string[] {
1636
+ return LUNORA_LIFECYCLE_HOOKS[event];
1637
+ }
1638
+ ${Tt}${_t}${ce}${z.override}${Lt}
1639
+ // One \`onQueryChange\` dispatch. Mirrors \`executeSubscription\` — the
1640
+ // socket-terminated half of the same reactivity — and differs only in who
1641
+ // consumes the result: the base's \`dispatchReactors\` stores the digest as
1642
+ // the new baseline and the footprint as the "should I even re-run this"
1643
+ // gate, instead of pushing a frame down a socket.
1644
+ //
1645
+ // Dispatched through \`runMutationTransaction\` because a reactor handler is
1646
+ // a mutation: its writes must commit all-or-nothing, the jobs it schedules
1647
+ // must wait for that commit, and the objects its row deletes orphaned must
1648
+ // be flushed once it lands. Safe to open a span here: the refresh drain
1649
+ // runs OUTSIDE any dispatch transaction (it is post-flush background work),
1650
+ // so this never nests.
1651
+ //
1652
+ // NO identity is threaded, and the ctx is built \`trusted\`. A reactor fires
1653
+ // because data moved, not because anyone asked, so there is no user for RLS
1654
+ // to scope to — it runs in the same system tier as migrations and the
1655
+ // external-source poll loop. Inheriting the shared per-request identity
1656
+ // would instead run an app's reactor as whichever user happened to write
1657
+ // last, which is worse than running it as nobody.
1658
+ protected override async runReactor(functionPath: string, previousDigest?: string): Promise<{ digest: string; ran: boolean; tables: readonly string[] } | undefined> {
1659
+ ${c?` const requestScope = this.captureRequestScope();
1660
+ const preparation = this.prepareFunction(functionPath, "reactor");
1661
+
1662
+ if (preparation !== undefined) {
1663
+ try {
1664
+ await preparation;
1665
+ } finally {
1666
+ this.restoreRequestScope(requestScope);
1667
+ }
1668
+ }
1669
+
1670
+ `:""} const registered = LUNORA_FUNCTIONS[functionPath];
1671
+
1672
+ if (!registered || registered.kind !== "mutation") {
1673
+ return undefined;
1674
+ }
1675
+
1676
+ this.ensureServable();
1677
+
1678
+ const footprint = createReadFootprint();
1679
+ const ctx = this.buildCtx({ functionPath, headroom: this.subscriptionHeadroom(), onRead: footprint.onRead, onReadRange: footprint.onReadRange, replayKey: false, trusted: true });
1680
+ const outcome = (await this.runMutationTransaction(ctx, async () =>
1681
+ registered.handler(ctx, { previousDigest } as unknown as Record<string, unknown>),
1682
+ )) as {
1683
+ digest: string;
1684
+ ran: boolean;
1685
+ };
1686
+
1687
+ return { digest: outcome.digest, ran: outcome.ran, tables: [...footprint.tables] };
1688
+ }
1689
+
1690
+
1691
+ protected override tableRefs(table: string): Record<string, string> | undefined {
1692
+ return LUNORA_TABLE_REFS()[table];
1693
+ }
1694
+
1695
+ protected override tableIndexes(table: string): Array<{ fields: string[]; name: string; type: "geo" | "index" | "rank" | "search" | "vector"; unique?: boolean }> {
1696
+ return LUNORA_TABLE_INDEXES()[table] ?? [];
1697
+ }
1698
+
1699
+ protected override ttlSweeps(): ReadonlyArray<{ after?: number; field: string; softDeleteField?: string; table: string }> {
1700
+ return LUNORA_TTL_SWEEPS;
1701
+ }
1702
+
1703
+ protected override tableColumns(table: string): Array<{
1704
+ bucket?: string;
1705
+ enumValues?: string[];
1706
+ isStorage?: boolean;
1707
+ name: string;
1708
+ nullable?: boolean;
1709
+ onDelete?: "cascade" | "restrict" | "set null";
1710
+ optional: boolean;
1711
+ pk?: boolean;
1712
+ ref?: string;
1713
+ type: string;
1714
+ }> {
1715
+ return LUNORA_TABLE_COLUMNS()[table] ?? [];
1716
+ }
1717
+
1718
+ protected override storageColumns(): Record<string, string[]> {
1719
+ return LUNORA_STORAGE_COLUMNS();
1720
+ }
1721
+
1722
+ protected override rlsMetadata(): RlsPoliciesResult {
1723
+ return LUNORA_RLS_METADATA();
1724
+ }
1725
+
1726
+ protected override maskMetadata(): MaskPoliciesResult {
1727
+ return LUNORA_MASK_METADATA();
1728
+ }
1729
+
1730
+ protected override storageRulesMetadata(): StorageRulesResult {
1731
+ return LUNORA_STORAGE_RULES();
1732
+ }
1733
+
1734
+ protected override studioFeatures(): StudioFeaturesResult {
1735
+ return LUNORA_STUDIO_FEATURES();
1736
+ }
1737
+ ${V.evaluateOverride}${V.subscriptionOverride}${ze}${Ze}
1738
+ protected override advisories(): AdvisoryFinding[] {
1739
+ return LUNORA_ADVISORIES();
1740
+ }
1741
+
1742
+ protected override advisorProcedures(): AdvisorProcedure[] {
1743
+ return LUNORA_ADVISOR_PROCEDURES();
1744
+ }
1745
+
1746
+ ${Tr}
1747
+
1748
+ /**
1749
+ * Migrate the base schema. A UNIQUE index that existing duplicate rows block
1750
+ * is deferred, not thrown: the rest of the pass completes so the admin path
1751
+ * (including the data migration that de-duplicates the table) keeps working.
1752
+ * User dispatch goes through \`ensureServable\`, which refuses until the
1753
+ * index exists. \`migrated\` stays false while one is outstanding, but the
1754
+ * retry (a schema-ledger write plus a full-table duplicate scan per blocked
1755
+ * index) runs at most once per \`BLOCKED_MIGRATION_RETRY_MS\`; in between,
1756
+ * callers see the cached diagnostics. The first call after a cold start
1757
+ * always migrates, and an admin data migration resets the clock so the
1758
+ * de-dup that unblocks the index takes effect on the next call.
1759
+ */
1760
+ protected override ensureMigrated(): void {
1761
+ if (this.migrated) {
1762
+ return;
1763
+ }
1764
+
1765
+ const now = Date.now();
1766
+
1767
+ if (this.blockedUniqueIndexes.length > 0 && now < this.migrationRetryAt) {
1768
+ return;
1769
+ }
1770
+
1771
+ this.blockedUniqueIndexes = runShardMigrations(this.sql as SqlExec, schema as unknown as SchemaLike, { cdc: config.cdc ?? false, deferBlockedUniqueIndexes: true${be} });
1772
+ this.migrated = this.blockedUniqueIndexes.length === 0;
1773
+ this.migrationRetryAt = now + BLOCKED_MIGRATION_RETRY_MS;
1774
+ }
1775
+
1776
+ private ensureServable(): void {
1777
+ this.ensureMigrated();
1778
+
1779
+ if (this.blockedUniqueIndexes.length > 0) {
1780
+ throw new LunoraError("INTERNAL", this.blockedUniqueIndexes.join("; "));
1781
+ }
1782
+ }
1783
+ ${Z?`
1784
+ protected override runShardSearchBackfill(options: { maxPages?: number }): SearchBackfillProgress {
1785
+ // Migrations create the fts5 companions; without them every backfill
1786
+ // statement would raise "no such table" on a shard that has never
1787
+ // served a request.
1788
+ this.ensureMigrated();
1789
+
1790
+ return backfillSearchIndexes(this.sql as SqlExec, schema as unknown as SchemaLike, options);
1791
+ }
1792
+ `:""}${vt}
1793
+ ${_r({hasSensitiveArgs:H,lazyFunctions:c,actionOnlyFields:Gt,agentsBuild:Pe,agentsContextField:Ne,containersBuild:Oe,containersContextField:De,databaseOptions:Rt,everyContextBuild:Wt,everyContextField:Kt,facadeBlock:jt,globalDatabaseLine:Ot,notifyBuild:Ht,ormContextField:At,paymentsBuild:qe,paymentsContextField:Ue,queuesBuild:ye,queuesContextField:we,topicsBuild:Se,topicsContextField:Re,vectorsBuild:kt,vectorsContextField:xt,workflowsBuild:Ee,workflowsContextField:Ie,actionOnlyBuild:Qt})}
1794
+ };
1795
+ `};export{$r as b,on as e};