@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.
Files changed (214) hide show
  1. package/README.md +1278 -12
  2. package/crate/Cargo.lock +1347 -0
  3. package/crate/Cargo.toml +72 -0
  4. package/crate/README.md +322 -0
  5. package/crate/rust-toolchain.toml +3 -0
  6. package/crate/src/build/emit.rs +103 -0
  7. package/crate/src/build.rs +367 -0
  8. package/crate/src/ctx.rs +186 -0
  9. package/crate/src/envelope.rs +163 -0
  10. package/crate/src/functions.rs +173 -0
  11. package/crate/src/lib.rs +118 -0
  12. package/crate/src/native.rs +212 -0
  13. package/crate/src/otel.rs +62 -0
  14. package/crate/src/server.rs +203 -0
  15. package/package.json +46 -37
  16. package/src/app.mjs +38 -8
  17. package/src/asset-receipt.d.mts +13 -0
  18. package/src/asset-receipt.mjs +61 -0
  19. package/src/auth/better-auth.ts +215 -17
  20. package/src/auth/drizzle.ts +1 -1
  21. package/src/auth.ts +21 -6
  22. package/src/boot.ts +19 -8
  23. package/src/bus/postgres.ts +32 -19
  24. package/src/bus.ts +7 -4
  25. package/src/{capture.mjs → capture.ts} +34 -1
  26. package/src/changes-sql.d.mts +8 -2
  27. package/src/changes-sql.mjs +26 -10
  28. package/src/changes.ts +165 -66
  29. package/src/channel/connections.ts +216 -0
  30. package/src/channel/socket.ts +396 -0
  31. package/src/channel-fake.ts +19 -4
  32. package/src/channel.ts +120 -492
  33. package/src/client.tsx +9 -2
  34. package/src/contract/describe.mjs +231 -0
  35. package/src/contract.mjs +144 -157
  36. package/src/data/cache.ts +354 -0
  37. package/src/data/definitions.ts +94 -0
  38. package/src/data/draft.ts +104 -0
  39. package/src/data/foundation.ts +95 -0
  40. package/src/data/live-api.ts +22 -0
  41. package/src/data/mutation.ts +138 -0
  42. package/src/data/primitives.ts +135 -0
  43. package/src/data/query.ts +106 -0
  44. package/src/data/transport.ts +121 -0
  45. package/src/data/types.ts +303 -0
  46. package/src/data.ts +69 -894
  47. package/src/db/bun-sqlite.d.ts +4 -1
  48. package/src/db/changes-mysql.ts +61 -21
  49. package/src/db/changes-postgres.ts +197 -118
  50. package/src/db/changes-sqlite.ts +131 -64
  51. package/src/db/core.ts +38 -13
  52. package/src/db/index.ts +20 -4
  53. package/src/db/mysql.ts +22 -6
  54. package/src/db/postgres.ts +49 -13
  55. package/src/db/sqlite.ts +53 -27
  56. package/src/derive.d.mts +4 -1
  57. package/src/derive.mjs +136 -39
  58. package/src/dev-runtime.mjs +8 -4
  59. package/src/exec/errors.ts +57 -0
  60. package/src/exec/landing.ts +66 -0
  61. package/src/exec/scope.ts +174 -0
  62. package/src/exec/touch.ts +80 -0
  63. package/src/exec/validation.ts +176 -0
  64. package/src/exec.ts +191 -341
  65. package/src/extension.ts +8 -2
  66. package/src/first-paint.ts +26 -9
  67. package/src/form/value.ts +15 -0
  68. package/src/form.tsx +132 -43
  69. package/src/hub/channels.ts +102 -0
  70. package/src/hub/connections.ts +147 -0
  71. package/src/hub/delivery.ts +62 -0
  72. package/src/hub/feed.ts +168 -0
  73. package/src/hub/presence.ts +64 -0
  74. package/src/hub/route.ts +158 -0
  75. package/src/hub/subscriptions.ts +212 -0
  76. package/src/hub/topic.ts +98 -0
  77. package/src/hub/tracked.ts +12 -0
  78. package/src/hub/types.ts +190 -0
  79. package/src/hub/view.ts +147 -0
  80. package/src/hub.ts +132 -758
  81. package/src/index.ts +84 -10
  82. package/src/inline.mjs +356 -0
  83. package/src/inspect/compare.mjs +162 -0
  84. package/src/inspect/routes.mjs +154 -0
  85. package/src/inspect/samples.mjs +59 -0
  86. package/src/inspect/url.mjs +2 -0
  87. package/src/inspect.mjs +146 -187
  88. package/src/link/election.ts +178 -0
  89. package/src/link/lines.ts +27 -0
  90. package/src/link.ts +90 -166
  91. package/src/list-drizzle.ts +104 -18
  92. package/src/list.ts +127 -36
  93. package/src/listen.ts +32 -6
  94. package/src/live/api.ts +18 -0
  95. package/src/live/backoff.ts +6 -0
  96. package/src/live/frames.ts +42 -0
  97. package/src/live/read.ts +77 -0
  98. package/src/live/sub-types.ts +24 -0
  99. package/src/live/transport.ts +86 -0
  100. package/src/live/types.ts +52 -0
  101. package/src/live-fake.ts +20 -5
  102. package/src/live-target.mjs +112 -37
  103. package/src/live.ts +116 -225
  104. package/src/media.ts +8 -1
  105. package/src/migrations.mjs +14 -6
  106. package/src/mux.ts +161 -64
  107. package/src/og/DefaultCard.tsx +40 -9
  108. package/src/og/fonts.mjs +16 -8
  109. package/src/og/images.mjs +43 -22
  110. package/src/og/index.mjs +119 -43
  111. package/src/og/native.mjs +11 -4
  112. package/src/range.mjs +15 -3
  113. package/src/router.ts +56 -6
  114. package/src/routes.mjs +73 -22
  115. package/src/run/admission.ts +80 -0
  116. package/src/run/boot.tsx +115 -0
  117. package/src/run/cache-presentation.ts +67 -0
  118. package/src/run/cache.ts +254 -0
  119. package/src/run/helpers.ts +197 -0
  120. package/src/run/listeners.ts +155 -0
  121. package/src/run/navigation.ts +382 -0
  122. package/src/run/page-skeleton.tsx +35 -0
  123. package/src/run/prefetch.ts +128 -0
  124. package/src/run/retain-view.ts +18 -0
  125. package/src/run/shape-preparation.ts +97 -0
  126. package/src/run/shape-styles.ts +72 -0
  127. package/src/run/source-context/pin.ts +124 -0
  128. package/src/run/source-context.ts +311 -0
  129. package/src/run.tsx +50 -178
  130. package/src/rust/common.ts +163 -0
  131. package/src/rust/native.ts +282 -0
  132. package/src/rust.ts +103 -229
  133. package/src/server/card.tsx +65 -0
  134. package/src/server/compression.ts +115 -0
  135. package/src/server/core.ts +70 -0
  136. package/src/server/dispatch.ts +70 -0
  137. package/src/server/form.ts +85 -0
  138. package/src/server/functions.ts +129 -0
  139. package/src/server/handler.ts +39 -0
  140. package/src/server/head.ts +96 -0
  141. package/src/server/media.ts +60 -0
  142. package/src/server/navigation-cache.ts +80 -0
  143. package/src/server/navigation.tsx +85 -0
  144. package/src/server/page-cold.ts +95 -0
  145. package/src/server/page-stream.tsx +181 -0
  146. package/src/server/page-types.ts +51 -0
  147. package/src/server/page-warm.ts +130 -0
  148. package/src/server/page.ts +69 -0
  149. package/src/server/payload.ts +89 -0
  150. package/src/server/prerender.ts +60 -0
  151. package/src/server/preview.tsx +67 -0
  152. package/src/server/render.ts +167 -0
  153. package/src/server/request.ts +98 -0
  154. package/src/server/resolver.ts +228 -0
  155. package/src/server/routes.ts +150 -0
  156. package/src/server/services.ts +69 -0
  157. package/src/server/text.ts +29 -0
  158. package/src/server/types.ts +160 -0
  159. package/src/server.tsx +82 -971
  160. package/src/shape.mjs +30 -6
  161. package/src/share/display.ts +50 -0
  162. package/src/share/network.ts +119 -0
  163. package/src/share/receive.ts +70 -0
  164. package/src/share/state.ts +30 -0
  165. package/src/share/tab-queue.ts +28 -0
  166. package/src/share-server.ts +128 -43
  167. package/src/share.ts +103 -209
  168. package/src/sheets.mjs +30 -18
  169. package/src/sidecar/build.mjs +170 -0
  170. package/src/sidecar/cargo.mjs +164 -0
  171. package/src/sidecar/common.mjs +17 -0
  172. package/src/sidecar/dev-build.mjs +91 -0
  173. package/src/sidecar/dev-watch.mjs +88 -0
  174. package/src/sidecar/manifest.mjs +19 -0
  175. package/src/sidecar/process.mjs +58 -0
  176. package/src/sidecar/refresh.mjs +19 -0
  177. package/src/sidecar.mjs +75 -416
  178. package/src/socket/check.ts +50 -0
  179. package/src/socket/deliver.ts +16 -0
  180. package/src/socket/publish.ts +56 -0
  181. package/src/socket/rooms.ts +41 -0
  182. package/src/socket/route.ts +79 -0
  183. package/src/socket/shared.ts +26 -0
  184. package/src/socket/types.ts +48 -0
  185. package/src/socket.ts +205 -194
  186. package/src/stream.ts +10 -1
  187. package/src/verify/app.ts +39 -0
  188. package/src/verify/cleanup.ts +27 -0
  189. package/src/verify/database.ts +11 -0
  190. package/src/verify/live.ts +63 -0
  191. package/src/verify/notify.ts +35 -0
  192. package/src/verify/skip.ts +19 -0
  193. package/src/verify/support.ts +149 -0
  194. package/src/verify/timing.ts +15 -0
  195. package/src/verify/trigger.ts +14 -0
  196. package/src/verify.ts +77 -176
  197. package/src/view.tsx +96 -11
  198. package/src/vite/asset-bytes.mjs +95 -0
  199. package/src/vite/assets.mjs +117 -0
  200. package/src/vite/config-hooks.mjs +166 -0
  201. package/src/vite/dev-hooks.mjs +103 -0
  202. package/src/vite/dev.mjs +102 -0
  203. package/src/vite/entries.mjs +145 -0
  204. package/src/vite/module-hooks.mjs +288 -0
  205. package/src/vite/route-modules.mjs +289 -0
  206. package/src/vite/server-functions.mjs +222 -0
  207. package/src/vite/shape-entry-css.mjs +187 -0
  208. package/src/vite/shape-override.mjs +226 -0
  209. package/src/vite/shared.mjs +51 -0
  210. package/src/vite/sheets.mjs +92 -0
  211. package/src/vite/state.mjs +89 -0
  212. package/src/vite/syntax.mjs +139 -0
  213. package/src/vite.mjs +152 -853
  214. package/src/capture.d.mts +0 -1
@@ -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([0x6969 /* nfs */, 0x517b /* smb */, 0xfe534d42 /* smb2 */, 0xff534d42 /* cifs */, 0x65735546 /* fuse */, 0x5346414f /* afs */, 0xc36400 /* ceph */, 0x47504653 /* gpfs */])
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(path: string, { platform = process.platform, statfs = statfsSync, df = (p: string) => execFileSync('df', ['-P', p], { encoding: 'utf8' }) } = {}): boolean {
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 { path: string; open: (path: string) => Promise<Conn>; tables: string[]; interval?: number }
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) => (c.query(`select count(*) from sqlite_master where name = '${LOG}'`, [], 'values') as number[][])[0]![0]! > 0
35
- const triggers = (c: Conn) => (c.query(`select tbl_name from sqlite_master where type = 'trigger' and name like 'weft\\_changes\\_%\\_upd' escape '\\'`, [], 'values') as string[][]).map(r => r[0])
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:') return { mode: 'off', via: 'none', why: 'an in-memory database has no other writers, and no second connection could see it', fix: 'use a file to have writes from other processes reach `.live`' }
40
- if (onNetwork(existsSync(path) ? path : '.')) return { mode: 'off', via: 'none', why: `\`${path}\` is on a network file system: \`data_version\` and WAL don't work across hosts`, fix: 'put the database on a local disk, or move to Postgres' }
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) return { mode: 'poll', via: 'sqlite-data-version', why: `no trigger on ${missing.map(t => `\`${t}\``).join(', ')}: any commit pokes every table, unrelated ones included`, fix: 'run `weft db live`' }
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 { tag: string; sql: string }
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?(tables: string[], options: { listenUrl?: string; interval?: number }): Promise<{ probe(): Promise<Probe>; start(sink: Sink, how: Probe): Promise<() => unknown>; settle?(tags: string[]): Promise<void> }>
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 weft's handle to a client. */
23
- export const register = <D extends object>(db: D, handle: DbHandle): D & { $weft: DbHandle } => Object.assign(db, { $weft: handle })
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('Q016', 'the database is unavailable, or a migration failed', 'start it and check `DATABASE_URL`; the server log has the driver\'s error', { status: 503, cause })
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.catch(() => {}).then(() => handle.migrate(m.list)).catch(e => {
44
- if (applied === m) applied = undefined
45
- m.report(e)
46
- throw unavailable(e)
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 = /^\s*(?:insert\s+into|update|delete\s+from)\s+(?:["`]?\w+["`]?\.)?["`]?([\w$]+)/i.exec(sql)?.[1]
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)) if (m[1] !== target && !handle.exempt.has(m[1]!)) t.read.add(m[1]!)
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) => sql.split('--> statement-breakpoint').map(s => s.trim()).filter(Boolean)
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 { createInsertSchema, createSelectSchema, createUpdateSchema, type BuildSchema } from 'drizzle-zod'
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> = { [K in keyof TablesOf<S>]: TablesOf<S>[K] extends infer T extends Table ? { select: Select<T>; insert: Insert<T>; update: Update<T> } : never }
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>>(schema: S, refine: { [K in keyof TablesOf<S>]?: Record<string, (s: never) => unknown> } = {}): Schemas<S> {
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] = { select: createSelectSchema(t, r), insert: createInsertSchema(t, r), update: createUpdateSchema(t, r) }
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>>({ schema, url, migrate = true, max = 10 }: MysqlOptions<S>): MysqlDb<S> {
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('Q013', `\`@weft-js/start/db/mysql\` needs a \`mysql://\` URL, not \`${(url ?? '').replace(/\/\/.*@/, '//…@')}\``, 'set `DATABASE_URL`, or `createDb` from `@weft-js/start/db/sqlite`')
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('create table if not exists __weft_migrations (tag varchar(255) primary key, at timestamp not null default current_timestamp)')
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) => import('./changes-mysql').then(m => m.mysqlChanges({ ...o, pool, tables })),
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, { schema, mode: 'default', logger: { logQuery: sql => touch(handle, sql) } })
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
  }
@@ -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> & { $weft: DbHandle }
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>>({ schema, url, migrate = true, max = 10 }: PostgresOptions<S>): PostgresDb<S> {
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('Q013', `\`@weft-js/start/db/postgres\` needs a \`postgres://\` URL, not \`${(url ?? '').replace(/\/\/.*@/, '//…@')}\``, 'set `DATABASE_URL`, or `createDb` from `@weft-js/start/db/sqlite` for a file')
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('create table if not exists __weft_migrations (tag text primary key, at timestamptz not null default now())')
40
- const done = new Set((await tx.unsafe('select tag from __weft_migrations')).map(r => r.tag))
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) => import('./changes-postgres').then(m => m.postgresChanges({ ...o, url, client, tables })),
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') return async (...a: Parameters<typeof t.begin>) => (await ready(), t.begin(...a))
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 (...a: Parameters<typeof t.unsafe>) => {
57
- const after = <T>(send: () => PromiseLike<T>) => Promise.resolve(ready()).then(send)
58
- // oxlint-disable-next-line unicorn/no-thenable -- postgres.js's query is a lazy thenable, and Drizzle awaits it as one
59
- return { values: () => after(() => t.unsafe(...a).values()), then: <A, B>(ok: (rows: unknown) => A, no: (e: unknown) => B) => after(() => t.unsafe(...a)).then(ok, no) }
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) => (method === 'run' ? void db.prepare(sql).run(...params) : pick(db.prepare(sql).values(...params), method)),
36
- exec: sql => void db.run(sql),
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> & { $weft: DbHandle }
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>>({ schema, url = 'file:.data/app.db', migrate = true }: SqliteOptions<S>): SqliteDb<S> {
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('Q013', `\`@weft-js/start/db/sqlite\` can't open \`${url.replace(/\/\/.*@/, '//…@')}\``, 'use `file:<path>`, or `createDb` from `@weft-js/start/db/postgres` for a Postgres URL')
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 = () => (conn ??= open(path).catch(e => { conn = undefined; throw e }))
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
- async migrate(list: Migration[]) {
83
- const c = await connect()
84
- c.exec('create table if not exists __weft_migrations (tag text primary key, at integer not null)')
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(q => (touch(handle, q.sql), { rows: c.query(q.sql, q.params, q.method) as unknown[] }))
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 => { release = 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(site: { url?: string | undefined } | undefined, env?: Record<string, string | undefined>): string | undefined
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. */