@weft-js/start 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1278 -12
- package/crate/Cargo.lock +1347 -0
- package/crate/Cargo.toml +72 -0
- package/crate/README.md +322 -0
- package/crate/rust-toolchain.toml +3 -0
- package/crate/src/build/emit.rs +103 -0
- package/crate/src/build.rs +367 -0
- package/crate/src/ctx.rs +186 -0
- package/crate/src/envelope.rs +163 -0
- package/crate/src/functions.rs +173 -0
- package/crate/src/lib.rs +118 -0
- package/crate/src/native.rs +212 -0
- package/crate/src/otel.rs +62 -0
- package/crate/src/server.rs +203 -0
- package/package.json +46 -37
- package/src/app.mjs +38 -8
- package/src/asset-receipt.d.mts +13 -0
- package/src/asset-receipt.mjs +61 -0
- package/src/auth/better-auth.ts +215 -17
- package/src/auth/drizzle.ts +1 -1
- package/src/auth.ts +21 -6
- package/src/boot.ts +19 -8
- package/src/bus/postgres.ts +32 -19
- package/src/bus.ts +7 -4
- package/src/{capture.mjs → capture.ts} +34 -1
- package/src/changes-sql.d.mts +8 -2
- package/src/changes-sql.mjs +26 -10
- package/src/changes.ts +165 -66
- package/src/channel/connections.ts +216 -0
- package/src/channel/socket.ts +396 -0
- package/src/channel-fake.ts +19 -4
- package/src/channel.ts +120 -492
- package/src/client.tsx +9 -2
- package/src/contract/describe.mjs +231 -0
- package/src/contract.mjs +144 -157
- package/src/data/cache.ts +354 -0
- package/src/data/definitions.ts +94 -0
- package/src/data/draft.ts +104 -0
- package/src/data/foundation.ts +95 -0
- package/src/data/live-api.ts +22 -0
- package/src/data/mutation.ts +138 -0
- package/src/data/primitives.ts +135 -0
- package/src/data/query.ts +106 -0
- package/src/data/transport.ts +121 -0
- package/src/data/types.ts +303 -0
- package/src/data.ts +69 -894
- package/src/db/bun-sqlite.d.ts +4 -1
- package/src/db/changes-mysql.ts +61 -21
- package/src/db/changes-postgres.ts +197 -118
- package/src/db/changes-sqlite.ts +131 -64
- package/src/db/core.ts +38 -13
- package/src/db/index.ts +20 -4
- package/src/db/mysql.ts +22 -6
- package/src/db/postgres.ts +49 -13
- package/src/db/sqlite.ts +53 -27
- package/src/derive.d.mts +4 -1
- package/src/derive.mjs +136 -39
- package/src/dev-runtime.mjs +8 -4
- package/src/exec/errors.ts +57 -0
- package/src/exec/landing.ts +66 -0
- package/src/exec/scope.ts +174 -0
- package/src/exec/touch.ts +80 -0
- package/src/exec/validation.ts +176 -0
- package/src/exec.ts +191 -341
- package/src/extension.ts +8 -2
- package/src/first-paint.ts +26 -9
- package/src/form/value.ts +15 -0
- package/src/form.tsx +132 -43
- package/src/hub/channels.ts +102 -0
- package/src/hub/connections.ts +147 -0
- package/src/hub/delivery.ts +62 -0
- package/src/hub/feed.ts +168 -0
- package/src/hub/presence.ts +64 -0
- package/src/hub/route.ts +158 -0
- package/src/hub/subscriptions.ts +212 -0
- package/src/hub/topic.ts +98 -0
- package/src/hub/tracked.ts +12 -0
- package/src/hub/types.ts +190 -0
- package/src/hub/view.ts +147 -0
- package/src/hub.ts +132 -758
- package/src/index.ts +84 -10
- package/src/inline.mjs +356 -0
- package/src/inspect/compare.mjs +162 -0
- package/src/inspect/routes.mjs +154 -0
- package/src/inspect/samples.mjs +59 -0
- package/src/inspect/url.mjs +2 -0
- package/src/inspect.mjs +146 -187
- package/src/link/election.ts +178 -0
- package/src/link/lines.ts +27 -0
- package/src/link.ts +90 -166
- package/src/list-drizzle.ts +104 -18
- package/src/list.ts +127 -36
- package/src/listen.ts +32 -6
- package/src/live/api.ts +18 -0
- package/src/live/backoff.ts +6 -0
- package/src/live/frames.ts +42 -0
- package/src/live/read.ts +77 -0
- package/src/live/sub-types.ts +24 -0
- package/src/live/transport.ts +86 -0
- package/src/live/types.ts +52 -0
- package/src/live-fake.ts +20 -5
- package/src/live-target.mjs +112 -37
- package/src/live.ts +116 -225
- package/src/media.ts +8 -1
- package/src/migrations.mjs +14 -6
- package/src/mux.ts +161 -64
- package/src/og/DefaultCard.tsx +40 -9
- package/src/og/fonts.mjs +16 -8
- package/src/og/images.mjs +43 -22
- package/src/og/index.mjs +119 -43
- package/src/og/native.mjs +11 -4
- package/src/range.mjs +15 -3
- package/src/router.ts +56 -6
- package/src/routes.mjs +73 -22
- package/src/run/admission.ts +80 -0
- package/src/run/boot.tsx +115 -0
- package/src/run/cache-presentation.ts +67 -0
- package/src/run/cache.ts +254 -0
- package/src/run/helpers.ts +197 -0
- package/src/run/listeners.ts +155 -0
- package/src/run/navigation.ts +382 -0
- package/src/run/page-skeleton.tsx +35 -0
- package/src/run/prefetch.ts +128 -0
- package/src/run/retain-view.ts +18 -0
- package/src/run/shape-preparation.ts +97 -0
- package/src/run/shape-styles.ts +72 -0
- package/src/run/source-context/pin.ts +124 -0
- package/src/run/source-context.ts +311 -0
- package/src/run.tsx +50 -178
- package/src/rust/common.ts +163 -0
- package/src/rust/native.ts +282 -0
- package/src/rust.ts +103 -229
- package/src/server/card.tsx +65 -0
- package/src/server/compression.ts +115 -0
- package/src/server/core.ts +70 -0
- package/src/server/dispatch.ts +70 -0
- package/src/server/form.ts +85 -0
- package/src/server/functions.ts +129 -0
- package/src/server/handler.ts +39 -0
- package/src/server/head.ts +96 -0
- package/src/server/media.ts +60 -0
- package/src/server/navigation-cache.ts +80 -0
- package/src/server/navigation.tsx +85 -0
- package/src/server/page-cold.ts +95 -0
- package/src/server/page-stream.tsx +181 -0
- package/src/server/page-types.ts +51 -0
- package/src/server/page-warm.ts +130 -0
- package/src/server/page.ts +69 -0
- package/src/server/payload.ts +89 -0
- package/src/server/prerender.ts +60 -0
- package/src/server/preview.tsx +67 -0
- package/src/server/render.ts +167 -0
- package/src/server/request.ts +98 -0
- package/src/server/resolver.ts +228 -0
- package/src/server/routes.ts +150 -0
- package/src/server/services.ts +69 -0
- package/src/server/text.ts +29 -0
- package/src/server/types.ts +160 -0
- package/src/server.tsx +82 -971
- package/src/shape.mjs +30 -6
- package/src/share/display.ts +50 -0
- package/src/share/network.ts +119 -0
- package/src/share/receive.ts +70 -0
- package/src/share/state.ts +30 -0
- package/src/share/tab-queue.ts +28 -0
- package/src/share-server.ts +128 -43
- package/src/share.ts +103 -209
- package/src/sheets.mjs +30 -18
- package/src/sidecar/build.mjs +170 -0
- package/src/sidecar/cargo.mjs +164 -0
- package/src/sidecar/common.mjs +17 -0
- package/src/sidecar/dev-build.mjs +91 -0
- package/src/sidecar/dev-watch.mjs +88 -0
- package/src/sidecar/manifest.mjs +19 -0
- package/src/sidecar/process.mjs +58 -0
- package/src/sidecar/refresh.mjs +19 -0
- package/src/sidecar.mjs +75 -416
- package/src/socket/check.ts +50 -0
- package/src/socket/deliver.ts +16 -0
- package/src/socket/publish.ts +56 -0
- package/src/socket/rooms.ts +41 -0
- package/src/socket/route.ts +79 -0
- package/src/socket/shared.ts +26 -0
- package/src/socket/types.ts +48 -0
- package/src/socket.ts +205 -194
- package/src/stream.ts +10 -1
- package/src/verify/app.ts +39 -0
- package/src/verify/cleanup.ts +27 -0
- package/src/verify/database.ts +11 -0
- package/src/verify/live.ts +63 -0
- package/src/verify/notify.ts +35 -0
- package/src/verify/skip.ts +19 -0
- package/src/verify/support.ts +149 -0
- package/src/verify/timing.ts +15 -0
- package/src/verify/trigger.ts +14 -0
- package/src/verify.ts +77 -176
- package/src/view.tsx +96 -11
- package/src/vite/asset-bytes.mjs +95 -0
- package/src/vite/assets.mjs +117 -0
- package/src/vite/config-hooks.mjs +166 -0
- package/src/vite/dev-hooks.mjs +103 -0
- package/src/vite/dev.mjs +102 -0
- package/src/vite/entries.mjs +145 -0
- package/src/vite/module-hooks.mjs +288 -0
- package/src/vite/route-modules.mjs +289 -0
- package/src/vite/server-functions.mjs +222 -0
- package/src/vite/shape-entry-css.mjs +187 -0
- package/src/vite/shape-override.mjs +226 -0
- package/src/vite/shared.mjs +51 -0
- package/src/vite/sheets.mjs +92 -0
- package/src/vite/state.mjs +89 -0
- package/src/vite/syntax.mjs +139 -0
- package/src/vite.mjs +152 -853
- package/src/capture.d.mts +0 -1
package/src/db/changes-sqlite.ts
CHANGED
|
@@ -15,10 +15,20 @@ interface Conn {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
// statfs(2) magic numbers: https://man7.org/linux/man-pages/man2/statfs.2.html
|
|
18
|
-
const NETWORK = new Set([
|
|
18
|
+
const NETWORK = new Set([
|
|
19
|
+
0x6969 /* nfs */, 0x517b /* smb */, 0xfe534d42 /* smb2 */, 0xff534d42 /* cifs */,
|
|
20
|
+
0x65735546 /* fuse */, 0x5346414f /* afs */, 0xc36400 /* ceph */, 0x47504653 /* gpfs */,
|
|
21
|
+
])
|
|
19
22
|
|
|
20
23
|
/** Whether a file is on a network file system. `exec` and `statfs` are the test's way in. */
|
|
21
|
-
export function onNetwork(
|
|
24
|
+
export function onNetwork(
|
|
25
|
+
path: string,
|
|
26
|
+
{
|
|
27
|
+
platform = process.platform,
|
|
28
|
+
statfs = statfsSync,
|
|
29
|
+
df = (p: string) => execFileSync('df', ['-P', p], { encoding: 'utf8' }),
|
|
30
|
+
} = {},
|
|
31
|
+
): boolean {
|
|
22
32
|
try {
|
|
23
33
|
if (platform === 'linux') return NETWORK.has(statfs(path).type)
|
|
24
34
|
const device = df(path).trim().split('\n').at(-1)!.split(/\s+/)[0] ?? ''
|
|
@@ -28,84 +38,141 @@ export function onNetwork(path: string, { platform = process.platform, statfs =
|
|
|
28
38
|
}
|
|
29
39
|
}
|
|
30
40
|
|
|
31
|
-
interface Options {
|
|
41
|
+
interface Options {
|
|
42
|
+
path: string
|
|
43
|
+
open: (path: string) => Promise<Conn>
|
|
44
|
+
tables: string[]
|
|
45
|
+
interval?: number
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function startWatcher(
|
|
49
|
+
sink: Sink,
|
|
50
|
+
how: Probe,
|
|
51
|
+
o: Required<Options> & { set: (tick?: () => void, out?: Sink) => void },
|
|
52
|
+
) {
|
|
53
|
+
const { path, open, tables, interval, set } = o
|
|
54
|
+
if (how.mode === 'off') return () => {}
|
|
55
|
+
const c = await open(path)
|
|
56
|
+
const version = () => (c.query('pragma data_version', [], 'values') as number[][])[0]![0]!
|
|
57
|
+
const usable = how.mode === 'feed'
|
|
58
|
+
const newest = () =>
|
|
59
|
+
(c.query(`select coalesce(max(seq), 0) from ${LOG}`, [], 'values') as number[][])[0]![0]!
|
|
60
|
+
let seq = usable ? newest() : 0
|
|
61
|
+
let at = version()
|
|
62
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
63
|
+
const read = () => {
|
|
64
|
+
const rows = c.query(
|
|
65
|
+
`select seq, tbl, pk from ${LOG} where seq > ? order by seq`,
|
|
66
|
+
[seq],
|
|
67
|
+
'values',
|
|
68
|
+
) as [number, string, string | null][]
|
|
69
|
+
if (!rows.length) return
|
|
70
|
+
if (rows[0]![0] > seq + 1) sink.lost() // the log moved past us
|
|
71
|
+
const byTable = new Map<string, Set<string> | null>()
|
|
72
|
+
for (const [, table, id] of rows) {
|
|
73
|
+
if (id === null) byTable.set(table, null)
|
|
74
|
+
else if (byTable.get(table) !== null) {
|
|
75
|
+
const ids = byTable.get(table) ?? new Set()
|
|
76
|
+
ids.add(id)
|
|
77
|
+
byTable.set(table, ids.size > MAX_IDS ? null : ids)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
seq = rows.at(-1)![0]
|
|
81
|
+
const list: Change[] = []
|
|
82
|
+
for (const [table, ids] of byTable)
|
|
83
|
+
ids ? list.push(...[...ids].map((id) => ({ table, id }))) : list.push({ table })
|
|
84
|
+
sink.changes(list)
|
|
85
|
+
}
|
|
86
|
+
const look = () => {
|
|
87
|
+
try {
|
|
88
|
+
const now = version()
|
|
89
|
+
if (now === at) return
|
|
90
|
+
at = now
|
|
91
|
+
if (usable) read()
|
|
92
|
+
else sink.changes(tables.map((table) => ({ table })))
|
|
93
|
+
sink.up()
|
|
94
|
+
} catch (e) {
|
|
95
|
+
sink.down(`SQLite watcher: ${(e as Error | null)?.message ?? String(e)}`)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
set(look, sink)
|
|
99
|
+
const every = () => {
|
|
100
|
+
look()
|
|
101
|
+
timer = setTimeout(every, interval)
|
|
102
|
+
;(timer as { unref?(): void }).unref?.()
|
|
103
|
+
}
|
|
104
|
+
every()
|
|
105
|
+
return () => {
|
|
106
|
+
set()
|
|
107
|
+
clearTimeout(timer)
|
|
108
|
+
c.close?.()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
32
111
|
|
|
33
112
|
export function sqliteChanges({ path, open, tables, interval = 100 }: Options) {
|
|
34
|
-
const log = (c: Conn) =>
|
|
35
|
-
|
|
113
|
+
const log = (c: Conn) =>
|
|
114
|
+
(
|
|
115
|
+
c.query(
|
|
116
|
+
`select count(*) from sqlite_master where name = '${LOG}'`,
|
|
117
|
+
[],
|
|
118
|
+
'values',
|
|
119
|
+
) as number[][]
|
|
120
|
+
)[0]![0]! > 0
|
|
121
|
+
const triggers = (c: Conn) =>
|
|
122
|
+
(
|
|
123
|
+
c.query(
|
|
124
|
+
`select tbl_name from sqlite_master where type = 'trigger' and name like 'weft\\_changes\\_%\\_upd' escape '\\'`,
|
|
125
|
+
[],
|
|
126
|
+
'values',
|
|
127
|
+
) as string[][]
|
|
128
|
+
).map((r) => r[0])
|
|
36
129
|
let tick: (() => void) | undefined, out: Sink | undefined
|
|
37
130
|
|
|
38
131
|
async function probe(): Promise<Probe> {
|
|
39
|
-
if (path === ':memory:')
|
|
40
|
-
|
|
132
|
+
if (path === ':memory:')
|
|
133
|
+
return {
|
|
134
|
+
mode: 'off',
|
|
135
|
+
via: 'none',
|
|
136
|
+
why: 'an in-memory database has no other writers, and no second connection could see it',
|
|
137
|
+
fix: 'use a file to have writes from other processes reach `.live`',
|
|
138
|
+
}
|
|
139
|
+
if (onNetwork(existsSync(path) ? path : '.'))
|
|
140
|
+
return {
|
|
141
|
+
mode: 'off',
|
|
142
|
+
via: 'none',
|
|
143
|
+
why: `\`${path}\` is on a network file system: \`data_version\` and WAL don't work across hosts`,
|
|
144
|
+
fix: 'put the database on a local disk, or move to Postgres',
|
|
145
|
+
}
|
|
41
146
|
const c = await open(path)
|
|
42
147
|
try {
|
|
43
148
|
const have = log(c) ? triggers(c) : []
|
|
44
|
-
const missing = tables.filter(t => !have.includes(t))
|
|
45
|
-
if (missing.length)
|
|
149
|
+
const missing = tables.filter((t) => !have.includes(t))
|
|
150
|
+
if (missing.length)
|
|
151
|
+
return {
|
|
152
|
+
mode: 'poll',
|
|
153
|
+
via: 'sqlite-data-version',
|
|
154
|
+
why: `no trigger on ${missing.map((t) => `\`${t}\``).join(', ')}: any commit pokes every table, unrelated ones included`,
|
|
155
|
+
fix: 'run `weft db live`',
|
|
156
|
+
}
|
|
46
157
|
return { mode: 'feed', via: 'sqlite-data-version' }
|
|
47
158
|
} finally {
|
|
48
159
|
c.close?.()
|
|
49
160
|
}
|
|
50
161
|
}
|
|
51
162
|
|
|
52
|
-
async function start(sink: Sink, how: Probe) {
|
|
53
|
-
if (how.mode === 'off') return () => {}
|
|
54
|
-
const c = await open(path)
|
|
55
|
-
const version = () => (c.query('pragma data_version', [], 'values') as number[][])[0]![0]!
|
|
56
|
-
const usable = how.mode === 'feed'
|
|
57
|
-
const newest = () => (c.query(`select coalesce(max(seq), 0) from ${LOG}`, [], 'values') as number[][])[0]![0]!
|
|
58
|
-
let seq = usable ? newest() : 0
|
|
59
|
-
let at = version()
|
|
60
|
-
let timer: ReturnType<typeof setTimeout> | undefined
|
|
61
|
-
const read = () => {
|
|
62
|
-
const rows = c.query(`select seq, tbl, pk from ${LOG} where seq > ? order by seq`, [seq], 'values') as [number, string, string | null][]
|
|
63
|
-
if (!rows.length) return
|
|
64
|
-
if (rows[0]![0] > seq + 1) sink.lost() // the log moved past us
|
|
65
|
-
const byTable = new Map<string, Set<string> | null>()
|
|
66
|
-
for (const [, table, id] of rows) {
|
|
67
|
-
if (id === null) byTable.set(table, null)
|
|
68
|
-
else if (byTable.get(table) !== null) {
|
|
69
|
-
const ids = byTable.get(table) ?? new Set()
|
|
70
|
-
ids.add(id)
|
|
71
|
-
byTable.set(table, ids.size > MAX_IDS ? null : ids)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
seq = rows.at(-1)![0]
|
|
75
|
-
const list: Change[] = []
|
|
76
|
-
for (const [table, ids] of byTable) ids ? list.push(...[...ids].map(id => ({ table, id }))) : list.push({ table })
|
|
77
|
-
sink.changes(list)
|
|
78
|
-
}
|
|
79
|
-
const look = () => {
|
|
80
|
-
try {
|
|
81
|
-
const now = version()
|
|
82
|
-
if (now === at) return
|
|
83
|
-
at = now
|
|
84
|
-
if (usable) read()
|
|
85
|
-
else sink.changes(tables.map(table => ({ table })))
|
|
86
|
-
sink.up()
|
|
87
|
-
} catch (e) {
|
|
88
|
-
sink.down(`SQLite watcher: ${(e as Error | null)?.message ?? String(e)}`)
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
tick = look
|
|
92
|
-
out = sink
|
|
93
|
-
const every = () => {
|
|
94
|
-
look()
|
|
95
|
-
timer = setTimeout(every, interval)
|
|
96
|
-
;(timer as { unref?(): void }).unref?.()
|
|
97
|
-
}
|
|
98
|
-
every()
|
|
99
|
-
return () => {
|
|
100
|
-
tick = out = undefined
|
|
101
|
-
clearTimeout(timer)
|
|
102
|
-
c.close?.()
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
163
|
return {
|
|
107
164
|
probe,
|
|
108
|
-
start,
|
|
165
|
+
start: (sink: Sink, how: Probe) =>
|
|
166
|
+
startWatcher(sink, how, {
|
|
167
|
+
path,
|
|
168
|
+
open,
|
|
169
|
+
tables,
|
|
170
|
+
interval,
|
|
171
|
+
set: (next, target) => {
|
|
172
|
+
tick = next
|
|
173
|
+
out = target
|
|
174
|
+
},
|
|
175
|
+
}),
|
|
109
176
|
/** In this process, so the barrier needs no trip: read what's committed now, and let the hub drop what the poke covers. */
|
|
110
177
|
settle(tags: string[]) {
|
|
111
178
|
tick?.()
|
package/src/db/core.ts
CHANGED
|
@@ -6,7 +6,10 @@ import { currentScope, runtime, StartError } from '../data'
|
|
|
6
6
|
import type { Probe, Sink } from '../changes'
|
|
7
7
|
|
|
8
8
|
/** One migration file: its name and the SQL. */
|
|
9
|
-
export interface Migration {
|
|
9
|
+
export interface Migration {
|
|
10
|
+
tag: string
|
|
11
|
+
sql: string
|
|
12
|
+
}
|
|
10
13
|
|
|
11
14
|
/** What every Weft database carries on `$weft`, whichever driver is under it. */
|
|
12
15
|
export interface DbHandle {
|
|
@@ -16,15 +19,28 @@ export interface DbHandle {
|
|
|
16
19
|
/** Tables Q011 doesn't report (Better Auth's). */
|
|
17
20
|
exempt: Set<string>
|
|
18
21
|
/** `dbChanges`'s source: loaded on the first use, so a database with no live tables loads none of it. */
|
|
19
|
-
changes?(
|
|
22
|
+
changes?(
|
|
23
|
+
tables: string[],
|
|
24
|
+
options: { listenUrl?: string; interval?: number },
|
|
25
|
+
): Promise<{
|
|
26
|
+
probe(): Promise<Probe>
|
|
27
|
+
start(sink: Sink, how: Probe): Promise<() => unknown>
|
|
28
|
+
settle?(tags: string[]): Promise<void>
|
|
29
|
+
}>
|
|
20
30
|
}
|
|
21
31
|
|
|
22
|
-
/** Attach
|
|
23
|
-
export const register = <D extends object>(db: D, handle: DbHandle): D & { $weft: DbHandle } =>
|
|
32
|
+
/** Attach Weft's handle to a client. */
|
|
33
|
+
export const register = <D extends object>(db: D, handle: DbHandle): D & { $weft: DbHandle } =>
|
|
34
|
+
Object.assign(db, { $weft: handle })
|
|
24
35
|
|
|
25
36
|
/** The database can't be used: its connection, or a migration it needs. The client sees this; the server log gets the cause. */
|
|
26
37
|
export const unavailable = (cause: unknown) =>
|
|
27
|
-
new StartError(
|
|
38
|
+
new StartError(
|
|
39
|
+
'Q016',
|
|
40
|
+
'the database is unavailable, or a migration failed',
|
|
41
|
+
"start it and check `DATABASE_URL`; the server log has the driver's error",
|
|
42
|
+
{ status: 503, cause },
|
|
43
|
+
)
|
|
28
44
|
|
|
29
45
|
/**
|
|
30
46
|
* What a provider awaits before each statement, or its connection call: resolved once the list the server's first request
|
|
@@ -40,11 +56,14 @@ export function gate(handle: DbHandle, migrate: boolean) {
|
|
|
40
56
|
if (!migrate || !m) return undefined
|
|
41
57
|
if (m !== applied) {
|
|
42
58
|
applied = m
|
|
43
|
-
run = run
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
59
|
+
run = run
|
|
60
|
+
.catch(() => {})
|
|
61
|
+
.then(() => handle.migrate(m.list))
|
|
62
|
+
.catch((e) => {
|
|
63
|
+
if (applied === m) applied = undefined
|
|
64
|
+
m.report(e)
|
|
65
|
+
throw unavailable(e)
|
|
66
|
+
})
|
|
48
67
|
}
|
|
49
68
|
return run
|
|
50
69
|
}
|
|
@@ -54,10 +73,16 @@ export function gate(handle: DbHandle, migrate: boolean) {
|
|
|
54
73
|
export function touch(handle: DbHandle, sql: string) {
|
|
55
74
|
const t = currentScope()?.touched
|
|
56
75
|
if (!t) return
|
|
57
|
-
const target =
|
|
76
|
+
const target =
|
|
77
|
+
/^\s*(?:insert\s+into|update|delete\s+from)\s+(?:["`]?\w+["`]?\.)?["`]?([\w$]+)/i.exec(sql)?.[1]
|
|
58
78
|
if (target && !handle.exempt.has(target)) t.write.add(target)
|
|
59
|
-
for (const m of sql.matchAll(/\b(?:from|join)\s+(?:["`]?\w+["`]?\.)?["`]?([\w$]+)/gi))
|
|
79
|
+
for (const m of sql.matchAll(/\b(?:from|join)\s+(?:["`]?\w+["`]?\.)?["`]?([\w$]+)/gi))
|
|
80
|
+
if (m[1] !== target && !handle.exempt.has(m[1]!)) t.read.add(m[1]!)
|
|
60
81
|
}
|
|
61
82
|
|
|
62
83
|
/** A migration file's statements (drizzle-kit separates them with this marker). */
|
|
63
|
-
export const statements = (sql: string) =>
|
|
84
|
+
export const statements = (sql: string) =>
|
|
85
|
+
sql
|
|
86
|
+
.split('--> statement-breakpoint')
|
|
87
|
+
.map((s) => s.trim())
|
|
88
|
+
.filter(Boolean)
|
package/src/db/index.ts
CHANGED
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
// schema per table, so a table is its own validator: `input: schemas.todos.insert.pick({ title: true })`.
|
|
3
3
|
|
|
4
4
|
import { getTableName, isTable, type Table } from 'drizzle-orm'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
createInsertSchema,
|
|
7
|
+
createSelectSchema,
|
|
8
|
+
createUpdateSchema,
|
|
9
|
+
type BuildSchema,
|
|
10
|
+
} from 'drizzle-zod'
|
|
6
11
|
|
|
7
12
|
export type { DbHandle, Migration } from './core'
|
|
8
13
|
|
|
@@ -11,15 +16,26 @@ type Select<T extends Table> = BuildSchema<'select', T['_']['columns'], undefine
|
|
|
11
16
|
type Insert<T extends Table> = BuildSchema<'insert', T['_']['columns'], undefined, undefined>
|
|
12
17
|
type Update<T extends Table> = BuildSchema<'update', T['_']['columns'], undefined, undefined>
|
|
13
18
|
/** The select and insert types of each table in a schema. */
|
|
14
|
-
export type Schemas<S> = {
|
|
19
|
+
export type Schemas<S> = {
|
|
20
|
+
[K in keyof TablesOf<S>]: TablesOf<S>[K] extends infer T extends Table
|
|
21
|
+
? { select: Select<T>; insert: Insert<T>; update: Update<T> }
|
|
22
|
+
: never
|
|
23
|
+
}
|
|
15
24
|
|
|
16
25
|
/** `refine`: per table, per column, what drizzle-zod takes: `{ todos: { title: s => s.trim().min(1) } }`. */
|
|
17
|
-
export function schemasOf<S extends Record<string, unknown>>(
|
|
26
|
+
export function schemasOf<S extends Record<string, unknown>>(
|
|
27
|
+
schema: S,
|
|
28
|
+
refine: { [K in keyof TablesOf<S>]?: Record<string, (s: never) => unknown> } = {},
|
|
29
|
+
): Schemas<S> {
|
|
18
30
|
const out: Record<string, unknown> = {}
|
|
19
31
|
for (const [k, t] of Object.entries(schema)) {
|
|
20
32
|
if (!isTable(t)) continue
|
|
21
33
|
const r = (refine as Record<string, never>)[k]
|
|
22
|
-
out[k] = {
|
|
34
|
+
out[k] = {
|
|
35
|
+
select: createSelectSchema(t, r),
|
|
36
|
+
insert: createInsertSchema(t, r),
|
|
37
|
+
update: createUpdateSchema(t, r),
|
|
38
|
+
}
|
|
23
39
|
}
|
|
24
40
|
return out as Schemas<S>
|
|
25
41
|
}
|
package/src/db/mysql.ts
CHANGED
|
@@ -24,10 +24,19 @@ export type MysqlDb<S extends Record<string, unknown>> = MySql2Database<S> & { $
|
|
|
24
24
|
const LOCK = 'weft_migrations'
|
|
25
25
|
|
|
26
26
|
/** A pooled Drizzle client on MySQL, with this app's migrations and write tracking. */
|
|
27
|
-
export function createDb<S extends Record<string, unknown>>({
|
|
27
|
+
export function createDb<S extends Record<string, unknown>>({
|
|
28
|
+
schema,
|
|
29
|
+
url,
|
|
30
|
+
migrate = true,
|
|
31
|
+
max = 10,
|
|
32
|
+
}: MysqlOptions<S>): MysqlDb<S> {
|
|
28
33
|
if (!url && process.env.WEFT_BUILD) url = 'mysql://localhost/build' // a build loads this module and never connects
|
|
29
34
|
if (!url?.startsWith('mysql://'))
|
|
30
|
-
throw new StartError(
|
|
35
|
+
throw new StartError(
|
|
36
|
+
'Q013',
|
|
37
|
+
`\`@weft-js/start/db/mysql\` needs a \`mysql://\` URL, not \`${(url ?? '').replace(/\/\/.*@/, '//…@')}\``,
|
|
38
|
+
'set `DATABASE_URL`, or `createDb` from `@weft-js/start/db/sqlite`',
|
|
39
|
+
)
|
|
31
40
|
// No `maxIdle` below the limit: that starts mysql2's reaper, a timer that keeps a finished `weft build` alive forever.
|
|
32
41
|
const pool = createPool({ uri: url, connectionLimit: max })
|
|
33
42
|
const handle: DbHandle = {
|
|
@@ -37,9 +46,11 @@ export function createDb<S extends Record<string, unknown>>({ schema, url, migra
|
|
|
37
46
|
const c = await pool.getConnection()
|
|
38
47
|
try {
|
|
39
48
|
await c.query('select get_lock(?, 60)', [LOCK])
|
|
40
|
-
await c.query(
|
|
49
|
+
await c.query(
|
|
50
|
+
'create table if not exists __weft_migrations (tag varchar(255) primary key, at timestamp not null default current_timestamp)',
|
|
51
|
+
)
|
|
41
52
|
const [rows] = await c.query('select tag from __weft_migrations')
|
|
42
|
-
const done = new Set((rows as { tag: string }[]).map(r => r.tag))
|
|
53
|
+
const done = new Set((rows as { tag: string }[]).map((r) => r.tag))
|
|
43
54
|
for (const m of list) {
|
|
44
55
|
if (done.has(m.tag)) continue
|
|
45
56
|
for (const s of statements(m.sql)) await c.query(s)
|
|
@@ -50,7 +61,8 @@ export function createDb<S extends Record<string, unknown>>({ schema, url, migra
|
|
|
50
61
|
c.release()
|
|
51
62
|
}
|
|
52
63
|
},
|
|
53
|
-
changes: (tables, o) =>
|
|
64
|
+
changes: (tables, o) =>
|
|
65
|
+
import('./changes-mysql').then((m) => m.mysqlChanges({ ...o, pool, tables })),
|
|
54
66
|
}
|
|
55
67
|
const ready = gate(handle, migrate)
|
|
56
68
|
// All Drizzle sends goes through these three, so holding them holds it; `handle.migrate` keeps the plain pool.
|
|
@@ -62,6 +74,10 @@ export function createDb<S extends Record<string, unknown>>({ schema, url, migra
|
|
|
62
74
|
return async (...a: unknown[]) => (await ready(), v.apply(t, a))
|
|
63
75
|
},
|
|
64
76
|
})
|
|
65
|
-
const db = drizzle(held, {
|
|
77
|
+
const db = drizzle(held, {
|
|
78
|
+
schema,
|
|
79
|
+
mode: 'default',
|
|
80
|
+
logger: { logQuery: (sql) => touch(handle, sql) },
|
|
81
|
+
})
|
|
66
82
|
return register(db, handle)
|
|
67
83
|
}
|
package/src/db/postgres.ts
CHANGED
|
@@ -16,28 +16,52 @@ export interface PostgresOptions<S extends Record<string, unknown>> {
|
|
|
16
16
|
migrate?: boolean
|
|
17
17
|
/** Pool size (default 10). */
|
|
18
18
|
max?: number
|
|
19
|
+
/**
|
|
20
|
+
* Keep each statement prepared on its connection (default true). Drizzle asks for neither, and an unprepared statement
|
|
21
|
+
* with parameters is two round trips, a describe before every bind. Off behind a pooler in transaction mode. A long
|
|
22
|
+
* list isn't kept: each length is a statement of its own, and each statement stays in the connection's memory.
|
|
23
|
+
*/
|
|
24
|
+
prepare?: boolean
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
/** Drizzle on postgres.js, with `$weft` for the framework's own needs. */
|
|
22
|
-
export type PostgresDb<S extends Record<string, unknown>> = PostgresJsDatabase<S> & {
|
|
28
|
+
export type PostgresDb<S extends Record<string, unknown>> = PostgresJsDatabase<S> & {
|
|
29
|
+
$weft: DbHandle
|
|
30
|
+
}
|
|
23
31
|
|
|
24
32
|
const LOCK = 7_301_986
|
|
33
|
+
/** The most parameters a statement that's kept takes. Past it, `inArray` over a growing list is what's asking. */
|
|
34
|
+
const KEPT = 32
|
|
25
35
|
|
|
26
36
|
/** A pooled Drizzle client on Postgres, with this app's migrations and write tracking. */
|
|
27
|
-
export function createDb<S extends Record<string, unknown>>({
|
|
37
|
+
export function createDb<S extends Record<string, unknown>>({
|
|
38
|
+
schema,
|
|
39
|
+
url,
|
|
40
|
+
migrate = true,
|
|
41
|
+
max = 10,
|
|
42
|
+
prepare = true,
|
|
43
|
+
}: PostgresOptions<S>): PostgresDb<S> {
|
|
28
44
|
if (!url && process.env.WEFT_BUILD) url = 'postgres://localhost/build' // a build loads this module and never connects
|
|
29
45
|
if (!/^postgres(ql)?:\/\//.test(url ?? ''))
|
|
30
|
-
throw new StartError(
|
|
46
|
+
throw new StartError(
|
|
47
|
+
'Q013',
|
|
48
|
+
`\`@weft-js/start/db/postgres\` needs a \`postgres://\` URL, not \`${(url ?? '').replace(/\/\/.*@/, '//…@')}\``,
|
|
49
|
+
'set `DATABASE_URL`, or `createDb` from `@weft-js/start/db/sqlite` for a file',
|
|
50
|
+
)
|
|
31
51
|
// Idle connections close, so a stopped server can exit.
|
|
32
52
|
const client = postgres(url, { max, idle_timeout: 20, onnotice: () => {} })
|
|
33
53
|
const handle: DbHandle = {
|
|
34
54
|
dialect: 'postgresql',
|
|
35
55
|
exempt: new Set(),
|
|
36
56
|
async migrate(list: Migration[]) {
|
|
37
|
-
await client.begin(async tx => {
|
|
57
|
+
await client.begin(async (tx) => {
|
|
38
58
|
await tx.unsafe(`select pg_advisory_xact_lock(${LOCK})`)
|
|
39
|
-
await tx.unsafe(
|
|
40
|
-
|
|
59
|
+
await tx.unsafe(
|
|
60
|
+
'create table if not exists __weft_migrations (tag text primary key, at timestamptz not null default now())',
|
|
61
|
+
)
|
|
62
|
+
const done = new Set(
|
|
63
|
+
(await tx.unsafe('select tag from __weft_migrations')).map((r) => r.tag),
|
|
64
|
+
)
|
|
41
65
|
for (const m of list) {
|
|
42
66
|
if (done.has(m.tag)) continue
|
|
43
67
|
for (const s of statements(m.sql)) await tx.unsafe(s)
|
|
@@ -45,21 +69,33 @@ export function createDb<S extends Record<string, unknown>>({ schema, url, migra
|
|
|
45
69
|
}
|
|
46
70
|
})
|
|
47
71
|
},
|
|
48
|
-
changes: (tables, o) =>
|
|
72
|
+
changes: (tables, o) =>
|
|
73
|
+
import('./changes-postgres').then((m) => m.postgresChanges({ ...o, url, client, tables })),
|
|
49
74
|
}
|
|
50
75
|
const ready = gate(handle, migrate)
|
|
51
76
|
// All Drizzle sends goes through `unsafe` and `begin`, so holding those holds it; `handle.migrate` keeps the plain client.
|
|
52
77
|
const held = new Proxy(client, {
|
|
53
78
|
get(t, key) {
|
|
54
|
-
if (key === 'begin')
|
|
79
|
+
if (key === 'begin')
|
|
80
|
+
return async (...a: Parameters<typeof t.begin>) => (await ready(), t.begin(...a))
|
|
55
81
|
if (key !== 'unsafe') return Reflect.get(t, key)
|
|
56
|
-
return (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
82
|
+
return (
|
|
83
|
+
text: string,
|
|
84
|
+
params?: postgres.ParameterOrJSON<never>[],
|
|
85
|
+
options?: postgres.UnsafeQueryOptions,
|
|
86
|
+
) => {
|
|
87
|
+
const send = () =>
|
|
88
|
+
t.unsafe(text, params, { ...options, prepare: prepare && (params?.length ?? 0) <= KEPT })
|
|
89
|
+
const after = <T>(go: () => PromiseLike<T>) => Promise.resolve(ready()).then(go)
|
|
90
|
+
// oxlint-disable unicorn/no-thenable -- postgres.js's query is a lazy thenable, and Drizzle awaits it as one
|
|
91
|
+
return {
|
|
92
|
+
values: () => after(() => send().values()),
|
|
93
|
+
then: <A, B>(ok: (rows: unknown) => A, no: (e: unknown) => B) =>
|
|
94
|
+
after(send).then(ok, no) /* oxlint-enable unicorn/no-thenable */,
|
|
95
|
+
}
|
|
60
96
|
}
|
|
61
97
|
},
|
|
62
98
|
})
|
|
63
|
-
const db = drizzle(held, { schema, logger: { logQuery: sql => touch(handle, sql) } })
|
|
99
|
+
const db = drizzle(held, { schema, logger: { logQuery: (sql) => touch(handle, sql) } })
|
|
64
100
|
return register(db, handle)
|
|
65
101
|
}
|
package/src/db/sqlite.ts
CHANGED
|
@@ -32,8 +32,11 @@ export async function open(path: string): Promise<Conn> {
|
|
|
32
32
|
const { Database } = await import('bun:sqlite')
|
|
33
33
|
const db = new Database(path, { create: true })
|
|
34
34
|
conn = {
|
|
35
|
-
query: (sql, params, method) =>
|
|
36
|
-
|
|
35
|
+
query: (sql, params, method) =>
|
|
36
|
+
method === 'run'
|
|
37
|
+
? void db.prepare(sql).run(...params)
|
|
38
|
+
: pick(db.prepare(sql).values(...params), method),
|
|
39
|
+
exec: (sql) => void db.run(sql),
|
|
37
40
|
close: () => db.close(),
|
|
38
41
|
}
|
|
39
42
|
} else {
|
|
@@ -46,7 +49,7 @@ export async function open(path: string): Promise<Conn> {
|
|
|
46
49
|
s.setReturnArrays(true)
|
|
47
50
|
return pick(s.all(...(params as SQLInputValue[])) as unknown as unknown[][], method)
|
|
48
51
|
},
|
|
49
|
-
exec: sql => db.exec(sql),
|
|
52
|
+
exec: (sql) => db.exec(sql),
|
|
50
53
|
close: () => db.close(),
|
|
51
54
|
}
|
|
52
55
|
}
|
|
@@ -55,6 +58,26 @@ export async function open(path: string): Promise<Conn> {
|
|
|
55
58
|
return conn
|
|
56
59
|
}
|
|
57
60
|
|
|
61
|
+
async function migrateSqlite(connect: () => Promise<Conn>, list: Migration[]) {
|
|
62
|
+
const c = await connect()
|
|
63
|
+
c.exec('create table if not exists __weft_migrations (tag text primary key, at integer not null)')
|
|
64
|
+
const done = new Set(
|
|
65
|
+
(c.query('select tag from __weft_migrations', [], 'values') as string[][]).map((r) => r[0]),
|
|
66
|
+
)
|
|
67
|
+
for (const m of list) {
|
|
68
|
+
if (done.has(m.tag)) continue
|
|
69
|
+
c.exec('begin')
|
|
70
|
+
try {
|
|
71
|
+
for (const s of statements(m.sql)) c.exec(s)
|
|
72
|
+
c.query('insert into __weft_migrations values (?, ?)', [m.tag, Date.now()], 'run')
|
|
73
|
+
c.exec('commit')
|
|
74
|
+
} catch (e) {
|
|
75
|
+
c.exec('rollback')
|
|
76
|
+
throw e
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
58
81
|
/** `createDb` on SQLite: the schema, the file, and whether to migrate on first use. */
|
|
59
82
|
export interface SqliteOptions<S extends Record<string, unknown>> {
|
|
60
83
|
schema: S
|
|
@@ -65,38 +88,37 @@ export interface SqliteOptions<S extends Record<string, unknown>> {
|
|
|
65
88
|
}
|
|
66
89
|
|
|
67
90
|
/** Drizzle over a local file, with `$weft` for the framework's own needs. */
|
|
68
|
-
export type SqliteDb<S extends Record<string, unknown>> = SqliteRemoteDatabase<S> & {
|
|
91
|
+
export type SqliteDb<S extends Record<string, unknown>> = SqliteRemoteDatabase<S> & {
|
|
92
|
+
$weft: DbHandle
|
|
93
|
+
}
|
|
69
94
|
|
|
70
95
|
/** A Drizzle client on a SQLite file, with this app's migrations and write tracking. */
|
|
71
|
-
export function createDb<S extends Record<string, unknown>>({
|
|
96
|
+
export function createDb<S extends Record<string, unknown>>({
|
|
97
|
+
schema,
|
|
98
|
+
url = 'file:.data/app.db',
|
|
99
|
+
migrate = true,
|
|
100
|
+
}: SqliteOptions<S>): SqliteDb<S> {
|
|
72
101
|
if (/^[a-z][a-z0-9+.-]+:\/\//i.test(url) && !url.startsWith('file:'))
|
|
73
|
-
throw new StartError(
|
|
102
|
+
throw new StartError(
|
|
103
|
+
'Q013',
|
|
104
|
+
`\`@weft-js/start/db/sqlite\` can't open \`${url.replace(/\/\/.*@/, '//…@')}\``,
|
|
105
|
+
'use `file:<path>`, or `createDb` from `@weft-js/start/db/postgres` for a Postgres URL',
|
|
106
|
+
)
|
|
74
107
|
const path = url.replace(/^file:/, '')
|
|
75
108
|
let conn: Promise<Conn> | undefined
|
|
76
|
-
const connect = () =>
|
|
109
|
+
const connect = () =>
|
|
110
|
+
(conn ??= open(path).catch((e) => {
|
|
111
|
+
conn = undefined
|
|
112
|
+
throw e
|
|
113
|
+
}))
|
|
77
114
|
const inside = new AsyncLocalStorage<boolean>()
|
|
78
115
|
let held: Promise<unknown> = Promise.resolve()
|
|
79
116
|
const handle: DbHandle = {
|
|
80
117
|
dialect: 'sqlite',
|
|
81
118
|
exempt: new Set(),
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const done = new Set((c.query('select tag from __weft_migrations', [], 'values') as string[][]).map(r => r[0]))
|
|
86
|
-
for (const m of list) {
|
|
87
|
-
if (done.has(m.tag)) continue
|
|
88
|
-
c.exec('begin')
|
|
89
|
-
try {
|
|
90
|
-
for (const s of statements(m.sql)) c.exec(s)
|
|
91
|
-
c.query('insert into __weft_migrations values (?, ?)', [m.tag, Date.now()], 'run')
|
|
92
|
-
c.exec('commit')
|
|
93
|
-
} catch (e) {
|
|
94
|
-
c.exec('rollback')
|
|
95
|
-
throw e
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
},
|
|
99
|
-
changes: (tables, o) => import('./changes-sqlite').then(m => m.sqliteChanges({ ...o, path, open, tables })),
|
|
119
|
+
migrate: (list) => migrateSqlite(connect, list),
|
|
120
|
+
changes: (tables, o) =>
|
|
121
|
+
import('./changes-sqlite').then((m) => m.sqliteChanges({ ...o, path, open, tables })),
|
|
100
122
|
}
|
|
101
123
|
const ready = gate(handle, migrate)
|
|
102
124
|
const run = async (sql: string, params: unknown[], method: Method) => {
|
|
@@ -112,7 +134,9 @@ export function createDb<S extends Record<string, unknown>>({ schema, url = 'fil
|
|
|
112
134
|
await held
|
|
113
135
|
c.exec('begin')
|
|
114
136
|
try {
|
|
115
|
-
const out = list.map(
|
|
137
|
+
const out = list.map(
|
|
138
|
+
(q) => (touch(handle, q.sql), { rows: c.query(q.sql, q.params, q.method) as unknown[] }),
|
|
139
|
+
)
|
|
116
140
|
c.exec('commit')
|
|
117
141
|
return out
|
|
118
142
|
} catch (e) {
|
|
@@ -127,7 +151,9 @@ export function createDb<S extends Record<string, unknown>>({ schema, url = 'fil
|
|
|
127
151
|
await ready()
|
|
128
152
|
const before = held
|
|
129
153
|
let release!: () => void
|
|
130
|
-
held = new Promise<void>(ok => {
|
|
154
|
+
held = new Promise<void>((ok) => {
|
|
155
|
+
release = ok
|
|
156
|
+
})
|
|
131
157
|
await before
|
|
132
158
|
try {
|
|
133
159
|
return await inside.run(true, () => transaction(...args))
|
package/src/derive.d.mts
CHANGED
|
@@ -9,7 +9,10 @@ export function descriptionProblem(text: string): string | undefined
|
|
|
9
9
|
/** A page's title and description, as a person would pick them out of its HTML. */
|
|
10
10
|
export function pageText(html: string): { title?: string; description?: string }
|
|
11
11
|
/** The site's URL, from its config or from what the host says about this deploy. */
|
|
12
|
-
export function deployUrl(
|
|
12
|
+
export function deployUrl(
|
|
13
|
+
site: { url?: string | undefined } | undefined,
|
|
14
|
+
env?: Record<string, string | undefined>,
|
|
15
|
+
): string | undefined
|
|
13
16
|
/** The card title's font size, smaller for a longer title. */
|
|
14
17
|
export function cardTitleSize(title: string): number
|
|
15
18
|
/** `paths` in pages that each fit in a response header. */
|