@lunora/cli 1.0.0-alpha.40 → 1.0.0-alpha.401

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 (134) hide show
  1. package/LICENSE.md +33 -0
  2. package/README.md +3 -1
  3. package/dist/bin.mjs +2 -10
  4. package/dist/index.d.mts +1034 -374
  5. package/dist/index.d.ts +1034 -374
  6. package/dist/index.mjs +1 -19
  7. package/dist/packem_chunks/dispatch.mjs +1 -0
  8. package/dist/packem_chunks/handler.mjs +1 -160
  9. package/dist/packem_chunks/handler10.mjs +1 -16
  10. package/dist/packem_chunks/handler11.mjs +1 -22
  11. package/dist/packem_chunks/handler12.mjs +1 -192
  12. package/dist/packem_chunks/handler13.mjs +3 -131
  13. package/dist/packem_chunks/handler14.mjs +1 -65
  14. package/dist/packem_chunks/handler15.mjs +1 -58
  15. package/dist/packem_chunks/handler16.mjs +1 -80
  16. package/dist/packem_chunks/handler17.mjs +7 -43
  17. package/dist/packem_chunks/handler18.mjs +1 -105
  18. package/dist/packem_chunks/handler19.mjs +1 -170
  19. package/dist/packem_chunks/handler2.mjs +1 -114
  20. package/dist/packem_chunks/handler20.mjs +3 -94
  21. package/dist/packem_chunks/handler21.mjs +3 -94
  22. package/dist/packem_chunks/handler22.mjs +1 -0
  23. package/dist/packem_chunks/handler23.mjs +2 -0
  24. package/dist/packem_chunks/handler24.mjs +99 -0
  25. package/dist/packem_chunks/handler25.mjs +2 -0
  26. package/dist/packem_chunks/handler26.mjs +9 -0
  27. package/dist/packem_chunks/handler27.mjs +1 -0
  28. package/dist/packem_chunks/handler3.mjs +1 -204
  29. package/dist/packem_chunks/handler4.mjs +3 -33
  30. package/dist/packem_chunks/handler5.mjs +1 -49
  31. package/dist/packem_chunks/handler6.mjs +1 -91
  32. package/dist/packem_chunks/handler7.mjs +1 -42
  33. package/dist/packem_chunks/handler8.mjs +2 -174
  34. package/dist/packem_chunks/handler9.mjs +1 -315
  35. package/dist/packem_chunks/runCodegenCommand.mjs +4 -52
  36. package/dist/packem_chunks/runDeployCommand.mjs +11 -594
  37. package/dist/packem_chunks/runDevCommand.mjs +2 -0
  38. package/dist/packem_chunks/runInitCommand.mjs +165 -1432
  39. package/dist/packem_chunks/runResetCommand.mjs +1 -41
  40. package/dist/packem_chunks/runRpcCommand.mjs +1 -68
  41. package/dist/packem_shared/COMMANDS-aSTSMwZS.mjs +1 -0
  42. package/dist/packem_shared/DEFAULT_IMPORT_BATCH_SIZE-CyIsI3S1.mjs +1 -0
  43. package/dist/packem_shared/EXIT_CODE-D_XCixv8.mjs +1 -0
  44. package/dist/packem_shared/admin-token-VdUnvnKW.mjs +1 -0
  45. package/dist/packem_shared/admin-url-DJB8P6Jh.mjs +1 -0
  46. package/dist/packem_shared/api-spec-lYyq4vL6.mjs +1 -0
  47. package/dist/packem_shared/binding-manifest-file-CQ1eQYy1.mjs +2 -0
  48. package/dist/packem_shared/buildRegistryIndex-Dc8D8AR6.mjs +1 -0
  49. package/dist/packem_shared/cap-error-body-YBKO32BF.mjs +1 -0
  50. package/dist/packem_shared/catalog-CahzmDLV.mjs +1 -0
  51. package/dist/packem_shared/cli-CFsmCYWp.mjs +2 -0
  52. package/dist/packem_shared/cli-manifest-DoQ_3bzs.mjs +1 -0
  53. package/dist/packem_shared/cloudflare-credentials-BVmZnDZk.mjs +1 -0
  54. package/dist/packem_shared/codegen-error-AmH54ofi.mjs +3 -0
  55. package/dist/packem_shared/command-BgjsvUFg.mjs +1 -0
  56. package/dist/packem_shared/commands-DvSfLvyt.mjs +13 -0
  57. package/dist/packem_shared/createLogger-4TujevyB.mjs +2 -0
  58. package/dist/packem_shared/createRecordingSpawner-Ch7zuQQE.mjs +1 -0
  59. package/dist/packem_shared/deploy-target-resolve-BTCW1Iv4.mjs +1 -0
  60. package/dist/packem_shared/diffSnapshots-htqNZT8Q.mjs +7 -0
  61. package/dist/packem_shared/docker-AqLAkOyY.mjs +1 -0
  62. package/dist/packem_shared/health-probe-Bw9JFfIy.mjs +1 -0
  63. package/dist/packem_shared/import-BRM1hs7j.mjs +12 -0
  64. package/dist/packem_shared/index-oHZBGBwB.mjs +1 -0
  65. package/dist/packem_shared/insertSchemaExtension-DuiV6cba.mjs +8 -0
  66. package/dist/packem_shared/lint-ignore-report-DKZagpqk.mjs +2 -0
  67. package/dist/packem_shared/mcp-config-file-CcDTeXIq.mjs +2 -0
  68. package/dist/packem_shared/open-url-EnKy--w-.mjs +1 -0
  69. package/dist/packem_shared/output-format-DWe_tkTR.mjs +2 -0
  70. package/dist/packem_shared/parseManifest-CwPTKdtS.mjs +1 -0
  71. package/dist/packem_shared/path-containment-CgxYZggb.mjs +1 -0
  72. package/dist/packem_shared/plan-DVfi3TrN.mjs +5 -0
  73. package/dist/packem_shared/planDevCommand-IglbXy5A.mjs +1 -0
  74. package/dist/packem_shared/platform-diagnostics-Cn2g6Jh-.mjs +4 -0
  75. package/dist/packem_shared/prompt-cancelled-BvsNxg_Q.mjs +1 -0
  76. package/dist/packem_shared/render-lunora-error-C3OJY5wk.mjs +2 -0
  77. package/dist/packem_shared/resolve-Bdc9KEUv.mjs +5 -0
  78. package/dist/packem_shared/resolve-target-C_ZloTvd.mjs +1 -0
  79. package/dist/packem_shared/runAddCommand-BChdJQ5L.mjs +1 -0
  80. package/dist/packem_shared/runExportCommand-slAq5BCF.mjs +5 -0
  81. package/dist/packem_shared/runMigrateGenerateCommand-BwemPIOz.mjs +11 -0
  82. package/dist/packem_shared/schemaIrToSnapshot-BHuNOfgs.mjs +1 -0
  83. package/dist/packem_shared/secret-key-8DPOH98z.mjs +1 -0
  84. package/dist/packem_shared/shared-D-zCOmgY.mjs +1 -0
  85. package/dist/packem_shared/storage-DNzYhXR-.mjs +1 -0
  86. package/dist/packem_shared/tui-prompts-B3YwUhGw.mjs +4 -0
  87. package/dist/packem_shared/variables-CPYyJtsL.mjs +1 -0
  88. package/dist/packem_shared/wrangler-name-Dsk5K1f-.mjs +1 -0
  89. package/dist/packem_shared/wrangler-secrets-CTQPpalP.mjs +1 -0
  90. package/package.json +39 -19
  91. package/skills/README.md +35 -17
  92. package/skills/lunora/SKILL.md +157 -65
  93. package/skills/lunora-create-package/SKILL.md +97 -84
  94. package/skills/lunora-deploy/SKILL.md +175 -105
  95. package/skills/lunora-functions/SKILL.md +363 -56
  96. package/skills/lunora-migration-helper/SKILL.md +162 -137
  97. package/skills/lunora-performance-audit/SKILL.md +137 -95
  98. package/skills/lunora-quickstart/SKILL.md +137 -136
  99. package/skills/lunora-realtime/SKILL.md +201 -97
  100. package/skills/lunora-setup-auth/SKILL.md +162 -102
  101. package/skills/lunora-setup-hyperdrive/SKILL.md +130 -109
  102. package/skills/lunora-setup-hyperdrive-global/SKILL.md +93 -90
  103. package/skills/lunora-setup-mail/SKILL.md +85 -113
  104. package/skills/lunora-setup-scheduler/SKILL.md +132 -96
  105. package/skills/lunora-setup-storage/SKILL.md +189 -95
  106. package/dist/packem_chunks/planDevCommand.mjs +0 -541
  107. package/dist/packem_chunks/runMigrateGenerateCommand.mjs +0 -397
  108. package/dist/packem_shared/COMMANDS-B0ftFD_3.mjs +0 -948
  109. package/dist/packem_shared/DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs +0 -244
  110. package/dist/packem_shared/admin-url-4UzT-CI4.mjs +0 -19
  111. package/dist/packem_shared/api-spec-CtA6ilu4.mjs +0 -13
  112. package/dist/packem_shared/buildRegistryIndex-BcYe607_.mjs +0 -38
  113. package/dist/packem_shared/codegen-error-DJG-ghs_.mjs +0 -31
  114. package/dist/packem_shared/command-D3lB_4Az.mjs +0 -19
  115. package/dist/packem_shared/commands-B-gR09Z_.mjs +0 -845
  116. package/dist/packem_shared/createLogger-B40gPzQo.mjs +0 -78
  117. package/dist/packem_shared/createRecordingSpawner-DxI3mebw.mjs +0 -43
  118. package/dist/packem_shared/detect-package-manager-DYp7n3mJ.mjs +0 -61
  119. package/dist/packem_shared/diffSnapshots-BeDvvNiF.mjs +0 -161
  120. package/dist/packem_shared/docker-hMQ97KSQ.mjs +0 -21
  121. package/dist/packem_shared/insertSchemaExtension-DAqbfr9Z.mjs +0 -64
  122. package/dist/packem_shared/open-url-Dfq6fAyT.mjs +0 -41
  123. package/dist/packem_shared/output-format-wUvAN6AL.mjs +0 -17
  124. package/dist/packem_shared/parseArgs-YXFuKdEk.mjs +0 -56
  125. package/dist/packem_shared/parseManifest--vZf2FY1.mjs +0 -94
  126. package/dist/packem_shared/prompt-cancelled-APzX1Im-.mjs +0 -9
  127. package/dist/packem_shared/resolve-target-qbsJ_5sF.mjs +0 -16
  128. package/dist/packem_shared/runAddCommand-bnY6-HKb.mjs +0 -4
  129. package/dist/packem_shared/schema-drift-gate-BtBt0as0.mjs +0 -79
  130. package/dist/packem_shared/schemaIrToSnapshot-DdsljJT-.mjs +0 -43
  131. package/dist/packem_shared/storage-BIsph-Vk.mjs +0 -84
  132. package/dist/packem_shared/tui-prompts-BjEN8XgP.mjs +0 -658
  133. package/dist/packem_shared/wrangler-name-cy4yhm9j.mjs +0 -12
  134. package/dist/packem_shared/wrangler-secrets-P2_ZUR-k.mjs +0 -47
@@ -1,143 +1,185 @@
1
1
  ---
2
2
  name: lunora-performance-audit
3
- description: Diagnoses and fixes Lunora performance problems — full-table scans, missing
4
- indexes, OCC write conflicts, oversized subscriptions, and sharding/`.global()`
5
- scaling. Use when queries are slow, mutations conflict, or `@lunora/advisor`
6
- flags a table.
3
+ description: Diagnoses and fixes Lunora performance problems — full-table scans and missing indexes, expensive `ctx.db.related` traversals, OCC write conflicts (409 `CONFLICT`), live queries that re-run too often, hot shards, and choosing `.shardBy(key)` or `.global()` to scale. Starts from measured signal (`lunora insights`) and static signal (`lunora advisor`, `@lunora/advisor` lints such as `filter_without_index`, `unbounded_collect`, `unindexed_foreign_key`, `hot_shard`). Use when the user says a query or page is slow, mutations fail with conflicts under load, subscriptions are chatty, asks to "audit performance", "add the right index", or "should I shard this table", or when the Studio Advisors tab or `lunora advisor` flags something.
7
4
  ---
8
5
 
9
6
  # Lunora Performance Audit
10
7
 
11
- Systematically diagnose Lunora performance issues, route to the right fix, and
12
- apply it across sibling functions consistently.
8
+ Find the measured problem, route it to the matching fix below, and apply the
9
+ fix to every function that touches the same table.
13
10
 
14
- ## When to Use
11
+ If traffic is modest and nothing is measured as slow, prefer simpler code. Don't
12
+ introduce sharding, digest tables or document splits on speculation.
15
13
 
16
- - Queries feel slow or read far more rows than they return.
17
- - Mutations retry or conflict under load (OCC).
18
- - Subscriptions fan out updates too broadly or re-run too often.
19
- - The Lunora Studio **Advisors** tab or `@lunora/advisor` flags a table.
14
+ ## Workflow
20
15
 
21
- ## When Not to Use
16
+ 1. **Scope** one concrete user flow with a clear entry and exit.
17
+ 2. **Gather signal** with `lunora insights` (runtime) and `lunora advisor`
18
+ (static).
19
+ 3. **Trace** every `ctx.db` read and write in that flow.
20
+ 4. **Route** to the problem class below and fix it.
21
+ 5. **Fix siblings** so every function reads the table the same indexed way. A
22
+ half-migrated access pattern is its own bug.
23
+ 6. **Verify:** behaviour unchanged, the advisor finding cleared, and the
24
+ `insights` row improved after the fix has served traffic.
25
+
26
+ ## Signal
27
+
28
+ ### Runtime: `lunora insights`
29
+
30
+ Needs a running worker that has served traffic. It reports measured problems:
31
+
32
+ ```bash
33
+ lunora insights # local dev worker (http://localhost:8787)
34
+ lunora insights --shard channel:demo # one shard instead of the root
35
+ lunora insights --limit 25 --format json
36
+ LUNORA_ADMIN_TOKEN=… lunora insights --prod --url https://app.example.com
37
+ ```
22
38
 
23
- - Scale is small, traffic is modest, and there is no measured problem — prefer
24
- simpler code. Do not introduce sharding, digests, or splits on speculation.
39
+ It ranks per-function **write-conflict hot-spots** (go to Write conflicts),
40
+ **error rates**, and **latency outliers** (usually Read amplification). The
41
+ Studio Issues panel and `lunora logs` give error detail once you know where to
42
+ look.
25
43
 
26
- ## Core Workflow
44
+ ### Static: `lunora advisor` and `@lunora/advisor`
27
45
 
28
- 1. **Scope** one concrete user flow with a clear entry and exit.
29
- 2. **Trace** every `ctx.db` read and write in that flow.
30
- 3. **Route** to the matching problem class below.
31
- 4. **Fix siblings** consistently — every function touching the same table should
32
- read it the same indexed way.
33
- 5. **Verify** behavior is unchanged and the advisor finding clears.
46
+ No traffic needed. The Studio **Advisors** tab shows the same findings live in
47
+ dev.
34
48
 
35
- ## Signal Gathering
49
+ ```bash
50
+ lunora advisor # score + write lunora.advisor.map.json
51
+ lunora advisor --entry messages#send # one procedure (file#exportName)
52
+ lunora advisor --baseline # CI: fail on regression vs the committed map
53
+ ```
36
54
 
37
- Start with the static advisors — they need no traffic:
55
+ Lint ids are snake_case. The performance-relevant ones:
38
56
 
39
- - **Lunora Studio → Advisors tab** surfaces `@lunora/advisor` findings live in
40
- dev.
41
- - `@lunora/advisor` runs static lints over `defineSchema` + discovered query
42
- reads / insert writes. Relevant performance/schema rules:
43
- - `filter-without-index` — a query filters a table with no covering index.
44
- - `unindexed-foreign-key` — a relation/FK column has no index.
45
- - `duplicate-index` / `empty-index` — wasted or malformed indexes.
46
- - `index-references-unknown-field`, `relation-references-unknown-field`,
47
- `relation-references-unknown-table` — broken index/relation definitions.
48
- - `table-without-insert` — a table is read but never written (often a
49
- schema/typo signal).
57
+ | Lint | Meaning |
58
+ | --------------------------------- | ---------------------------------------------------------------------------- |
59
+ | `filter_without_index` | A query filters a table with no covering index. |
60
+ | `unbounded_collect` | `.collect()` with no index or filter loads every row (and re-sends it live). |
61
+ | `filter_on_primary_key` | Filtering on `_id` scans; use `ctx.db.get(id)`. |
62
+ | `unindexed_foreign_key` | An FK column has no index. |
63
+ | `unindexed_relation_target` | A `many` relation's target FK has no index, so `with:` reads scan. |
64
+ | `duplicate_index` / `empty_index` | Wasted or malformed index. |
65
+ | `index_utilization` (runtime) | A declared index nothing uses, or a hot table read with none. |
66
+ | `hot_shard` (runtime) | One shard takes a disproportionate share of a sharded function's load. |
67
+ | `fan_out_breadth` (runtime) | A shard set so wide a cross-shard read nears the subrequest limit. |
50
68
 
51
- ## Problem Class: Read Amplification (the common one)
69
+ ## Read amplification (the common one)
52
70
 
53
- **Symptom:** a query reads the whole table to return a few rows;
54
- `filter-without-index` fires.
71
+ **Symptom:** a query reads the whole table to return a few rows; a latency
72
+ outlier in `insights`, or `filter_without_index` / `unbounded_collect`.
55
73
 
56
- **Fix:** read through an index, not `.filter()`. Declare the index on the table
57
- and use `.withIndex()` with an equality/range on the leading columns.
74
+ **Fix:** declare an index and read through it with `.withIndex()`:
58
75
 
59
76
  ```ts
60
- // Bad — scans every row, then filters in memory.
77
+ // (auth / org membership check on `orgId` elided — see lunora-functions)
78
+ // Scans every row, then filters in memory.
61
79
  const mine = (await ctx.db.query("documents").collect()).filter((d) => d.orgId === orgId);
62
80
 
63
- // Good — declare the index…
81
+ // schema.ts
64
82
  documents: defineTable({ orgId: v.string(), createdAt: v.number() /* … */ }).index("by_org_created", ["orgId", "createdAt"]);
65
83
 
66
- // …and read through it.
84
+ // Reads only the org's range.
67
85
  const mine = await ctx.db
68
86
  .query("documents")
69
87
  .withIndex("by_org_created", (q) => q.eq("orgId", orgId))
70
88
  .collect();
71
89
  ```
72
90
 
73
- Index columns are ordered: put equality columns first, then the range/sort
74
- column. Fix every sibling query on the table the same way.
91
+ Put equality columns first, then the range/sort column. For a large range,
92
+ paginate instead of `.collect()`.
75
93
 
76
- ## Problem Class: Write Conflicts (OCC)
94
+ ## Relation traversal cost
77
95
 
78
- **Symptom:** mutations on hot rows retry or fail under concurrency. ShardDO uses
79
- optimistic concurrency control — concurrent writes to the same DO that touch
80
- overlapping state conflict and retry.
96
+ **Symptom:** a query using `ctx.db.related` is slow, or one page reads far more
97
+ rows than it returns.
81
98
 
82
- **Fixes, in order of preference:**
99
+ A traversal is one batched `findMany` per edge, per hop, and the frontier grows
100
+ multiplicatively. The options are bounded, and an out-of-range value is rejected
101
+ with `BAD_REQUEST` instead of being clamped, so a throwing traversal means the
102
+ request is too big, not that there's a bug to route around.
83
103
 
84
- 1. **Narrow the write.** Patch only the fields that changed; avoid read-modify-
85
- write over rows another mutation also touches.
86
- 2. **Partition with `.shardBy(key)`.** Move per-user / per-tenant / per-room
87
- state into its own DO so writes for different keys never contend. This is the
88
- primary horizontal-scale lever — most write contention is a single-DO
89
- hotspot.
90
- 3. **Avoid unbounded counters/aggregates in the hot path.** Accumulate in a
91
- sharded/append shape and fold lazily rather than serializing every writer
92
- through one row.
104
+ | Option | Default | Cap | Why |
105
+ | ------------- | ------- | -------- | ------------------------------------------------------------- |
106
+ | `depth` | `1` | `4` | Past 4 hops, request time is the limit, not `limit`. |
107
+ | `limit` | `50` | `200` | Nodes per page. |
108
+ | cursor offset | `0` | `10 000` | The offset is re-walked: each hop reads `N + limit + 1` rows. |
93
109
 
94
- ## Problem Class: Subscription Cost
110
+ Fixes, in order:
95
111
 
96
- **Symptom:** a `useQuery` re-runs and re-pushes to many clients on unrelated
97
- writes.
112
+ 1. **Narrow `edges`** to the `"<table>.<column>"` edges the feature needs. One
113
+ unrelated `v.id(...)` column can double the frontier per hop.
114
+ 2. **Set `direction`** to `"in"` or `"out"` instead of the default `"both"`.
115
+ 3. **Lower `depth` before raising `limit`.** Hops multiply; a page is linear.
116
+ 4. **Index the foreign keys.** Every inward hop is a `WHERE fk IN (…)` read
117
+ (`unindexed_foreign_key`).
118
+ 5. **Stop deep-paging.** Needing an offset past 10 000 means the traversal is
119
+ the wrong tool; narrow the walk.
120
+
121
+ ## Write conflicts (OCC)
98
122
 
99
- **Fixes:**
123
+ **Symptom:** mutations on hot rows fail under concurrency, or a function tops
124
+ the write-conflict list in `insights`.
100
125
 
101
- - **Scope query args tightly** so a subscription only depends on the rows it
102
- renders — a query keyed by `orgId` should not re-run for another org's write.
103
- - **Read through indexes** (above) so the reactive dependency is the narrow
104
- index range, not the whole table.
105
- - ShardDO subscriptions are **hibernated WebSockets** — idle connections cost
106
- nothing; the lever is _how many rows each live query depends on_, not raw
107
- connection count.
126
+ ShardDO uses optimistic concurrency. When a concurrent write commits first, the
127
+ mutation throws `ConflictError` (`code: "CONFLICT"`, HTTP 409). The runtime does
128
+ not retry it; a mutation body runs at most once per call, and the caller
129
+ decides what to do. On the client, check with `isConflictError(error)` from
130
+ `@lunora/client` (also exported via `lunorash/client`).
108
131
 
109
- ## Problem Class: Cross-Region Reads
132
+ Not every 409 is contention. On the server, `ConflictError.kind` is `"occ"` for
133
+ real write contention, and `"unique"`, `"restrict"` or `"trigger"` for
134
+ constraint and guard failures. The client only sees `code: "CONFLICT"` (`kind`
135
+ is not part of the error envelope), so it cannot tell them apart. `insights` counts only `occ` as a write conflict, so fix a `unique`
136
+ 409 in the data or the insert logic, not by sharding.
110
137
 
111
- **Symptom:** read-heavy, rarely-written data is slow for far-away users.
138
+ Fixes, in order:
139
+
140
+ 1. **Narrow the write.** Patch only changed fields; avoid read-modify-write over
141
+ rows another mutation also writes.
142
+ 2. **Partition with `.shardBy(key)`.** Per-user / per-tenant / per-room state
143
+ in its own Durable Object never contends across keys. This is the main
144
+ horizontal-scale lever, since most contention is one hot DO.
145
+ 3. **Take counters and aggregates off the hot row.** Append and fold lazily
146
+ instead of serializing every writer through one row.
147
+
148
+ If `hot_shard` fires on an already-sharded table, the key is skewed: re-shard on
149
+ a higher-cardinality key or split the hot entity's state.
150
+
151
+ ## Subscription cost
152
+
153
+ **Symptom:** a `useQuery` re-runs and re-pushes to many clients on unrelated
154
+ writes.
112
155
 
113
- **Fix:** chain `.global()` on the table to replicate it to D1 for low-latency
114
- cross-region reads (with read-your-writes via the Sessions API). Reserve it for
115
- read-mostly tables — `.global()` adds the D1 migration flow (see the
116
- `lunora-migration-helper` skill) and write-path cost.
156
+ - Scope query args tightly so a live query depends only on the rows it renders;
157
+ a query keyed by `orgId` shouldn't re-run on another org's write.
158
+ - Read through indexes so the dependency is the index range, not the table.
159
+ - Idle WebSockets are hibernated and cost nothing. The lever is how many rows
160
+ each live query depends on, not the connection count.
117
161
 
118
- ### `.shardBy(key)` vs `.global()` — choose one per table
162
+ ## Cross-region reads
119
163
 
120
- - `.shardBy(key)`: partitions a table across Durable Objects by key — scales
121
- _writes_ (e.g. messages per room). Reads are per-shard.
122
- - `.global()`: replicates a table to D1 — scales _cross-region reads_ with
123
- read-your-writes (e.g. a mostly-read catalog).
124
- - They are not combined on the same table; the default (neither) is a single
125
- root-scoped ShardDO.
164
+ **Symptom:** read-heavy, rarely-written data is slow for distant users.
126
165
 
127
- ## Guardrails
166
+ **Fix:** `.global()` replicates the table to D1 for low-latency reads from the
167
+ edge, with read-your-writes via D1 sessions. Use it only for read-mostly tables:
168
+ it adds write-path cost and its own DDL flow (`lunora-migration-helper`). If the
169
+ dataset outgrows D1, `.global({ backend: "hyperdrive" })` serves the same
170
+ reactive contract from Postgres/MySQL (`lunora-setup-hyperdrive-global`).
128
171
 
129
- - Prefer simpler code when scale is small or the signal is weak.
130
- - Do not recommend structural changes (digest tables, document splitting,
131
- sharding) without a measured signal or a known hot path.
132
- - When you change how one function reads/writes a table, change its siblings to
133
- match — half-migrated access patterns are their own bug.
172
+ `.shardBy(key)` scales writes (rows in one DO per key); `.global()` scales
173
+ cross-region reads. A table can't have both; the default is the single root
174
+ ShardDO. Switching an existing table's mode moves its rows, which is an
175
+ export/import, not a schema edit (see `lunora-migration-helper`).
134
176
 
135
177
  ## Checklist
136
178
 
137
- - [ ] Scoped one concrete flow; traced every `ctx.db` read/write.
138
- - [ ] Checked the Studio Advisors tab / `@lunora/advisor` findings.
139
- - [ ] Read amplification: replaced `.filter()` with an indexed `.withIndex()`.
140
- - [ ] Write conflicts: narrowed writes and/or partitioned with `.shardBy(key)`.
141
- - [ ] Subscription cost: scoped query args so live queries depend on few rows.
142
- - [ ] Cross-region: applied `.global()` only to read-mostly tables.
143
- - [ ] Fixed sibling functions consistently; verified behavior unchanged.
179
+ - [ ] Scoped one flow; traced every `ctx.db` read/write.
180
+ - [ ] Checked `lunora insights` (if there's traffic) and `lunora advisor`.
181
+ - [ ] Scans replaced with `.withIndex()` reads; FKs indexed.
182
+ - [ ] Traversals narrowed with `edges` / `direction` before touching `depth`.
183
+ - [ ] Write conflicts confirmed as `occ`, then writes narrowed and/or `.shardBy(key)` applied.
184
+ - [ ] Live queries depend on few rows; `.global()` only on read-mostly tables.
185
+ - [ ] Siblings fixed consistently; behaviour unchanged; finding cleared.