@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/mux.ts
CHANGED
|
@@ -11,11 +11,19 @@ import { attach, type Attached, type Peer } from './socket'
|
|
|
11
11
|
const LINES = 64
|
|
12
12
|
const HIGH = 64 * 1024
|
|
13
13
|
|
|
14
|
-
interface Physical {
|
|
14
|
+
interface Physical {
|
|
15
|
+
send(text: string): void
|
|
16
|
+
close(code: number, reason?: string): void
|
|
17
|
+
backlog(): number
|
|
18
|
+
}
|
|
15
19
|
type Open = { kind: 'c'; a: Promise<Attached> } | { kind: 'r'; c: Promise<Conn | undefined> }
|
|
16
20
|
|
|
17
21
|
const parsed = (text: string): unknown => {
|
|
18
|
-
try {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(text)
|
|
24
|
+
} catch {
|
|
25
|
+
return undefined
|
|
26
|
+
}
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
const counts = new WeakMap<Hub, Map<string, number>>()
|
|
@@ -25,27 +33,154 @@ const byUser = (h: Hub) => {
|
|
|
25
33
|
return m
|
|
26
34
|
}
|
|
27
35
|
|
|
28
|
-
|
|
36
|
+
interface Socket {
|
|
37
|
+
peer: Physical
|
|
38
|
+
request: Request
|
|
39
|
+
scope: Scope
|
|
40
|
+
h: Hub
|
|
41
|
+
lines: Map<string, Open>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function openLines(
|
|
45
|
+
{ peer, request, scope, h, lines }: Socket,
|
|
46
|
+
end: (id: string, code: number, reason?: string) => void,
|
|
47
|
+
) {
|
|
48
|
+
function openC(id: string) {
|
|
49
|
+
const p: Peer = {
|
|
50
|
+
send: (t) => {
|
|
51
|
+
if (lines.has(id)) peer.send(`f:${id}:${t}`)
|
|
52
|
+
},
|
|
53
|
+
close: (code, reason) => end(id, code, reason),
|
|
54
|
+
backlog: () => peer.backlog(),
|
|
55
|
+
}
|
|
56
|
+
lines.set(id, {
|
|
57
|
+
kind: 'c',
|
|
58
|
+
a: attach(p, request, scope, h, true).catch(() => {
|
|
59
|
+
end(id, 1011)
|
|
60
|
+
return { message() {}, close() {} } as unknown as Attached
|
|
61
|
+
}),
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function openR(id: string, hello: string) {
|
|
66
|
+
const body = parsed(hello)
|
|
67
|
+
if (!validBody(body) || body.k)
|
|
68
|
+
return peer.send(
|
|
69
|
+
`x:${id}:4000:${JSON.stringify({ code: 'Q002', message: 'a realtime line opened with a body of the wrong shape' })}`,
|
|
70
|
+
)
|
|
71
|
+
const sink: Sink = {
|
|
72
|
+
write: (event, data) => {
|
|
73
|
+
if (lines.has(id)) peer.send(`r:${id}:${event}:${data}`)
|
|
74
|
+
},
|
|
75
|
+
ping: () => {
|
|
76
|
+
if (lines.has(id)) peer.send(`r:${id}:ping:`)
|
|
77
|
+
},
|
|
78
|
+
backlog: () => peer.backlog(),
|
|
79
|
+
close: () => end(id, 1000),
|
|
80
|
+
}
|
|
81
|
+
const c = h.connect(request, scope, body, sink, true).catch((e) => {
|
|
82
|
+
const out =
|
|
83
|
+
e instanceof StartError
|
|
84
|
+
? { code: e.code, message: e.message }
|
|
85
|
+
: { message: 'Realtime failed.' }
|
|
86
|
+
end(id, 4000, JSON.stringify(out))
|
|
87
|
+
return undefined
|
|
88
|
+
})
|
|
89
|
+
lines.set(id, { kind: 'r', c })
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { openC, openR }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function messages(
|
|
96
|
+
{ peer, request, scope, h, lines }: Socket,
|
|
97
|
+
{
|
|
98
|
+
allowed,
|
|
99
|
+
end,
|
|
100
|
+
shut,
|
|
101
|
+
openC,
|
|
102
|
+
openR,
|
|
103
|
+
}: {
|
|
104
|
+
allowed: () => boolean
|
|
105
|
+
end: (id: string, code: number, reason?: string) => void
|
|
106
|
+
shut: (id: string, line: Open) => void
|
|
107
|
+
openC: (id: string) => void
|
|
108
|
+
openR: (id: string, hello: string) => void
|
|
109
|
+
},
|
|
110
|
+
) {
|
|
111
|
+
return (text: string) => {
|
|
112
|
+
if (text === 'p') return peer.send('p')
|
|
113
|
+
if (text.length > h.limits.body + 64) return peer.close(1009, 'too big')
|
|
114
|
+
if (!allowed()) {
|
|
115
|
+
scope.options.onDiagnostic?.({
|
|
116
|
+
code: 'R008',
|
|
117
|
+
severity: 'error',
|
|
118
|
+
message: `R008: one browser sent over ${h.limits.perSecond * 8} messages a second`,
|
|
119
|
+
})
|
|
120
|
+
return peer.close(1008, 'rate limit')
|
|
121
|
+
}
|
|
122
|
+
const at = text.indexOf(':', 2)
|
|
123
|
+
const id = text.slice(2, at),
|
|
124
|
+
rest = text.slice(at + 1)
|
|
125
|
+
const line = lines.get(id)
|
|
126
|
+
if (text[0] === 'f') {
|
|
127
|
+
if (line?.kind === 'c') void line.a.then((a) => a.message(rest))
|
|
128
|
+
else if (line) {
|
|
129
|
+
let body: Body | undefined
|
|
130
|
+
try {
|
|
131
|
+
body = JSON.parse(rest)
|
|
132
|
+
} catch {}
|
|
133
|
+
if (validBody(body))
|
|
134
|
+
void line.c
|
|
135
|
+
.then((c) => c && h.change(c, request, scope.options, body))
|
|
136
|
+
.catch(() => end(id, 1011))
|
|
137
|
+
}
|
|
138
|
+
} else if (text[0] === 'x') {
|
|
139
|
+
if (line) shut(id, line)
|
|
140
|
+
} else if (text[0] === 'o') {
|
|
141
|
+
if (line) return
|
|
142
|
+
if (lines.size >= LINES) return peer.send(`x:${id}:1008:too many lines`)
|
|
143
|
+
if (rest[0] === 'c') openC(id)
|
|
144
|
+
else if (rest[0] === 'r') openR(id, rest.slice(2))
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface Muxed {
|
|
150
|
+
message(text: string): void
|
|
151
|
+
close(): void
|
|
152
|
+
}
|
|
29
153
|
|
|
30
|
-
export async function muxed(
|
|
154
|
+
export async function muxed(
|
|
155
|
+
peer: Physical,
|
|
156
|
+
request: Request,
|
|
157
|
+
scope: Scope,
|
|
158
|
+
h: Hub = hub(),
|
|
159
|
+
): Promise<Muxed> {
|
|
31
160
|
const ctx = await scope.ctx()
|
|
32
161
|
const user = (ctx as { user?: { id?: string } }).user?.id
|
|
33
162
|
const users = byUser(h)
|
|
34
163
|
if (user !== undefined) {
|
|
35
164
|
if ((users.get(user) ?? 0) >= h.limits.connections) {
|
|
36
165
|
peer.close(1008, 'too many sockets')
|
|
37
|
-
scope.options.onDiagnostic?.({
|
|
166
|
+
scope.options.onDiagnostic?.({
|
|
167
|
+
code: 'R009',
|
|
168
|
+
severity: 'error',
|
|
169
|
+
message: `R009: user \`${user}\` already has ${h.limits.connections} sockets`,
|
|
170
|
+
})
|
|
38
171
|
return { message() {}, close() {} }
|
|
39
172
|
}
|
|
40
173
|
users.set(user, (users.get(user) ?? 0) + 1)
|
|
41
174
|
}
|
|
42
175
|
const lines = new Map<string, Open>()
|
|
176
|
+
const socket = { peer, request, scope, h, lines }
|
|
43
177
|
const bucket = { tokens: h.limits.burst * 8, at: Date.now() }
|
|
44
178
|
let closed = false
|
|
45
179
|
|
|
46
180
|
/** Every tab has the per-socket bucket; this is what stops them adding up without end. */
|
|
47
181
|
const allowed = () => {
|
|
48
|
-
const now = Date.now(),
|
|
182
|
+
const now = Date.now(),
|
|
183
|
+
rate = h.limits.perSecond * 8
|
|
49
184
|
bucket.tokens = Math.min(h.limits.burst * 8, bucket.tokens + ((now - bucket.at) * rate) / 1000)
|
|
50
185
|
bucket.at = now
|
|
51
186
|
return bucket.tokens-- >= 1
|
|
@@ -55,81 +190,43 @@ export async function muxed(peer: Physical, request: Request, scope: Scope, h: H
|
|
|
55
190
|
peer.send(`x:${id}:${code}:${reason}`)
|
|
56
191
|
}
|
|
57
192
|
|
|
58
|
-
|
|
59
|
-
const p: Peer = { send: t => { if (lines.has(id)) peer.send(`f:${id}:${t}`) }, close: (code, reason) => end(id, code, reason), backlog: () => peer.backlog() }
|
|
60
|
-
lines.set(id, { kind: 'c', a: attach(p, request, scope, h, true).catch(() => {
|
|
61
|
-
end(id, 1011)
|
|
62
|
-
return { message() {}, close() {} } as unknown as Attached
|
|
63
|
-
}) })
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function openR(id: string, hello: string) {
|
|
67
|
-
const body = parsed(hello)
|
|
68
|
-
if (!validBody(body) || body.k) return peer.send(`x:${id}:4000:${JSON.stringify({ code: 'Q002', message: 'a realtime line opened with a body of the wrong shape' })}`)
|
|
69
|
-
const sink: Sink = {
|
|
70
|
-
write: (event, data) => { if (lines.has(id)) peer.send(`r:${id}:${event}:${data}`) },
|
|
71
|
-
ping: () => { if (lines.has(id)) peer.send(`r:${id}:ping:`) },
|
|
72
|
-
backlog: () => peer.backlog(),
|
|
73
|
-
close: () => end(id, 1000),
|
|
74
|
-
}
|
|
75
|
-
const c = h.connect(request, scope, body, sink, true).catch(e => {
|
|
76
|
-
const out = e instanceof StartError ? { code: e.code, message: e.message } : { message: 'Realtime failed.' }
|
|
77
|
-
end(id, 4000, JSON.stringify(out))
|
|
78
|
-
return undefined
|
|
79
|
-
})
|
|
80
|
-
lines.set(id, { kind: 'r', c })
|
|
81
|
-
}
|
|
193
|
+
const { openC, openR } = openLines(socket, end)
|
|
82
194
|
|
|
83
195
|
function shut(id: string, line: Open) {
|
|
84
196
|
lines.delete(id)
|
|
85
|
-
if (line.kind === 'c') void line.a.then(a => a.close())
|
|
86
|
-
else void line.c.then(c => c && h.close(c))
|
|
197
|
+
if (line.kind === 'c') void line.a.then((a) => a.close())
|
|
198
|
+
else void line.c.then((c) => c && h.close(c))
|
|
87
199
|
}
|
|
88
200
|
|
|
89
201
|
const drain = setInterval(() => {
|
|
90
202
|
if (peer.backlog() >= HIGH) return
|
|
91
|
-
for (const l of lines.values()) if (l.kind === 'r') void l.c.then(c => c && h.drain(c))
|
|
203
|
+
for (const l of lines.values()) if (l.kind === 'r') void l.c.then((c) => c && h.drain(c))
|
|
92
204
|
}, 100)
|
|
93
205
|
;(drain as { unref?(): void }).unref?.()
|
|
94
206
|
|
|
95
|
-
|
|
207
|
+
const release = () => {
|
|
208
|
+
clearInterval(drain)
|
|
209
|
+
if (user === undefined) return
|
|
210
|
+
const n = (users.get(user) ?? 1) - 1
|
|
211
|
+
if (n) users.set(user, n)
|
|
212
|
+
else users.delete(user)
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
peer.send(`h:${await sessionOf(ctx)}`)
|
|
216
|
+
} catch (e) {
|
|
217
|
+
release() // the caller never gets a close() for a socket that failed to open
|
|
218
|
+
throw e
|
|
219
|
+
}
|
|
220
|
+
const message = messages(socket, { allowed, end, shut, openC, openR })
|
|
96
221
|
return {
|
|
97
222
|
message(text) {
|
|
98
|
-
|
|
99
|
-
if (text.length > h.limits.body + 64) return peer.close(1009, 'too big')
|
|
100
|
-
if (!allowed()) {
|
|
101
|
-
scope.options.onDiagnostic?.({ code: 'R008', severity: 'error', message: `R008: one browser sent over ${h.limits.perSecond * 8} messages a second` })
|
|
102
|
-
return peer.close(1008, 'rate limit')
|
|
103
|
-
}
|
|
104
|
-
const at = text.indexOf(':', 2)
|
|
105
|
-
const id = text.slice(2, at), rest = text.slice(at + 1)
|
|
106
|
-
const line = lines.get(id)
|
|
107
|
-
if (text[0] === 'f') {
|
|
108
|
-
if (line?.kind === 'c') void line.a.then(a => a.message(rest))
|
|
109
|
-
else if (line) {
|
|
110
|
-
let body: Body | undefined
|
|
111
|
-
try { body = JSON.parse(rest) } catch {}
|
|
112
|
-
if (validBody(body)) void line.c.then(c => c && h.change(c, request, scope.options, body)).catch(() => end(id, 1011))
|
|
113
|
-
}
|
|
114
|
-
} else if (text[0] === 'x') {
|
|
115
|
-
if (line) shut(id, line)
|
|
116
|
-
} else if (text[0] === 'o') {
|
|
117
|
-
if (line) return
|
|
118
|
-
if (lines.size >= LINES) return peer.send(`x:${id}:1008:too many lines`)
|
|
119
|
-
if (rest[0] === 'c') openC(id)
|
|
120
|
-
else if (rest[0] === 'r') openR(id, rest.slice(2))
|
|
121
|
-
}
|
|
223
|
+
return message(text)
|
|
122
224
|
},
|
|
123
225
|
close() {
|
|
124
226
|
if (closed) return
|
|
125
227
|
closed = true
|
|
126
|
-
clearInterval(drain)
|
|
127
228
|
for (const [id, line] of Array.from(lines)) shut(id, line)
|
|
128
|
-
|
|
129
|
-
const n = (users.get(user) ?? 1) - 1
|
|
130
|
-
if (n) users.set(user, n)
|
|
131
|
-
else users.delete(user)
|
|
132
|
-
}
|
|
229
|
+
release()
|
|
133
230
|
},
|
|
134
231
|
}
|
|
135
232
|
}
|
package/src/og/DefaultCard.tsx
CHANGED
|
@@ -24,7 +24,11 @@ const family = (name: string | undefined) => (name ? `'${name}',Inter` : 'Inter'
|
|
|
24
24
|
/** The title without the template the site wraps around it: the card shows the site's name on its own. */
|
|
25
25
|
function heading(title: string, template: string | undefined) {
|
|
26
26
|
const [before = '', after = ''] = template?.split('%s') ?? []
|
|
27
|
-
return title.startsWith(before) &&
|
|
27
|
+
return title.startsWith(before) &&
|
|
28
|
+
title.endsWith(after) &&
|
|
29
|
+
title.length > before.length + after.length
|
|
30
|
+
? title.slice(before.length, title.length - after.length)
|
|
31
|
+
: title
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
/** Everything the card draws, worked out once: a card renders one time, to a string. */
|
|
@@ -36,8 +40,13 @@ function look({ title, description, site, path }: DefaultProps) {
|
|
|
36
40
|
const host = site.url?.replace(/^https?:\/\//, '').replace(/\/+$/, '')
|
|
37
41
|
const shown = heading(title, site.title)
|
|
38
42
|
return {
|
|
39
|
-
c,
|
|
40
|
-
|
|
43
|
+
c,
|
|
44
|
+
description,
|
|
45
|
+
title: shown,
|
|
46
|
+
name: site.name ?? '',
|
|
47
|
+
size: cardTitleSize(shown),
|
|
48
|
+
display: family(card.fonts?.display?.family),
|
|
49
|
+
body: family(card.fonts?.body?.family),
|
|
41
50
|
address: host && host + (path && path !== '/' ? path.replace(/[?#].*/, '') : ''),
|
|
42
51
|
}
|
|
43
52
|
}
|
|
@@ -45,16 +54,38 @@ function look({ title, description, site, path }: DefaultProps) {
|
|
|
45
54
|
export function DefaultCard(props: DefaultProps) {
|
|
46
55
|
const { c, title, description, name, size, display, body, address } = untrack(() => look(props))
|
|
47
56
|
return (
|
|
48
|
-
<div
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
<div
|
|
58
|
+
style={`display:flex;flex-direction:column;width:100%;height:100%;padding:68px 76px 60px;background-color:${c.background};color:${c.text};font-family:${body}`}
|
|
59
|
+
>
|
|
60
|
+
<div
|
|
61
|
+
style={`display:flex;align-items:center;font-family:${display};font-size:32px;font-weight:600;letter-spacing:-0.3px`}
|
|
62
|
+
>
|
|
63
|
+
<div
|
|
64
|
+
style={`width:22px;height:22px;border-radius:5px;background-color:${c.accent};margin-right:16px`}
|
|
65
|
+
/>
|
|
51
66
|
<span>{name}</span>
|
|
52
67
|
</div>
|
|
53
68
|
<div style="display:flex;flex-direction:column;justify-content:center;flex:1;min-height:0;padding:24px 0">
|
|
54
|
-
<div
|
|
55
|
-
|
|
69
|
+
<div
|
|
70
|
+
style={`display:block;font-family:${display};font-size:${size}px;font-weight:700;line-height:1.08;letter-spacing:-1.5px;overflow-wrap:anywhere;line-clamp:3`}
|
|
71
|
+
>
|
|
72
|
+
{title}
|
|
73
|
+
</div>
|
|
74
|
+
{description ? (
|
|
75
|
+
<div
|
|
76
|
+
style={`display:block;margin-top:26px;font-size:30px;line-height:1.38;color:${c.subtle};line-clamp:3`}
|
|
77
|
+
>
|
|
78
|
+
{description}
|
|
79
|
+
</div>
|
|
80
|
+
) : null}
|
|
56
81
|
</div>
|
|
57
|
-
{address ?
|
|
82
|
+
{address ? (
|
|
83
|
+
<div
|
|
84
|
+
style={`display:flex;align-items:center;font-size:24px;color:${c.subtle};border-top:1px solid ${c.subtle}44;padding-top:22px`}
|
|
85
|
+
>
|
|
86
|
+
{address}
|
|
87
|
+
</div>
|
|
88
|
+
) : null}
|
|
58
89
|
</div>
|
|
59
90
|
)
|
|
60
91
|
}
|
package/src/og/fonts.mjs
CHANGED
|
@@ -26,9 +26,15 @@ export async function woffToSfnt(woff) {
|
|
|
26
26
|
const tables = []
|
|
27
27
|
for (let i = 0; i < numTables; i++) {
|
|
28
28
|
const at = 44 + i * 20
|
|
29
|
-
const offset = v.getUint32(at + 4),
|
|
29
|
+
const offset = v.getUint32(at + 4),
|
|
30
|
+
compLength = v.getUint32(at + 8),
|
|
31
|
+
origLength = v.getUint32(at + 12)
|
|
30
32
|
const raw = woff.subarray(offset, offset + compLength)
|
|
31
|
-
tables.push({
|
|
33
|
+
tables.push({
|
|
34
|
+
tag: v.getUint32(at),
|
|
35
|
+
checksum: v.getUint32(at + 16),
|
|
36
|
+
data: compLength < origLength ? await inflate(raw) : raw,
|
|
37
|
+
})
|
|
32
38
|
}
|
|
33
39
|
let size = 12 + 16 * numTables
|
|
34
40
|
for (const t of tables) size += (t.data.length + 3) & ~3
|
|
@@ -55,10 +61,12 @@ export async function woffToSfnt(woff) {
|
|
|
55
61
|
|
|
56
62
|
/** `{ name, data, weight?, style? }[]` in any format → the same with bytes every Takumi build reads. */
|
|
57
63
|
export async function normalizeFonts(fonts) {
|
|
58
|
-
return Promise.all(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
return Promise.all(
|
|
65
|
+
fonts.map(async (f) => {
|
|
66
|
+
const data = f.data instanceof Uint8Array ? f.data : new Uint8Array(f.data)
|
|
67
|
+
const format = fontFormat(data)
|
|
68
|
+
if (!format) throw new Error(`weft/og: font "${f.name}" isn't TTF, OTF, WOFF or WOFF2`)
|
|
69
|
+
return { ...f, data: format === 'woff' ? await woffToSfnt(data) : data }
|
|
70
|
+
}),
|
|
71
|
+
)
|
|
64
72
|
}
|
package/src/og/images.mjs
CHANGED
|
@@ -8,7 +8,10 @@ export function imageCache(maxBytes = 32 << 20) {
|
|
|
8
8
|
return {
|
|
9
9
|
get(src) {
|
|
10
10
|
const hit = map.get(src)
|
|
11
|
-
if (hit) {
|
|
11
|
+
if (hit) {
|
|
12
|
+
map.delete(src)
|
|
13
|
+
map.set(src, hit)
|
|
14
|
+
}
|
|
12
15
|
return hit
|
|
13
16
|
},
|
|
14
17
|
set(src, data) {
|
|
@@ -29,26 +32,44 @@ export function imageCache(maxBytes = 32 << 20) {
|
|
|
29
32
|
* renders data from `load`, so this is the SSRF guard); the default allows any https URL.
|
|
30
33
|
* Returns the images Takumi needs, and the srcs that failed.
|
|
31
34
|
*/
|
|
32
|
-
export async function loadImages(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (data
|
|
47
|
-
cache?.
|
|
48
|
-
images.push({ src, data })
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
35
|
+
export async function loadImages(
|
|
36
|
+
srcs,
|
|
37
|
+
{
|
|
38
|
+
cache,
|
|
39
|
+
timeoutMs = 1500,
|
|
40
|
+
maxBytes = 4 << 20,
|
|
41
|
+
allow = (u) => u.protocol === 'https:',
|
|
42
|
+
fetch = globalThis.fetch,
|
|
43
|
+
} = {},
|
|
44
|
+
) {
|
|
45
|
+
const images = [],
|
|
46
|
+
failed = []
|
|
47
|
+
await Promise.all(
|
|
48
|
+
[...new Set(srcs)].map(async (src) => {
|
|
49
|
+
if (src.startsWith('data:')) return // Takumi decodes data URIs itself
|
|
50
|
+
const hit = cache?.get(src)
|
|
51
|
+
if (hit) return void images.push({ src, data: hit })
|
|
52
|
+
try {
|
|
53
|
+
const url = new URL(src)
|
|
54
|
+
if (!allow(url)) throw new Error('not allowed')
|
|
55
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs), redirect: 'error' })
|
|
56
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
57
|
+
if (!res.headers.get('content-type')?.startsWith('image/')) throw new Error('not an image')
|
|
58
|
+
if (Number(res.headers.get('content-length') ?? 0) > maxBytes) throw new Error('too large')
|
|
59
|
+
const data = new Uint8Array(await res.arrayBuffer())
|
|
60
|
+
if (data.length > maxBytes) throw new Error('too large')
|
|
61
|
+
cache?.set(src, data)
|
|
62
|
+
images.push({ src, data })
|
|
63
|
+
} catch (e) {
|
|
64
|
+
failed.push({
|
|
65
|
+
src,
|
|
66
|
+
reason:
|
|
67
|
+
e?.name === 'TimeoutError'
|
|
68
|
+
? `timed out after ${timeoutMs} ms`
|
|
69
|
+
: String(e?.message ?? e),
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
}),
|
|
73
|
+
)
|
|
53
74
|
return { images, failed }
|
|
54
75
|
}
|