@weft-js/start 0.0.0-stage → 0.1.0

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 (75) hide show
  1. package/LICENSE +16 -0
  2. package/README.md +41 -2
  3. package/package.json +107 -4
  4. package/src/app.mjs +82 -0
  5. package/src/auth/better-auth.ts +58 -0
  6. package/src/auth/drizzle.ts +17 -0
  7. package/src/auth.ts +47 -0
  8. package/src/boot.ts +44 -0
  9. package/src/builtin.tsx +19 -0
  10. package/src/bus/postgres.ts +91 -0
  11. package/src/bus.ts +48 -0
  12. package/src/capture.d.mts +1 -0
  13. package/src/capture.mjs +15 -0
  14. package/src/changes-sql.d.mts +14 -0
  15. package/src/changes-sql.mjs +99 -0
  16. package/src/changes.ts +253 -0
  17. package/src/channel-fake.ts +55 -0
  18. package/src/channel.ts +616 -0
  19. package/src/client.tsx +20 -0
  20. package/src/contract.mjs +308 -0
  21. package/src/data.ts +929 -0
  22. package/src/db/bun-sqlite.d.ts +10 -0
  23. package/src/db/changes-mysql.ts +86 -0
  24. package/src/db/changes-postgres.ts +159 -0
  25. package/src/db/changes-sqlite.ts +116 -0
  26. package/src/db/core.ts +63 -0
  27. package/src/db/index.ts +27 -0
  28. package/src/db/mysql.ts +67 -0
  29. package/src/db/postgres.ts +65 -0
  30. package/src/db/sqlite.ts +139 -0
  31. package/src/derive.d.mts +16 -0
  32. package/src/derive.mjs +140 -0
  33. package/src/dev-runtime.mjs +42 -0
  34. package/src/exec.ts +519 -0
  35. package/src/extension.ts +38 -0
  36. package/src/first-paint.ts +57 -0
  37. package/src/form.tsx +134 -0
  38. package/src/gate.css +6 -0
  39. package/src/hub.ts +822 -0
  40. package/src/index.ts +106 -0
  41. package/src/inline.mjs +2 -0
  42. package/src/inspect.mjs +280 -0
  43. package/src/link.ts +456 -0
  44. package/src/list-drizzle.ts +50 -0
  45. package/src/list.ts +224 -0
  46. package/src/listen.ts +28 -0
  47. package/src/live-fake.ts +42 -0
  48. package/src/live-target.mjs +144 -0
  49. package/src/live.ts +499 -0
  50. package/src/media.ts +43 -0
  51. package/src/migrations.mjs +19 -0
  52. package/src/mux-session.ts +6 -0
  53. package/src/mux.ts +135 -0
  54. package/src/og/DefaultCard.tsx +60 -0
  55. package/src/og/fonts.mjs +64 -0
  56. package/src/og/images.mjs +54 -0
  57. package/src/og/index.mjs +146 -0
  58. package/src/og/native.mjs +29 -0
  59. package/src/range.mjs +19 -0
  60. package/src/router.ts +149 -0
  61. package/src/routes.mjs +142 -0
  62. package/src/run.tsx +198 -0
  63. package/src/rust.ts +333 -0
  64. package/src/server.tsx +1014 -0
  65. package/src/shape.d.mts +3 -0
  66. package/src/shape.mjs +73 -0
  67. package/src/share-server.ts +208 -0
  68. package/src/share.ts +503 -0
  69. package/src/sheets.mjs +75 -0
  70. package/src/sidecar.mjs +503 -0
  71. package/src/socket.ts +389 -0
  72. package/src/stream.ts +13 -0
  73. package/src/verify.ts +251 -0
  74. package/src/view.tsx +47 -0
  75. package/src/vite.mjs +899 -0
@@ -0,0 +1,3 @@
1
+ export function toJson(schema: unknown, side: 'input' | 'output'): unknown
2
+ export function schemaTree(schema: unknown, name?: string): unknown
3
+ export function shapeTree(def: unknown): unknown
package/src/shape.mjs ADDED
@@ -0,0 +1,73 @@
1
+ // A query's result shape from its `output` schema (research 31 §3). The client never gets the schema: the plugin puts
2
+ // this tree in the query's stub, and the server computes the same one, so a streamed fallback hydrates cleanly.
3
+ // @weft-js/core/shape's `fill` turns a tree into a placeholder value.
4
+
5
+ /** A Standard Schema as JSON Schema (draft 2020-12), or undefined when it has no converter or can't be represented. */
6
+ export function toJson(schema, side) {
7
+ const std = schema?.['~standard']
8
+ try {
9
+ if (std?.jsonSchema) return std.jsonSchema[side]({ target: 'draft-2020-12' })
10
+ if (typeof schema?.toJSONSchema === 'function') return schema.toJSONSchema()
11
+ } catch {}
12
+ }
13
+
14
+ /** Typical lengths by field name, for strings with no hint (research 11 §7.1). */
15
+ const NAMES = { title: 40, name: 16, label: 16, email: 22, body: 200, content: 200, text: 120, description: 120, summary: 120, slug: 24, url: 40, id: 8 }
16
+ const FORMATS = { email: 22, uuid: 36, url: 40, uri: 40, 'date-time': 24, date: 10, time: 8 }
17
+ /** A string with no hint: X036 if a skeleton shows one. */
18
+ const UNHINTED = 16
19
+
20
+ function string(s, name) {
21
+ if (s.minLength !== undefined && s.minLength === s.maxLength) return { $s: s.minLength }
22
+ if (s.maxLength !== undefined) return { $s: Math.round(((s.minLength ?? 0) + s.maxLength) / 2) }
23
+ const known = FORMATS[s.format] ?? NAMES[name]
24
+ if (known !== undefined) return { $s: known }
25
+ if (s.minLength !== undefined) return { $s: s.minLength }
26
+ return { $u: UNHINTED }
27
+ }
28
+
29
+ /** JSON Schema → shape tree. `name` is the field it's under. */
30
+ export function schemaTree(s, name = '') {
31
+ if (!s || typeof s !== 'object') return null
32
+ const example = s.example ?? s.examples?.[0]
33
+ if (example !== undefined) return example
34
+ if (s.const !== undefined) return s.const
35
+ if (s.enum) return s.enum[0]
36
+ const union = s.anyOf ?? s.oneOf
37
+ if (union) {
38
+ const some = union.filter(x => x.type !== 'null')
39
+ return some.length < union.length && !(s.probability >= 0.5) ? null : schemaTree(some[0], name)
40
+ }
41
+ switch (s.type) {
42
+ case 'string':
43
+ return string(s, name)
44
+ case 'number':
45
+ case 'integer':
46
+ return s.minimum ?? 0
47
+ case 'boolean':
48
+ return false
49
+ case 'array':
50
+ return { ...(s.maxItems !== undefined ? { $a: s.maxItems } : {}), $of: schemaTree(s.items, name) }
51
+ case 'object': {
52
+ const out = {}
53
+ for (const [k, v] of Object.entries(s.properties ?? {})) if (s.required?.includes(k) || v.probability >= 0.5) out[k] = schemaTree(v, k)
54
+ return out
55
+ }
56
+ }
57
+ return null
58
+ }
59
+
60
+ /** `example` (a declared `shape`) over `tree`. */
61
+ function merge(tree, example) {
62
+ if (example === undefined) return tree
63
+ if (Array.isArray(example)) return example.length > 1 || !tree?.$of ? example : { ...tree, $of: merge(tree.$of, example[0]) }
64
+ if (example && typeof example === 'object') return Object.fromEntries([...Object.entries(tree ?? {}), ...Object.entries(example).map(([k, v]) => [k, merge(tree?.[k], v)])])
65
+ return example
66
+ }
67
+
68
+ /** A definition's tree: its `output`, with its declared `shape` over it. Undefined when it declares neither. */
69
+ export function shapeTree(def) {
70
+ const json = def.output && toJson(def.output, 'output')
71
+ if (!json) return def.shape
72
+ return merge(schemaTree(json), def.shape)
73
+ }
@@ -0,0 +1,208 @@
1
+ // The server's half of `interaction({ share })` (research 45 §6, §7). One join per socket, `$share`, and every key goes
2
+ // through `realtime({ share })`: no such option, no join (R011), and a key it refuses gets nothing and can say nothing.
3
+ //
4
+ // rooms 64 hub channels, by a hash of the key, so bus subscriptions stay bounded. One listener per instance: an
5
+ // event costs the keys it names and who holds them, not every socket. A client still receives only its keys.
6
+ // batching one `u` frame adds and drops many keys and one `set` frame carries many bits; each comes back as one frame.
7
+ // snapshot each instance answers for its own sockets, from what those sockets last said, so a joiner costs its
8
+ // holders nothing on the wire. What's held lives and dies with the connection: nothing outlives a socket.
9
+ // rule per key, or `{ keys }` for a whole frame in one call, so a join of up to 400 keys is one database read.
10
+ // leaving `left` says zero for what the socket held. Every event is volatile: no ring, no replay.
11
+ // identity the sender's connection id is stamped here. Bits and an opaque id, nothing else exists to see.
12
+
13
+ import type { Context, Fn, StandardSchemaV1 } from './data'
14
+ import { StartError } from './data'
15
+ import type { Ev, Hub } from './hub'
16
+
17
+ export const SHARE = '$share'
18
+ export const MAX_KEY = 128
19
+ /** Entries one frame may carry. The client chunks to this. */
20
+ export const MAX_BATCH = 400
21
+ const SHARDS = 64
22
+
23
+ /** One key at a time, or `{ keys }` to answer a whole join at once: a rule that reads a database runs once for it. */
24
+ export type ShareRule =
25
+ | ((args: { ctx: Context; key: string }) => boolean | Promise<boolean>)
26
+ | { keys: (args: { ctx: Context; keys: string[] }) => Iterable<string> | Promise<Iterable<string>> }
27
+
28
+ type Issue = { issues: { message: string }[] }
29
+ type Sent = { add?: unknown; drop?: unknown; e?: unknown } | null | undefined
30
+ const isIssue = (r: unknown): r is Issue => !!(r as Partial<Issue> | null | undefined)?.issues
31
+ const schema = <T,>(validate: (v: Sent) => T | Issue): StandardSchemaV1<T, T> => ({ '~standard': { version: 1, vendor: 'weft', validate: (v: unknown) => { const r = validate(v as Sent); return isIssue(r) ? r : { value: r } } } })
32
+
33
+ /** What a handler of the share channel is given. */
34
+ interface Said<M = unknown> { msg: M; id: string; ctx: Context; reply: Client['reply'] }
35
+ const no = (message: string): Issue => ({ issues: [{ message }] })
36
+ const isKey = (k: unknown): k is string => typeof k === 'string' && k.length > 0 && k.length <= MAX_KEY
37
+ const isBits = (b: unknown) => Number.isInteger(b) && (b as number) >= 0 && (b as number) <= 7
38
+ const anything = schema(v => v)
39
+
40
+ /** Who has `k` in `s`: `[id, key, bits]`. */
41
+ type Triple = [id: string, key: string, bits: number]
42
+ interface Client { id: string; keys: Map<string, number>; reply(name: string, msg: unknown): void }
43
+ interface Shard { name: string; refs: number; ch: ReturnType<Hub['channel']>; on: (ev: Ev | null) => void }
44
+ interface Sharing { clients: Map<string, Client>; holders: Map<string, Set<Client>>; shards: Map<number, Shard> }
45
+
46
+ const states = new WeakMap<Hub, Sharing>()
47
+ const stateOf = (h: Hub) => {
48
+ let s = states.get(h)
49
+ if (!s) states.set(h, (s = { clients: new Map(), holders: new Map(), shards: new Map() }))
50
+ return s
51
+ }
52
+
53
+ /** FNV-1a. It only has to spread keys, and to agree across instances. */
54
+ function shardOf(key: string) {
55
+ let x = 2166136261
56
+ for (let i = 0; i < key.length; i++) x = Math.imul(x ^ key.charCodeAt(i), 16777619)
57
+ return (x >>> 0) % SHARDS
58
+ }
59
+
60
+ const byShard = <T,>(items: T[], key: (item: T) => string) => {
61
+ const out = new Map<number, T[]>()
62
+ for (const item of items) {
63
+ const s = shardOf(key(item))
64
+ const list = out.get(s)
65
+ if (list) list.push(item)
66
+ else out.set(s, [item])
67
+ }
68
+ return out
69
+ }
70
+
71
+ const publish = (h: Hub, shard: number, body: object) => h.bus.publish(`weft:s:${shard}`, `{"v":1,${JSON.stringify(body).slice(1)}`)
72
+
73
+ function heard(h: Hub, st: Sharing, shard: number, ev: Ev | null) {
74
+ if (!ev) return // the bus lost something: bits are re-said on a beat, and peers age out
75
+ const m = (ev.p ??= JSON.parse(ev.raw)) as { f?: string; e?: [string, number][]; a?: string; k?: string[]; t?: string; s?: Triple[] }
76
+ if (m.t) return (m.s && st.clients.get(m.t)?.reply('b', { s: m.s }))
77
+ if (m.a) {
78
+ const s: Triple[] = []
79
+ for (const key of m.k ?? []) for (const c of st.holders.get(key) ?? []) {
80
+ const bits = c.keys.get(key)
81
+ if (bits && c.id !== m.a) s.push([c.id, key, bits])
82
+ }
83
+ if (s.length) void publish(h, shard, { t: m.a, s })
84
+ return
85
+ }
86
+ // One frame per client per event, whatever it names.
87
+ const each = new Map<Client, Triple[]>()
88
+ for (const [key, bits] of m.e ?? []) for (const c of st.holders.get(key) ?? []) {
89
+ if (c.id === m.f) continue
90
+ const list = each.get(c)
91
+ if (list) list.push([m.f!, key, bits])
92
+ else each.set(c, [[m.f!, key, bits]])
93
+ }
94
+ for (const [c, s] of each) c.reply('b', { s })
95
+ }
96
+
97
+ function hold(h: Hub, st: Sharing, c: Client, key: string) {
98
+ if (c.keys.has(key)) return false
99
+ c.keys.set(key, 0)
100
+ let set = st.holders.get(key)
101
+ if (!set) st.holders.set(key, (set = new Set()))
102
+ set.add(c)
103
+ const n = shardOf(key)
104
+ let shard = st.shards.get(n)
105
+ if (!shard) {
106
+ const name = `weft:s:${n}`, ch = h.channel(name), on = (ev: Ev | null) => heard(h, st, n, ev)
107
+ ch.listeners.add(on)
108
+ st.shards.set(n, (shard = { name, refs: 0, ch, on }))
109
+ }
110
+ shard.refs++
111
+ return true
112
+ }
113
+
114
+ function unhold(h: Hub, st: Sharing, c: Client, key: string) {
115
+ if (!c.keys.delete(key)) return
116
+ const set = st.holders.get(key)!
117
+ set.delete(c)
118
+ if (!set.size) st.holders.delete(key)
119
+ const n = shardOf(key), shard = st.shards.get(n)!
120
+ if (--shard.refs) return
121
+ st.shards.delete(n)
122
+ h.unlisten(shard.ch, shard.on)
123
+ }
124
+
125
+ /** Say zero for what these keys held, then let go of them. */
126
+ function release(h: Hub, st: Sharing, c: Client, keys: string[]) {
127
+ const held = keys.filter(k => c.keys.get(k))
128
+ for (const k of keys) unhold(h, st, c, k)
129
+ return Promise.all([...byShard(held, k => k)].map(([n, list]) => publish(h, n, { f: c.id, e: list.map(k => [k, 0]) })))
130
+ }
131
+
132
+ async function allowedKeys(rule: ShareRule, ctx: Context, keys: string[]) {
133
+ if (!keys.length) return keys
134
+ if (typeof rule !== 'function') {
135
+ const yes = await Promise.resolve().then(() => rule.keys({ ctx, keys })).catch(() => [])
136
+ const passed = new Set(yes)
137
+ return keys.filter(k => passed.has(k))
138
+ }
139
+ const answers = await Promise.all(keys.map(key => Promise.resolve().then(() => rule({ ctx, key })).catch(() => false)))
140
+ return keys.filter((_, i) => answers[i])
141
+ }
142
+
143
+ /** The `channel` behind `share`, made per hub. socket.ts takes it for `$share`, so every limit a channel has applies. */
144
+ export function shareChannel(h: Hub): Fn {
145
+ const rule = h.share
146
+ const st = stateOf(h)
147
+ const client = (id: string, reply: Client['reply']) => {
148
+ let c = st.clients.get(id)
149
+ if (!c) st.clients.set(id, (c = { id, keys: new Map(), reply }))
150
+ return c
151
+ }
152
+ return {
153
+ kind: 'channel', $id: SHARE,
154
+ def: {
155
+ authorize() {
156
+ if (rule) return true
157
+ throw new StartError('R011', '`interaction({ share })` crossed to the socket, and nothing allows it', 'add `share: ({ ctx, key }) => …` to `realtime()`; `() => true` must be written out')
158
+ },
159
+ client: {
160
+ u: schema<{ add: string[]; drop: string[] }>(v => {
161
+ const add = v?.add ?? [], drop = v?.drop ?? []
162
+ return Array.isArray(add) && Array.isArray(drop) && add.length + drop.length <= MAX_BATCH && add.every(isKey) && drop.every(isKey) ? { add, drop } : no(`\`u\` takes up to ${MAX_BATCH} keys of 1 to ${MAX_KEY} characters`)
163
+ }),
164
+ set: schema<{ e: [string, number][] }>(v => (Array.isArray(v?.e) && v.e.length <= MAX_BATCH && v.e.every(x => Array.isArray(x) && isKey(x[0]) && isBits(x[1])) ? { e: v.e } : no('`set` takes `[key, bits]` pairs, bits 0 to 7'))),
165
+ },
166
+ server: { ok: anything, b: anything },
167
+ on: {
168
+ async u({ msg, id, ctx, reply }: Said<{ add: string[]; drop: string[] }>) {
169
+ const c = client(id, reply)
170
+ await release(h, st, c, msg.drop)
171
+ const room = Math.max(0, h.limits.shareKeys - c.keys.size)
172
+ const fresh = msg.add.filter(k => !c.keys.has(k))
173
+ const asked = fresh.slice(0, room)
174
+ const ok = await allowedKeys(rule!, ctx, asked)
175
+ if (!st.clients.has(id)) return // it left while the rule ran
176
+ const passed = new Set(ok)
177
+ for (const k of ok) hold(h, st, c, k)
178
+ await Promise.all(ok.length ? [...byShard<string>(ok, k => k)].map(([n, k]) => st.shards.get(n)!.ch.ready.then(() => publish(h, n, { a: id, k }))) : [])
179
+ reply('ok', { id, no: fresh.filter(k => !passed.has(k)) })
180
+ },
181
+ set({ msg, id }: Said<{ e: [string, number][] }>) {
182
+ const c = st.clients.get(id)
183
+ if (!c) return
184
+ const known = msg.e.filter(([k]) => c.keys.has(k))
185
+ for (const [k, bits] of known) c.keys.set(k, bits)
186
+ return Promise.all([...byShard(known, e => e[0])].map(([n, e]) => publish(h, n, { f: id, e })))
187
+ },
188
+ },
189
+ left({ id }: Pick<Said, 'id'>) {
190
+ const c = st.clients.get(id)
191
+ if (!c) return
192
+ st.clients.delete(id)
193
+ return release(h, st, c, [...c.keys.keys()])
194
+ },
195
+ /** The session again: every key this socket holds goes back through the rule. */
196
+ async recheck({ id, ctx, reply }: Said) {
197
+ const c = st.clients.get(id)
198
+ if (!c || !rule) return
199
+ const keys = [...c.keys.keys()]
200
+ const passed = new Set(await allowedKeys(rule, ctx, keys))
201
+ const gone = keys.filter(k => !passed.has(k))
202
+ if (!gone.length) return
203
+ await release(h, st, c, gone)
204
+ reply('ok', { id, no: gone })
205
+ },
206
+ },
207
+ } as unknown as Fn
208
+ }