@weft-js/start 0.1.0 → 0.1.2
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 +36 -3
- package/crate/Cargo.lock +1347 -0
- package/crate/Cargo.toml +72 -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 +51 -10
- 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 +124 -490
- package/src/client.tsx +9 -2
- package/src/contract/describe.mjs +231 -0
- package/src/contract.mjs +146 -155
- 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 +97 -168
- 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 +113 -35
- 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 -174
- 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 -964
- 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/sidecar.mjs
CHANGED
|
@@ -16,175 +16,32 @@
|
|
|
16
16
|
// says which, so the next call loads it in place: an addon can't be unloaded, and needn't be. build: an addon and a
|
|
17
17
|
// `.wasm` next to the server, in `.output/server/rust/`, so whatever moves the server moves them too.
|
|
18
18
|
|
|
19
|
-
import {
|
|
20
|
-
import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, watch, writeFileSync } from 'node:fs'
|
|
19
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from 'node:fs'
|
|
21
20
|
import { createServer, request as httpRequest, Agent } from 'node:http'
|
|
22
21
|
import { randomBytes } from 'node:crypto'
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
22
|
+
import { join, resolve } from 'node:path'
|
|
23
|
+
import { makeRefresh } from './sidecar/refresh.mjs'
|
|
24
|
+
import { watchServices } from './sidecar/dev-watch.mjs'
|
|
25
|
+
import { buildable, isNative } from './sidecar/common.mjs'
|
|
26
|
+
import { publisher } from './sidecar/manifest.mjs'
|
|
27
|
+
import { devBuilder } from './sidecar/dev-build.mjs'
|
|
28
|
+
import { stop } from './sidecar/process.mjs'
|
|
26
29
|
|
|
27
30
|
/** The app names a sidecar: a server module imports `remote`. Nothing else here runs for an app that doesn't. */
|
|
28
31
|
export function usesRemote(root) {
|
|
29
32
|
const src = join(root, 'src')
|
|
30
33
|
if (!existsSync(src)) return false
|
|
31
|
-
return readdirSync(src, { recursive: true, withFileTypes: true }).some(
|
|
34
|
+
return readdirSync(src, { recursive: true, withFileTypes: true }).some(
|
|
35
|
+
(f) =>
|
|
36
|
+
f.isFile() &&
|
|
37
|
+
/\.[cm]?[jt]sx?$/.test(f.name) &&
|
|
38
|
+
/['"]@weft-js\/start\/rust['"]/.test(readFileSync(join(f.parentPath, f.name), 'utf8')),
|
|
39
|
+
)
|
|
32
40
|
}
|
|
33
41
|
|
|
34
|
-
|
|
35
|
-
export const urlVariable = service => `WEFT_RUST_${service.toUpperCase().replaceAll('-', '_')}_URL`
|
|
42
|
+
export { crateDir, urlVariable } from './sidecar/common.mjs'
|
|
36
43
|
|
|
37
|
-
|
|
38
|
-
/** The crate folder of a service. */
|
|
39
|
-
export const crateDir = (root, rust, service) => resolve(root, rust?.services?.[service]?.crate ?? `crates/${service}`)
|
|
40
|
-
|
|
41
|
-
/** The services in a contract that have a crate here. */
|
|
42
|
-
const buildable = (contract, rust) => Object.keys(contract.services ?? {}).filter(s => !external(rust, s))
|
|
43
|
-
const isNative = (contract, service) => contract.services?.[service]?.kind === 'native'
|
|
44
|
-
const WASM = 'wasm32-unknown-unknown'
|
|
45
|
-
|
|
46
|
-
// ---- cargo --------------------------------------------------------------------------------------------------------
|
|
47
|
-
|
|
48
|
-
function run(cmd, args, { cwd, env, onLine }) {
|
|
49
|
-
return new Promise(ok => {
|
|
50
|
-
let child
|
|
51
|
-
try {
|
|
52
|
-
child = spawn(cmd, args, { cwd, env: { ...process.env, ...env }, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
53
|
-
} catch (e) {
|
|
54
|
-
ok({ code: 127, stdout: '', stderr: e.message, missing: true })
|
|
55
|
-
return
|
|
56
|
-
}
|
|
57
|
-
let stdout = '', stderr = ''
|
|
58
|
-
child.stdout.on('data', d => { stdout += d })
|
|
59
|
-
child.stderr.on('data', d => {
|
|
60
|
-
stderr += d
|
|
61
|
-
onLine?.(`${d}`)
|
|
62
|
-
})
|
|
63
|
-
child.on('error', e => ok({ code: 127, stdout, stderr: e.message, missing: e.code === 'ENOENT' }))
|
|
64
|
-
child.on('close', code => ok({ code, stdout, stderr }))
|
|
65
|
-
})
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const pascal = name => name[0].toUpperCase() + name.slice(1)
|
|
69
|
-
const snake = name => name.replace(/[A-Z]/g, (c, i) => `${i ? '_' : ''}${c.toLowerCase()}`)
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* cargo's JSON messages as diagnostics. An error that names a contract function's types or method is R103, with the
|
|
73
|
-
* function's id, so it points at the TS edit that caused it (research 06 §7); any other is R109.
|
|
74
|
-
*/
|
|
75
|
-
export function readCargo(stdout, { contract, service, root, dir }) {
|
|
76
|
-
const fns = (contract?.functions ?? []).filter(f => f.impl?.service === service)
|
|
77
|
-
const names = fns.map(f => ({ id: f.id, re: new RegExp(`\\b(${pascal(f.name)}\\w*|${snake(f.name)})\\b`) }))
|
|
78
|
-
const diagnostics = []
|
|
79
|
-
let exe, lib, ok = false
|
|
80
|
-
for (const line of stdout.split('\n')) {
|
|
81
|
-
if (!line.startsWith('{')) continue
|
|
82
|
-
let m
|
|
83
|
-
try { m = JSON.parse(line) } catch { continue }
|
|
84
|
-
if (m.reason === 'compiler-artifact' && m.executable && m.target?.kind?.includes('bin')) exe = m.executable
|
|
85
|
-
if (m.reason === 'compiler-artifact' && m.target?.kind?.includes('cdylib')) lib = m.filenames?.find(f => /\.(dylib|so|dll|wasm)$/.test(f))
|
|
86
|
-
if (m.reason === 'build-finished') ok = m.success
|
|
87
|
-
if (m.reason !== 'compiler-message' || m.message?.level !== 'error' || !m.message.code) continue
|
|
88
|
-
const text = m.message.rendered ?? m.message.message
|
|
89
|
-
const hit = names.find(n => n.re.test(text))
|
|
90
|
-
const at = m.message.spans?.find(s => s.is_primary) ?? m.message.spans?.[0]
|
|
91
|
-
const file = at && relative(root, resolve(dir, at.file_name)).replaceAll('\\', '/')
|
|
92
|
-
diagnostics.push({
|
|
93
|
-
code: hit ? 'R103' : 'R109', severity: 'error', ...(hit && { function: hit.id }), file: file ?? '.weft/contract.json',
|
|
94
|
-
span: at ? [at.line_start, at.column_start, at.line_end, at.column_end] : [1, 1, 1, 1],
|
|
95
|
-
why: hit ? `\`${hit.id}\`: the Rust no longer matches the contract: ${m.message.message}` : `\`${service}\` doesn't compile: ${m.message.message}`,
|
|
96
|
-
rendered: text,
|
|
97
|
-
})
|
|
98
|
-
}
|
|
99
|
-
return { ok, exe, lib, diagnostics }
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** `verb`: `build` or `check`. Failure names what to fix: a build script's panic is already R103 with the fix. */
|
|
103
|
-
export async function cargo(verb, { root, dir, service, contract, release, target, onLine }) {
|
|
104
|
-
const args = [verb, '--message-format=json', ...(release ? ['--release'] : []), ...(target ? ['--target', target] : [])]
|
|
105
|
-
const env = { WEFT_CONTRACT: join(root, '.weft', 'contract.json'), ...(target === WASM && { CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS: '-C target-feature=+simd128' }) }
|
|
106
|
-
const r = await run('cargo', args, { cwd: dir, env, onLine })
|
|
107
|
-
if (r.missing) return { ok: false, diagnostics: [{ code: 'R109', severity: 'error', file: 'package.json', span: [1, 1, 1, 1], why: `\`${service}\` is Rust you opted into, so it needs \`cargo\`, and it isn't on PATH. Install it from rustup.rs, or set \`external: true\` for a sidecar built elsewhere. The rest of weft doesn't need Rust.` }] }
|
|
108
|
-
const read = readCargo(r.stdout, { contract, service, root, dir })
|
|
109
|
-
if (r.code === 0) return read
|
|
110
|
-
if (!read.diagnostics.length) {
|
|
111
|
-
const panic = /R103: ([^\n]+)/.exec(r.stderr)?.[1]
|
|
112
|
-
read.diagnostics.push({ code: panic ? 'R103' : 'R109', severity: 'error', file: relative(root, dir).replaceAll('\\', '/') + '/Cargo.toml', span: [1, 1, 1, 1], why: panic ?? `cargo ${verb} failed in \`${service}\`:\n${r.stderr.trim().split('\n').slice(-12).join('\n')}` })
|
|
113
|
-
}
|
|
114
|
-
return { ...read, ok: false }
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** R104 for a `.wasm` that won't build: a warning unless the build needs it. `error`: the preset has no other way to run. */
|
|
118
|
-
export async function checkWasm({ root, dir, service, contract, preset, rust, build }) {
|
|
119
|
-
if (rust?.services?.[service]?.wasm === false) return { diagnostics: [] }
|
|
120
|
-
const r = await cargo(build ? 'build' : 'check', { root, dir, service, contract, target: WASM, release: build })
|
|
121
|
-
if (r.ok) return { lib: r.lib, diagnostics: [] }
|
|
122
|
-
const missing = r.diagnostics.find(d => /can't find crate for `(std|core)`|target may not be installed/.test(`${d.why}${d.rendered ?? ''}`))
|
|
123
|
-
const why = missing ? `\`rustup target add ${WASM}\`: this toolchain can't build \`${service}\`'s WASM fallback` : `\`${service}\` has no WASM fallback: ${r.diagnostics[0]?.why ?? 'the build failed'}`
|
|
124
|
-
const needed = preset && !/^(node|bun|deno[-_]server)/.test(preset)
|
|
125
|
-
return { diagnostics: [{ code: 'R104', severity: needed ? 'error' : 'warning', file: relative(root, dir).replaceAll('\\', '/') + '/Cargo.toml', span: [1, 1, 1, 1], why: `${why}. An addon loads on this machine, and nothing else can run it. Fix it, or set \`rust.services.${service}.wasm: false\` to say so` }] }
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const stem = service => service.replaceAll('-', '_')
|
|
129
|
-
|
|
130
|
-
/** Copy, then rename: macOS kills a process that loads a signed binary overwritten in place. */
|
|
131
|
-
function place(from, to) {
|
|
132
|
-
copyFileSync(from, `${to}.tmp`)
|
|
133
|
-
renameSync(`${to}.tmp`, to)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// ---- children -----------------------------------------------------------------------------------------------------
|
|
137
|
-
|
|
138
|
-
/** Start a binary and wait until it listens and its health check names `hash`. Rejects with why, and leaves nothing running. */
|
|
139
|
-
function launch({ exe, service, hash, listen, secret, onExit, log }) {
|
|
140
|
-
return new Promise((ok, no) => {
|
|
141
|
-
const child = spawn(exe, [], { env: { ...process.env, WEFT_LISTEN: listen, WEFT_RUST_SECRET: secret }, stdio: ['ignore', 'pipe', 'inherit'] })
|
|
142
|
-
let out = '', ready = false
|
|
143
|
-
const failed = why => {
|
|
144
|
-
clearTimeout(timer)
|
|
145
|
-
child.kill('SIGKILL')
|
|
146
|
-
no(Object.assign(new Error(why), { code: 'R109' }))
|
|
147
|
-
}
|
|
148
|
-
const timer = setTimeout(() => failed(`\`${service}\` didn't listen within 30 s`), 30_000)
|
|
149
|
-
child.on('error', e => failed(`\`${service}\` didn't start: ${e.message}`))
|
|
150
|
-
child.on('exit', (code, signal) => {
|
|
151
|
-
if (!ready) return failed(`\`${service}\` exited before it listened (${signal ?? code})`)
|
|
152
|
-
onExit?.(code, signal)
|
|
153
|
-
})
|
|
154
|
-
child.stdout.on('data', d => void heard(d))
|
|
155
|
-
const heard = async d => {
|
|
156
|
-
if (ready) return void process.stdout.write(d)
|
|
157
|
-
out += d
|
|
158
|
-
const addr = /weft-listening (\S+)/.exec(out)?.[1]
|
|
159
|
-
if (!addr) return
|
|
160
|
-
ready = true
|
|
161
|
-
clearTimeout(timer)
|
|
162
|
-
const url = `http://${addr}`
|
|
163
|
-
try {
|
|
164
|
-
const health = await (await fetch(`${url}/_weft/health`)).json()
|
|
165
|
-
if (health.contract !== hash) {
|
|
166
|
-
ready = false
|
|
167
|
-
return failed(`R103: \`${service}\` was built from contract ${health.contract}, and this app's is ${hash}: rebuild it`)
|
|
168
|
-
}
|
|
169
|
-
log?.(`[weft] ${service}: listening on ${addr}`)
|
|
170
|
-
ok({ child, url, port: Number(addr.split(':').pop()), inflight: 0 })
|
|
171
|
-
} catch (e) {
|
|
172
|
-
ready = false
|
|
173
|
-
failed(`\`${service}\`'s health check failed: ${e.message}`)
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
})
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** SIGTERM, which the crate drains on; SIGKILL if that takes too long. */
|
|
180
|
-
function stop(p, wait = 5_000) {
|
|
181
|
-
if (!p || p.child.exitCode !== null) return
|
|
182
|
-
p.stopped = true
|
|
183
|
-
p.child.kill('SIGTERM')
|
|
184
|
-
const t = setTimeout(() => p.child.kill('SIGKILL'), wait)
|
|
185
|
-
t.unref()
|
|
186
|
-
p.child.once('exit', () => clearTimeout(t))
|
|
187
|
-
}
|
|
44
|
+
export { cargo, checkWasm, readCargo } from './sidecar/cargo.mjs'
|
|
188
45
|
|
|
189
46
|
// ---- dev ----------------------------------------------------------------------------------------------------------
|
|
190
47
|
|
|
@@ -196,18 +53,28 @@ function proxy(state) {
|
|
|
196
53
|
if (!to) {
|
|
197
54
|
if (!state.failure) return void res.destroy() // between crash and restart: `remote()` reports it, R107
|
|
198
55
|
res.writeHead(422, { 'content-type': 'application/json' })
|
|
199
|
-
return res.end(
|
|
56
|
+
return res.end(
|
|
57
|
+
JSON.stringify({ ok: false, error: { code: 'Drift', message: state.failure } }),
|
|
58
|
+
)
|
|
200
59
|
}
|
|
201
60
|
to.inflight++
|
|
202
|
-
res.once('close', () => {
|
|
203
|
-
|
|
204
|
-
res.writeHead(answer.statusCode, answer.headers)
|
|
205
|
-
answer.pipe(res)
|
|
61
|
+
res.once('close', () => {
|
|
62
|
+
if (--to.inflight === 0 && to.retired) stop(to)
|
|
206
63
|
})
|
|
64
|
+
const up = httpRequest(
|
|
65
|
+
`${to.url}${req.url}`,
|
|
66
|
+
{ method: req.method, headers: req.headers, agent },
|
|
67
|
+
(answer) => {
|
|
68
|
+
res.writeHead(answer.statusCode, answer.headers)
|
|
69
|
+
answer.pipe(res)
|
|
70
|
+
},
|
|
71
|
+
)
|
|
207
72
|
up.on('error', () => res.destroy())
|
|
208
73
|
req.pipe(up)
|
|
209
74
|
})
|
|
210
|
-
return new Promise(ok
|
|
75
|
+
return new Promise((ok) => {
|
|
76
|
+
server.listen(0, '127.0.0.1', () => ok({ server, agent, port: server.address().port }))
|
|
77
|
+
})
|
|
211
78
|
}
|
|
212
79
|
|
|
213
80
|
/**
|
|
@@ -218,151 +85,53 @@ function proxy(state) {
|
|
|
218
85
|
export async function devSidecars({ root, site, rust, log = console.log }) {
|
|
219
86
|
root = resolve(root)
|
|
220
87
|
const secret = (process.env.WEFT_RUST_SECRET ||= randomBytes(32).toString('hex'))
|
|
221
|
-
const say = d =>
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const { contract, diagnostics } = await writeContract({ root, site, rust })
|
|
229
|
-
diagnostics.filter(d => d.code.startsWith('R1')).forEach(say)
|
|
230
|
-
return { contract, blocked: diagnostics.find(d => /^R10[12]$/.test(d.code) && d.severity === 'error') }
|
|
231
|
-
} finally {
|
|
232
|
-
if (held === undefined) delete process.env.WEFT_BUILD
|
|
233
|
-
else process.env.WEFT_BUILD = held
|
|
234
|
-
}
|
|
235
|
-
}
|
|
88
|
+
const say = (d) =>
|
|
89
|
+
log(`[weft] ${d.code} ${d.severity}: ${d.why}${d.rendered ? `\n${d.rendered}` : ''}`)
|
|
90
|
+
let vite,
|
|
91
|
+
closed = false
|
|
92
|
+
const overlay = (message) =>
|
|
93
|
+
vite?.ws.send({ type: 'error', err: { message, stack: '', plugin: 'weft:sidecar' } })
|
|
94
|
+
const refresh = makeRefresh(root, site, rust, say)
|
|
236
95
|
|
|
237
96
|
const first = await refresh()
|
|
238
97
|
let contract = first.contract
|
|
239
98
|
const states = new Map()
|
|
240
99
|
const nativeDir = join(root, '.weft', 'native')
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const manifest = Object.fromEntries([...states].filter(([k]) => isNative(contract, k)).map(([k, s]) => [k, s.loaded ?? { hash: '', failure: s.failure ?? `\`${k}\` hasn't built` }]))
|
|
244
|
-
writeFileSync(join(nativeDir, 'manifest.tmp'), JSON.stringify(manifest))
|
|
245
|
-
renameSync(join(nativeDir, 'manifest.tmp'), join(nativeDir, 'manifest.json'))
|
|
246
|
-
}
|
|
247
|
-
if (buildable(contract, rust).some(k => isNative(contract, k))) {
|
|
100
|
+
const publish = publisher(states, nativeDir, () => contract)
|
|
101
|
+
if (buildable(contract, rust).some((k) => isNative(contract, k))) {
|
|
248
102
|
rmSync(nativeDir, { recursive: true, force: true })
|
|
249
103
|
mkdirSync(nativeDir, { recursive: true })
|
|
250
104
|
process.env.WEFT_NATIVE_DIR = nativeDir
|
|
251
105
|
}
|
|
252
106
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
async function once(service, s) {
|
|
269
|
-
const hash = contract.services?.[service]?.hash
|
|
270
|
-
if (!hash) return
|
|
271
|
-
const dir = crateDir(root, rust, service)
|
|
272
|
-
const t = performance.now()
|
|
273
|
-
const built = await cargo('build', { root, dir, service, contract })
|
|
274
|
-
if (!built.ok || !(built.exe || built.lib)) {
|
|
275
|
-
built.diagnostics.forEach(say)
|
|
276
|
-
s.failure = built.diagnostics.map(d => d.why).join('\n') || `\`${service}\` didn't build`
|
|
277
|
-
overlay(s.failure)
|
|
278
|
-
if (isNative(contract, service) && !s.loaded) publish()
|
|
279
|
-
return
|
|
280
|
-
}
|
|
281
|
-
log(`[weft] ${service}: built in ${Math.round(performance.now() - t)} ms`)
|
|
282
|
-
if (isNative(contract, service)) {
|
|
283
|
-
s.generation = (s.generation ?? 0) + 1
|
|
284
|
-
s.loaded = { hash, addon: `${stem(service)}-${s.generation}.node` }
|
|
285
|
-
s.failure = undefined
|
|
286
|
-
place(built.lib, join(nativeDir, s.loaded.addon))
|
|
287
|
-
return publish()
|
|
288
|
-
}
|
|
289
|
-
let next
|
|
290
|
-
try {
|
|
291
|
-
next = await launch({ exe: built.exe, service, hash, listen: '127.0.0.1:0', secret, log, onExit: (code, signal) => exited(service, s, next, code, signal) })
|
|
292
|
-
} catch (e) {
|
|
293
|
-
s.failure = e.message
|
|
294
|
-
log(`[weft] ${e.message}`)
|
|
295
|
-
return overlay(e.message)
|
|
296
|
-
}
|
|
297
|
-
// The swap: new calls go to `next` from here; the old child finishes what it has, then goes.
|
|
298
|
-
const old = s.current
|
|
299
|
-
s.current = next
|
|
300
|
-
s.failure = undefined
|
|
301
|
-
s.exe = built.exe
|
|
302
|
-
s.crashes = 0
|
|
303
|
-
if (old) {
|
|
304
|
-
old.retired = true
|
|
305
|
-
if (old.inflight === 0) stop(old)
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function exited(service, s, child, code, signal) {
|
|
310
|
-
if (closed || !child || child.stopped || s.current !== child) return
|
|
311
|
-
s.current = undefined
|
|
312
|
-
const wait = Math.min(100 * 2 ** (s.crashes = (s.crashes ?? 0) + 1), 5_000)
|
|
313
|
-
log(`[weft] ${service} exited (${signal ?? code}): restarting in ${wait} ms`)
|
|
314
|
-
setTimeout(() => {
|
|
315
|
-
if (closed || s.current || s.building) return
|
|
316
|
-
void rebuild(service) // cargo finds nothing to do, and the binary starts again
|
|
317
|
-
}, wait).unref()
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const watchers = []
|
|
321
|
-
const debounce = (fn, ms = 200) => {
|
|
322
|
-
let t
|
|
323
|
-
return () => { clearTimeout(t); t = setTimeout(() => fn(), ms) }
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
for (const service of buildable(contract, rust)) {
|
|
327
|
-
const dir = crateDir(root, rust, service)
|
|
328
|
-
const s = { current: undefined, failure: existsSync(dir) ? undefined : `\`crates/${service}\` doesn't exist`, building: false }
|
|
329
|
-
states.set(service, s)
|
|
330
|
-
if (!isNative(contract, service)) {
|
|
331
|
-
const p = await proxy(s)
|
|
332
|
-
s.proxy = p
|
|
333
|
-
process.env[urlVariable(service)] = `http://127.0.0.1:${p.port}`
|
|
334
|
-
}
|
|
335
|
-
if (!existsSync(dir)) {
|
|
336
|
-
log(`[weft] R109 error: ${s.failure}. Point \`rust.services.${service}.crate\` at it, or set \`external: true\``)
|
|
337
|
-
if (isNative(contract, service)) publish()
|
|
338
|
-
continue
|
|
339
|
-
}
|
|
340
|
-
if (!first.blocked) await rebuild(service)
|
|
341
|
-
const changed = debounce(() => rebuild(service))
|
|
342
|
-
watchers.push(watch(dir, { recursive: true }, (_, file) => {
|
|
343
|
-
if (file && !/^target([\\/]|$)/.test(file) && !file.endsWith('.lock')) changed()
|
|
344
|
-
}))
|
|
345
|
-
}
|
|
107
|
+
const rebuild = devBuilder({
|
|
108
|
+
root,
|
|
109
|
+
rust,
|
|
110
|
+
secret,
|
|
111
|
+
log,
|
|
112
|
+
say,
|
|
113
|
+
overlay,
|
|
114
|
+
states,
|
|
115
|
+
nativeDir,
|
|
116
|
+
publish,
|
|
117
|
+
contract: () => contract,
|
|
118
|
+
closed: () => closed,
|
|
119
|
+
})
|
|
346
120
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
log(`[weft] the contract couldn't be regenerated: ${e.message.split('\n')[0]}`)
|
|
361
|
-
}
|
|
121
|
+
const watchers = await watchServices({
|
|
122
|
+
root,
|
|
123
|
+
rust,
|
|
124
|
+
contract: () => contract,
|
|
125
|
+
setContract: (next) => (contract = next),
|
|
126
|
+
first,
|
|
127
|
+
states,
|
|
128
|
+
proxy,
|
|
129
|
+
publish,
|
|
130
|
+
rebuild,
|
|
131
|
+
refresh,
|
|
132
|
+
overlay,
|
|
133
|
+
log,
|
|
362
134
|
})
|
|
363
|
-
if (existsSync(src)) watchers.push(watch(src, { recursive: true }, (_, file) => {
|
|
364
|
-
if (file && isServerModule(`src/${file.replaceAll('\\', '/')}`)) contractChanged()
|
|
365
|
-
}))
|
|
366
135
|
|
|
367
136
|
const killAll = () => {
|
|
368
137
|
closed = true
|
|
@@ -371,7 +140,9 @@ export async function devSidecars({ root, site, rust, log = console.log }) {
|
|
|
371
140
|
process.once('exit', killAll)
|
|
372
141
|
return {
|
|
373
142
|
ready: Object.fromEntries([...states].map(([k, s]) => [k, !!(s.current || s.loaded)])),
|
|
374
|
-
attach(server) {
|
|
143
|
+
attach(server) {
|
|
144
|
+
vite = server
|
|
145
|
+
},
|
|
375
146
|
async close() {
|
|
376
147
|
process.removeListener('exit', killAll)
|
|
377
148
|
killAll()
|
|
@@ -379,125 +150,13 @@ export async function devSidecars({ root, site, rust, log = console.log }) {
|
|
|
379
150
|
for (const s of states.values()) {
|
|
380
151
|
s.proxy?.agent.destroy()
|
|
381
152
|
s.proxy?.server.closeAllConnections()
|
|
382
|
-
await new Promise(ok => {
|
|
153
|
+
await new Promise((ok) => {
|
|
154
|
+
if (s.proxy) s.proxy.server.close(ok)
|
|
155
|
+
else ok()
|
|
156
|
+
})
|
|
383
157
|
}
|
|
384
158
|
},
|
|
385
159
|
}
|
|
386
160
|
}
|
|
387
161
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const BUILT = ['.weft', 'sidecars'] // a build empties `.output`, and these have to run during it
|
|
391
|
-
const SHIPPED = ['.output', 'rust']
|
|
392
|
-
const SHIPPED_NATIVE = ['.output', 'server', 'rust'] // with the server, so a host that moves it moves these
|
|
393
|
-
|
|
394
|
-
/** Release binaries into `.weft/sidecars/`, and what `weft start` needs to run them. */
|
|
395
|
-
export async function buildSidecars({ root, contract, rust, preset, log = () => {} }) {
|
|
396
|
-
root = resolve(root)
|
|
397
|
-
const out = join(root, ...BUILT)
|
|
398
|
-
const services = buildable(contract, rust)
|
|
399
|
-
rmSync(out, { recursive: true, force: true })
|
|
400
|
-
if (!services.length) return { services: [], diagnostics: [] }
|
|
401
|
-
mkdirSync(out, { recursive: true })
|
|
402
|
-
const manifest = {}
|
|
403
|
-
const diagnostics = []
|
|
404
|
-
for (const service of services) {
|
|
405
|
-
const dir = crateDir(root, rust, service)
|
|
406
|
-
if (!existsSync(dir)) {
|
|
407
|
-
diagnostics.push({ code: 'R109', severity: 'error', file: 'weft.config.ts', span: [1, 1, 1, 1], why: `\`rust://${service}\` has no crate at \`${relative(root, dir)}\`. Create it, point \`rust.services.${service}.crate\` at it, or set \`external: true\`.` })
|
|
408
|
-
continue
|
|
409
|
-
}
|
|
410
|
-
log(`building ${service}…`)
|
|
411
|
-
const built = await cargo('build', { root, dir, service, contract, release: true })
|
|
412
|
-
if (!built.ok || !(built.exe || built.lib)) {
|
|
413
|
-
diagnostics.push(...built.diagnostics)
|
|
414
|
-
continue
|
|
415
|
-
}
|
|
416
|
-
if (isNative(contract, service)) {
|
|
417
|
-
const addon = `${stem(service)}.${process.platform}-${process.arch}.node`
|
|
418
|
-
copyFileSync(built.lib, join(out, addon))
|
|
419
|
-
manifest[service] = { kind: 'native', addon, hash: contract.services[service].hash, bytes: statSync(built.lib).size }
|
|
420
|
-
const wasm = await checkWasm({ root, dir, service, contract, preset, rust, build: true })
|
|
421
|
-
diagnostics.push(...wasm.diagnostics)
|
|
422
|
-
if (wasm.lib) {
|
|
423
|
-
copyFileSync(wasm.lib, join(out, `${stem(service)}.wasm`))
|
|
424
|
-
Object.assign(manifest[service], { wasm: `${stem(service)}.wasm`, wasmBytes: statSync(wasm.lib).size })
|
|
425
|
-
}
|
|
426
|
-
continue
|
|
427
|
-
}
|
|
428
|
-
const bin = basename(built.exe)
|
|
429
|
-
copyFileSync(built.exe, join(out, bin))
|
|
430
|
-
manifest[service] = { bin, hash: contract.services[service].hash, bytes: statSync(built.exe).size }
|
|
431
|
-
}
|
|
432
|
-
writeFileSync(join(out, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n')
|
|
433
|
-
return { services: Object.keys(manifest), diagnostics }
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/** Into the output, once the server is built. */
|
|
437
|
-
export function shipSidecars(root) {
|
|
438
|
-
const from = join(resolve(root), ...BUILT)
|
|
439
|
-
if (!existsSync(from)) return
|
|
440
|
-
const manifest = JSON.parse(readFileSync(join(from, 'manifest.json'), 'utf8'))
|
|
441
|
-
const natives = Object.entries(manifest).filter(([, m]) => m.kind === 'native')
|
|
442
|
-
const sidecars = Object.entries(manifest).filter(([, m]) => m.kind !== 'native')
|
|
443
|
-
const copy = (list, to, names) => {
|
|
444
|
-
if (!list.length) return
|
|
445
|
-
mkdirSync(to, { recursive: true })
|
|
446
|
-
for (const [, m] of list) for (const f of names(m)) cpSync(join(from, f), join(to, f))
|
|
447
|
-
writeFileSync(join(to, 'manifest.json'), JSON.stringify(Object.fromEntries(list), null, 2) + '\n')
|
|
448
|
-
}
|
|
449
|
-
copy(sidecars, join(resolve(root), ...SHIPPED), m => [m.bin])
|
|
450
|
-
copy(natives, join(resolve(root), ...SHIPPED_NATIVE), m => [m.addon, m.wasm].filter(Boolean))
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// ---- start --------------------------------------------------------------------------------------------------------
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* Run `.output/rust/`'s binaries for `weft start` (or `built`'s, while `weft build` prerenders). A service with `WEFT_RUST_<NAME>_URL` already set is someone else's.
|
|
457
|
-
* Each keeps its port across restarts, so the server's address stays good.
|
|
458
|
-
* @returns {Promise<{ env: Record<string, string>, close(): void }>} `env` for the server process
|
|
459
|
-
*/
|
|
460
|
-
export async function startSidecars({ root, built = false, log = console.log }) {
|
|
461
|
-
const dir = join(resolve(root), ...(built ? BUILT : SHIPPED))
|
|
462
|
-
const file = join(dir, 'manifest.json')
|
|
463
|
-
if (!existsSync(file)) return { env: {}, close() {} }
|
|
464
|
-
const manifest = JSON.parse(readFileSync(file, 'utf8'))
|
|
465
|
-
const names = Object.keys(manifest).filter(s => manifest[s].kind !== 'native' && !process.env[urlVariable(s)])
|
|
466
|
-
// While a build prerenders, the libraries aren't next to a server yet.
|
|
467
|
-
const paths = built && Object.values(manifest).some(m => m.kind === 'native') ? { WEFT_NATIVE_DIR: dir } : {}
|
|
468
|
-
if (!names.length) return { env: paths, close() {} }
|
|
469
|
-
const secret = process.env.WEFT_RUST_SECRET || randomBytes(32).toString('hex')
|
|
470
|
-
const env = { ...paths, WEFT_RUST_SECRET: secret }
|
|
471
|
-
let closed = false
|
|
472
|
-
const live = new Map()
|
|
473
|
-
|
|
474
|
-
const supervise = service => {
|
|
475
|
-
const { bin, hash } = manifest[service]
|
|
476
|
-
let listen = '127.0.0.1:0', tries = 0, up
|
|
477
|
-
const begin = async () => {
|
|
478
|
-
up = Date.now()
|
|
479
|
-
const p = await launch({ exe: join(dir, bin), service, hash, listen, secret, log, onExit: (code, signal) => again(`exited (${signal ?? code})`) })
|
|
480
|
-
listen = `127.0.0.1:${p.port}` // restarts rebind what the first start got
|
|
481
|
-
live.set(service, p)
|
|
482
|
-
return p
|
|
483
|
-
}
|
|
484
|
-
const again = why => {
|
|
485
|
-
if (closed) return
|
|
486
|
-
// One that ran for a while starts over; one that dies at once backs off.
|
|
487
|
-
tries = Date.now() - up > 30_000 ? 1 : tries + 1
|
|
488
|
-
const wait = Math.min(100 * 2 ** tries, 5_000)
|
|
489
|
-
log(`[weft] ${service} ${why}: restarting in ${wait} ms`)
|
|
490
|
-
setTimeout(() => { if (!closed) begin().catch(e => again(e.message.split('\n')[0])) }, wait)
|
|
491
|
-
}
|
|
492
|
-
return begin()
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
for (const service of names) env[urlVariable(service)] = (await supervise(service)).url
|
|
496
|
-
|
|
497
|
-
const killAll = () => {
|
|
498
|
-
closed = true
|
|
499
|
-
for (const p of live.values()) stop(p, 1_000)
|
|
500
|
-
}
|
|
501
|
-
process.once('exit', killAll)
|
|
502
|
-
return { env, close: killAll }
|
|
503
|
-
}
|
|
162
|
+
export { buildSidecars, shipSidecars, startSidecars } from './sidecar/build.mjs'
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { StartError, type Context, type Fn, type StandardSchemaV1 } from '../data'
|
|
2
|
+
import type { Bucket } from './types'
|
|
3
|
+
|
|
4
|
+
export function take(b: Bucket, perSecond: number, burst: number) {
|
|
5
|
+
const now = Date.now()
|
|
6
|
+
b.tokens = Math.min(burst, b.tokens + ((now - b.at) * perSecond) / 1000)
|
|
7
|
+
b.at = now
|
|
8
|
+
if (b.tokens < 1) return false
|
|
9
|
+
b.tokens--
|
|
10
|
+
return true
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function check(schema: StandardSchemaV1 | undefined, value: unknown, what: string) {
|
|
14
|
+
if (!schema) return value
|
|
15
|
+
const r = await schema['~standard'].validate(value)
|
|
16
|
+
if (!r.issues) return r.value
|
|
17
|
+
throw new StartError(
|
|
18
|
+
'Q002',
|
|
19
|
+
`${what} was refused: ${r.issues.map((i) => i.message).join('; ')}`,
|
|
20
|
+
'send what the schema accepts',
|
|
21
|
+
{
|
|
22
|
+
status: 400,
|
|
23
|
+
issues: r.issues.map((i) => ({
|
|
24
|
+
message: i.message,
|
|
25
|
+
path: i.path?.map((p) => (typeof p === 'object' ? p.key : p)),
|
|
26
|
+
})),
|
|
27
|
+
},
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type Authorize = ((args: { ctx: Context; params: unknown }) => unknown) &
|
|
32
|
+
Record<'read' | 'write', (args: { ctx: Context; key: unknown }) => unknown>
|
|
33
|
+
export const allowed = async (
|
|
34
|
+
fn: Fn,
|
|
35
|
+
ctx: Context,
|
|
36
|
+
params: unknown,
|
|
37
|
+
write?: boolean,
|
|
38
|
+
): Promise<boolean> => {
|
|
39
|
+
const a = fn.def.authorize as Authorize
|
|
40
|
+
return !!(await (fn.kind === 'shared'
|
|
41
|
+
? a[write ? 'write' : 'read']({ ctx, key: params })
|
|
42
|
+
: a({ ctx, params })))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const denied = (fn: Fn) =>
|
|
46
|
+
new StartError(
|
|
47
|
+
'R011',
|
|
48
|
+
`\`${fn.$id}\`: \`authorize\` refused this client`,
|
|
49
|
+
'sign in, or check `authorize`',
|
|
50
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Hub, Ev } from '../hub'
|
|
2
|
+
import type { Sock, Join } from './types'
|
|
3
|
+
import { ASK } from './rooms'
|
|
4
|
+
|
|
5
|
+
/** Fan-out of a room's messages to one join. */
|
|
6
|
+
export function deliver(c: Sock, j: Join, h: Hub, ev: Ev, send: (c: Sock, out: unknown[]) => void) {
|
|
7
|
+
const m = (ev.p ??= JSON.parse(ev.raw)) as {
|
|
8
|
+
f?: string
|
|
9
|
+
x?: 1
|
|
10
|
+
t?: string
|
|
11
|
+
n: string
|
|
12
|
+
m: unknown
|
|
13
|
+
}
|
|
14
|
+
if (m.n === ASK || (m.x && m.f === c.k) || (m.t && m.t !== c.k)) return
|
|
15
|
+
send(c, ['m', j.n, h.cursorOf(ev.seq), m.n, m.m])
|
|
16
|
+
}
|