@weft-js/start 0.0.0-stage → 0.1.1
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/LICENSE +16 -0
- package/README.md +41 -2
- package/package.json +107 -4
- package/src/app.mjs +93 -0
- package/src/auth/better-auth.ts +58 -0
- package/src/auth/drizzle.ts +17 -0
- package/src/auth.ts +47 -0
- package/src/boot.ts +44 -0
- package/src/builtin.tsx +19 -0
- package/src/bus/postgres.ts +91 -0
- package/src/bus.ts +48 -0
- package/src/capture.d.mts +1 -0
- package/src/capture.mjs +15 -0
- package/src/changes-sql.d.mts +14 -0
- package/src/changes-sql.mjs +99 -0
- package/src/changes.ts +253 -0
- package/src/channel-fake.ts +55 -0
- package/src/channel.ts +622 -0
- package/src/client.tsx +20 -0
- package/src/contract.mjs +312 -0
- package/src/data.ts +929 -0
- package/src/db/bun-sqlite.d.ts +10 -0
- package/src/db/changes-mysql.ts +86 -0
- package/src/db/changes-postgres.ts +159 -0
- package/src/db/changes-sqlite.ts +116 -0
- package/src/db/core.ts +63 -0
- package/src/db/index.ts +27 -0
- package/src/db/mysql.ts +67 -0
- package/src/db/postgres.ts +65 -0
- package/src/db/sqlite.ts +139 -0
- package/src/derive.d.mts +16 -0
- package/src/derive.mjs +140 -0
- package/src/dev-runtime.mjs +42 -0
- package/src/exec.ts +519 -0
- package/src/extension.ts +38 -0
- package/src/first-paint.ts +57 -0
- package/src/form.tsx +134 -0
- package/src/gate.css +6 -0
- package/src/hub.ts +822 -0
- package/src/index.ts +106 -0
- package/src/inline.mjs +2 -0
- package/src/inspect.mjs +280 -0
- package/src/link.ts +461 -0
- package/src/list-drizzle.ts +50 -0
- package/src/list.ts +224 -0
- package/src/listen.ts +28 -0
- package/src/live-fake.ts +42 -0
- package/src/live-target.mjs +147 -0
- package/src/live.ts +499 -0
- package/src/media.ts +43 -0
- package/src/migrations.mjs +19 -0
- package/src/mux-session.ts +6 -0
- package/src/mux.ts +135 -0
- package/src/og/DefaultCard.tsx +60 -0
- package/src/og/fonts.mjs +64 -0
- package/src/og/images.mjs +54 -0
- package/src/og/index.mjs +146 -0
- package/src/og/native.mjs +29 -0
- package/src/range.mjs +19 -0
- package/src/router.ts +149 -0
- package/src/routes.mjs +142 -0
- package/src/run.tsx +202 -0
- package/src/rust.ts +333 -0
- package/src/server.tsx +1021 -0
- package/src/shape.d.mts +3 -0
- package/src/shape.mjs +73 -0
- package/src/share-server.ts +208 -0
- package/src/share.ts +503 -0
- package/src/sheets.mjs +75 -0
- package/src/sidecar.mjs +503 -0
- package/src/socket.ts +389 -0
- package/src/stream.ts +13 -0
- package/src/verify.ts +251 -0
- package/src/view.tsx +47 -0
- package/src/vite.mjs +899 -0
package/src/shape.d.mts
ADDED
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
|
+
}
|