@lunora/hyperdrive 1.0.0-alpha.93 → 1.0.0-alpha.95

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 (2) hide show
  1. package/README.md +38 -1
  2. package/package.json +3 -3
package/README.md CHANGED
@@ -113,6 +113,43 @@ export const syncCustomer = action.input({ orgId: v.string() }).action(async ({
113
113
  | `pg` (node-postgres) | `fromNodePg` | `$1, $2, …` |
114
114
  | `mysql2/promise` | `fromMysql2` | `?` |
115
115
 
116
+ ## Reactive reads over your schema: the `.source()` modifier
117
+
118
+ The projection above is the escape hatch. For the common case — an existing Postgres you are not going to move, and a read path that should feel live — declare the source on the table and Lunora runs the whole loop on the shard's alarm: pull, diff, materialize, poke subscribers. No action, no mutation, no cron to write:
119
+
120
+ ```ts
121
+ // lunora/schema.ts
122
+ export default defineSchema({
123
+ messages: defineTable({ body: v.string(), channelId: v.string() })
124
+ .shardBy("channelId")
125
+ .source({
126
+ binding: "HYPERDRIVE_MESSAGES",
127
+ query: 'select id, body, channel_id as "channelId" from messages where channel_id = $1',
128
+ tenantBy: (shardKey) => [shardKey], // mandatory under .shardBy() — the tenant boundary
129
+ }),
130
+ });
131
+
132
+ export const channelMessages = defineShape({ table: "messages", where: () => ({}) });
133
+ ```
134
+
135
+ Supply the driver once, when the shard DO is constructed. Lunora memoizes it per binding:
136
+
137
+ ```ts
138
+ createShardDO({
139
+ sourceClient: (env, binding) => fromPostgresJs(postgres((env[binding] as { connectionString: string }).connectionString)),
140
+ });
141
+ ```
142
+
143
+ Clients subscribe with the shape they would use over any table — external data stops being external once it is materialized.
144
+
145
+ Three things to know before you build on it:
146
+
147
+ - **It polls; it is not logical replication.** Freshness is the refresh cadence: every alarm tick by default, `refresh: { everyMs }` to throttle, `refresh: "manual"` to drive it yourself. Writes from other systems land on the next pull.
148
+ - **`tenantBy` is the isolation boundary.** `defineSchema` throws at load if a sourced `.shardBy()` table omits it, and the `external_source_unscoped` advisor lint catches it at build time. Without it, one tenant's DO pulls every tenant's rows.
149
+ - **`mode: "full-pull"` (the default) diffs the whole slice each tick**, so upstream deletes are detected for free, at a bench ceiling around 10k rows. Past that, `mode: "incremental"` pulls only rows past a durable watermark via a cursor column. An incremental slice cannot see a delete on its own — an absent row means "unchanged", not "deleted" — so it **requires** a delete-visibility path: either `reconcileEveryMs` (a periodic full-pull sweep) or `softDeleteColumn` (an upstream tombstone column the cursor query returns, so do not filter it out). `defineSchema` throws and the `external_source_incremental_no_delete_path` lint fails the build if an incremental source declares neither.
150
+
151
+ `.source()` cannot be combined with `.global()` — they are contradictory tiers, and `defineSchema` rejects it.
152
+
116
153
  ## Reactive `.global()` over Hyperdrive
117
154
 
118
155
  The `@lunora/hyperdrive/global` subpath is the opposite trade-off: Lunora **owns** the schema and a `.global()` table gets a real column-per-field layout on your Postgres/MySQL, with every write routed through the shared store core. Live queries stay reactive — identical to D1 — because the writer drives the same broadcast hook. Build the writer inside the Durable Object that hosts the `.global()` store and inject it as `globalDb`:
@@ -127,7 +164,7 @@ const globalDb = createPostgresGlobalCtxDb({ query: (text, params) => sql.unsafe
127
164
 
128
165
  For MySQL use `createMysqlGlobalCtxDb` with a `mysql2/promise` pool created with `flags: ["FOUND_ROWS"]` — without `CLIENT_FOUND_ROWS` the affected-rows OCC guard sees changed (not matched) rows and raises spurious conflicts. Lower-level building blocks (`buildPgExec`, `buildMysqlExec`, `postgresDialect`, `mysqlDialect`, `createHyperdriveGlobalCtxDb`) are exported for custom wiring.
129
166
 
130
- > This README covers the basics. For the full API and the determinism/realtime rationale, see the **[documentation](https://lunora.sh/docs/addons/hyperdrive)**.
167
+ > This README covers the basics. For the full API and the determinism/realtime rationale, see the **[documentation](https://lunora.sh/docs/packages/hyperdrive)**.
131
168
 
132
169
  ## Non-goals
133
170
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/hyperdrive",
3
- "version": "1.0.0-alpha.93",
3
+ "version": "1.0.0-alpha.95",
4
4
  "description": "Bring-your-own Postgres/MySQL for Lunora via Cloudflare Hyperdrive: a driver-agnostic, action-only ctx.sql",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -51,8 +51,8 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@lunora/errors": "1.0.0-alpha.24",
54
- "@lunora/shard-engine": "1.0.0-alpha.42",
55
- "@lunora/sql-store": "1.0.0-alpha.94",
54
+ "@lunora/shard-engine": "1.0.0-alpha.44",
55
+ "@lunora/sql-store": "1.0.0-alpha.96",
56
56
  "drizzle-orm": "^0.45.2"
57
57
  },
58
58
  "peerDependencies": {