@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/live.ts
CHANGED
|
@@ -5,92 +5,47 @@
|
|
|
5
5
|
//
|
|
6
6
|
// A poke is a local `cache.invalidate`: readers refetch through `query`, and the keyed list writes what changed.
|
|
7
7
|
|
|
8
|
-
import { DEV, batch, createRoot,
|
|
9
|
-
import type { AuthScope,
|
|
10
|
-
import type {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
import { DEV, batch, createRoot, state, untrack } from '@weft-js/core'
|
|
9
|
+
import type { AuthScope, Fn, LiveMode, LiveOptions, LiveStatus, StandardSchemaV1 } from './data'
|
|
10
|
+
import type { SubEntry } from './hub'
|
|
11
|
+
import { noSockets } from './link'
|
|
12
|
+
import { transports } from './live/transport'
|
|
13
|
+
import { backoff, CAP } from './live/backoff'
|
|
14
|
+
|
|
15
|
+
import { cache, fns, FnError, keyOf, net, rt, runtime, track } from './live/api'
|
|
16
|
+
|
|
17
|
+
import type { In, StreamDef, StreamFn, StreamOptions } from './live/types'
|
|
18
|
+
import type { Sub } from './live/sub-types'
|
|
19
|
+
import { read } from './live/read'
|
|
20
|
+
import type { Link } from './live/transport'
|
|
21
|
+
import { ack, update, type Row } from './live/frames'
|
|
22
|
+
export { backoff } from './live/backoff'
|
|
23
|
+
export { use } from './live/api'
|
|
24
|
+
export type { StreamStatus, StreamDef, StreamOptions, Stream, StreamFn } from './live/types'
|
|
25
|
+
export type { Link } from './live/transport'
|
|
17
26
|
|
|
18
27
|
// ---- definitions ----------------------------------------------------------------------------------------------------
|
|
19
28
|
|
|
20
|
-
type In<S> = S extends StandardSchemaV1<infer I, unknown> ? I : void
|
|
21
|
-
type Out<S> = S extends StandardSchemaV1<unknown, infer O> ? O : undefined
|
|
22
|
-
|
|
23
|
-
/** Where a stream stands; `done` is the end of a generator. */
|
|
24
|
-
export type StreamStatus = LiveStatus | 'done'
|
|
25
|
-
|
|
26
|
-
/** `stream()`: the input, the message schema and the generator. */
|
|
27
|
-
export interface StreamDef<S, M, A = undefined> {
|
|
28
|
-
input?: S
|
|
29
|
-
/** Checked in dev and tests (C002), and the contract's `message`. */
|
|
30
|
-
message?: StandardSchemaV1<unknown, M>
|
|
31
|
-
auth?: A
|
|
32
|
-
/** `lastEventId`: the last `tracked` id the client saw, on a resume. Stop when `signal` aborts. */
|
|
33
|
-
run: (args: { input: Out<S>; ctx: Context; signal: AbortSignal; lastEventId?: string }) => AsyncIterable<M | Tracked<M>>
|
|
34
|
-
$id?: string
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** `.live()` on a stream: what the value starts as, and how messages fold into it. */
|
|
38
|
-
export interface StreamOptions<M, T> extends LiveOptions {
|
|
39
|
-
/** Keep every message: `fold(acc, message)`. Its state lives in the cache, so a remount finds it. */
|
|
40
|
-
fold?: (acc: T, message: M) => T
|
|
41
|
-
initial?: T
|
|
42
|
-
/** Messages the server holds for a slow client. A fold defaults to 256; without one, the latest wins. */
|
|
43
|
-
buffer?: number
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** A stream as a page reads it. */
|
|
47
|
-
export interface Stream<T> {
|
|
48
|
-
/** The latest message, or the fold. */
|
|
49
|
-
readonly value: T
|
|
50
|
-
readonly status: Derived<StreamStatus>
|
|
51
|
-
readonly error: unknown
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** What `stream()` returns. */
|
|
55
|
-
export interface StreamFn<I, M> {
|
|
56
|
-
(input: Arg<I>): Stream<M | undefined>
|
|
57
|
-
<T>(input: Arg<I>, options: StreamOptions<M, T> & { initial: T }): Stream<T>
|
|
58
|
-
(input: Arg<I>, options: StreamOptions<M, M>): Stream<M | undefined>
|
|
59
|
-
readonly kind: 'stream'
|
|
60
|
-
readonly $id: string
|
|
61
|
-
}
|
|
62
|
-
type Arg<I> = I | (() => I | Skip)
|
|
63
|
-
|
|
64
29
|
const FN = Symbol.for('weft.fn')
|
|
65
30
|
let auto = 0
|
|
66
31
|
|
|
67
|
-
export function stream<
|
|
32
|
+
export function stream<
|
|
33
|
+
M,
|
|
34
|
+
S extends StandardSchemaV1 | undefined = undefined,
|
|
35
|
+
const A extends AuthScope | undefined = undefined,
|
|
36
|
+
>(def: StreamDef<S, M, A>): StreamFn<In<S>, M> {
|
|
68
37
|
const id = def.$id ?? `s${++auto}`
|
|
69
38
|
const fn: Fn = { kind: 'stream', $id: id, def: def as unknown as Fn['def'] }
|
|
70
39
|
fns.set(id, fn)
|
|
71
|
-
return Object.assign(
|
|
40
|
+
return Object.assign(
|
|
41
|
+
(input: unknown, options?: StreamOptions<never, unknown>) =>
|
|
42
|
+
read(fn, input, options, acquire, held),
|
|
43
|
+
{ kind: 'stream', $id: id, [FN]: fn },
|
|
44
|
+
) as unknown as StreamFn<In<S>, M>
|
|
72
45
|
}
|
|
73
46
|
|
|
74
47
|
// ---- subscriptions --------------------------------------------------------------------------------------------------
|
|
75
48
|
|
|
76
|
-
interface View { fold?: ((acc: never, m: never) => unknown) | undefined; initial: unknown; box: State<unknown> }
|
|
77
|
-
interface Sub {
|
|
78
|
-
n: number
|
|
79
|
-
key: string
|
|
80
|
-
fn: Fn
|
|
81
|
-
input: unknown
|
|
82
|
-
refs: number
|
|
83
|
-
timer?: ReturnType<typeof setTimeout> | undefined
|
|
84
|
-
/** Stream: the last id, sent back on a resume. Query: its tags, from the ack. */
|
|
85
|
-
cursor?: string | undefined
|
|
86
|
-
tags: string[]
|
|
87
|
-
buffer?: number
|
|
88
|
-
acked: boolean
|
|
89
|
-
views: Map<string, View>
|
|
90
|
-
error: State<unknown>
|
|
91
|
-
done: State<boolean>
|
|
92
|
-
}
|
|
93
|
-
|
|
94
49
|
const subs = new Map<string, Sub>()
|
|
95
50
|
const byN = new Map<number, Sub>()
|
|
96
51
|
let nth = 0
|
|
@@ -106,7 +61,21 @@ function acquire(fn: Fn, input: unknown): Sub {
|
|
|
106
61
|
const key = keyOf(fn.$id, input)
|
|
107
62
|
let s = subs.get(key)
|
|
108
63
|
if (!s) {
|
|
109
|
-
s = createRoot(
|
|
64
|
+
s = createRoot(
|
|
65
|
+
() => ({
|
|
66
|
+
n: ++nth,
|
|
67
|
+
key,
|
|
68
|
+
fn,
|
|
69
|
+
input,
|
|
70
|
+
refs: 0,
|
|
71
|
+
tags: [],
|
|
72
|
+
acked: false,
|
|
73
|
+
views: new Map(),
|
|
74
|
+
error: state<unknown>(undefined),
|
|
75
|
+
done: state(false),
|
|
76
|
+
}),
|
|
77
|
+
null,
|
|
78
|
+
)
|
|
110
79
|
// Not at the top level: a side effect there would keep this chunk in apps that never read `.live`.
|
|
111
80
|
if (DEV && net() && !net()!.resets.includes(reset)) net()!.resets.push(reset)
|
|
112
81
|
subs.set(key, s)
|
|
@@ -149,102 +118,9 @@ export function hold(fn: Fn, input: unknown, options?: LiveOptions): () => void
|
|
|
149
118
|
return held(acquire(fn, input), options?.keep)
|
|
150
119
|
}
|
|
151
120
|
|
|
152
|
-
function read(fn: Fn, input: unknown, o: StreamOptions<never, unknown> = {}): Stream<unknown> {
|
|
153
|
-
if (!getOwner())
|
|
154
|
-
throw new StartError('W051', DEV ? `\`${fn.$id}()\` (a stream) was called outside a component or scope: nothing would close it` : fn.$id, DEV ? 'call it in a component, a `defineState` setup or `createRoot(dispose => …)`' : undefined)
|
|
155
|
-
const scope = currentScope()
|
|
156
|
-
if (scope) {
|
|
157
|
-
scope.live = true // the server renders `initial`, and the client hydrates to the same
|
|
158
|
-
return { value: o.initial, status: rt.status, error: undefined }
|
|
159
|
-
}
|
|
160
|
-
const view = state<{ s: Sub; v: View } | undefined>(undefined)
|
|
161
|
-
const fold = o.fold ? String(o.fold) : ''
|
|
162
|
-
effect(() => {
|
|
163
|
-
const arg = typeof input === 'function' ? (input as () => unknown)() : input
|
|
164
|
-
if (arg === skip) return undefined
|
|
165
|
-
return untrack(() => {
|
|
166
|
-
const s = acquire(fn, arg)
|
|
167
|
-
if (o.fold) s.buffer ??= o.buffer ?? 256
|
|
168
|
-
else if (o.buffer) s.buffer ??= o.buffer
|
|
169
|
-
let v = s.views.get(fold)
|
|
170
|
-
if (!v) s.views.set(fold, (v = createRoot(() => ({ fold: o.fold, initial: o.initial, box: state<unknown>(o.initial) }), null)))
|
|
171
|
-
v.fold = o.fold
|
|
172
|
-
onCleanup(held(s, o.keep))
|
|
173
|
-
return { s, v }
|
|
174
|
-
})
|
|
175
|
-
}, now => { view.value = now })
|
|
176
|
-
const status = derived(() => (view.value?.s.done.value ? 'done' : rt.status.value))
|
|
177
|
-
return {
|
|
178
|
-
get value() { const now = view.value; return now ? now.v.box.value : o.initial },
|
|
179
|
-
status,
|
|
180
|
-
get error() { return view.value?.s.error.value },
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
121
|
// ---- the connection ---------------------------------------------------------------------------------------------------
|
|
185
122
|
|
|
186
|
-
|
|
187
|
-
export interface Link {
|
|
188
|
-
/** Resolves when the connection ends. */
|
|
189
|
-
open(body: Body, frame: (event: string, data: unknown) => void, signal: AbortSignal, alive: () => void): Promise<void>
|
|
190
|
-
change(body: Body): Promise<void>
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const post = (body: Body, signal?: AbortSignal) =>
|
|
194
|
-
fetch('/_weft/rt', { method: 'POST', headers: { 'content-type': 'application/json', accept: 'text/event-stream', 'x-weft': '1' }, body: JSON.stringify(body), ...(signal && { signal }) })
|
|
195
|
-
|
|
196
|
-
const http: Link = {
|
|
197
|
-
async open(body, heard, signal, alive) {
|
|
198
|
-
const res = await post(body, signal)
|
|
199
|
-
if (!res.ok) {
|
|
200
|
-
const { e } = await res.json().catch(() => ({ e: {} }))
|
|
201
|
-
throw new FnError(e, res.status)
|
|
202
|
-
}
|
|
203
|
-
const reader = res.body!.getReader()
|
|
204
|
-
const decoder = new TextDecoder()
|
|
205
|
-
let buf = ''
|
|
206
|
-
for (;;) {
|
|
207
|
-
const { done, value } = await reader.read()
|
|
208
|
-
if (done) return
|
|
209
|
-
alive()
|
|
210
|
-
buf += decoder.decode(value, { stream: true })
|
|
211
|
-
for (let i; (i = buf.indexOf('\n\n')) >= 0; buf = buf.slice(i + 2)) {
|
|
212
|
-
if (buf[0] === ':') continue
|
|
213
|
-
const line = buf.indexOf('\n')
|
|
214
|
-
heard(buf.slice(7, line), JSON.parse(buf.slice(line + 7, i)))
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
},
|
|
218
|
-
async change(body) {
|
|
219
|
-
const res = await post(body)
|
|
220
|
-
if (!res.ok) throw new FnError({}, res.status)
|
|
221
|
-
},
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/** A line on the browser's one socket (link.ts): the same events, and `/_weft/rt`'s SSE is for a host with no sockets. */
|
|
225
|
-
let line: Line | undefined
|
|
226
|
-
const socket: Link = {
|
|
227
|
-
open(body, heard, signal, alive) {
|
|
228
|
-
return new Promise<void>((done, fail) => {
|
|
229
|
-
const l = (line = openLine('r', JSON.stringify(body), text => {
|
|
230
|
-
alive()
|
|
231
|
-
const at = text.indexOf(':'), event = text.slice(0, at)
|
|
232
|
-
if (event !== 'ping') heard(event, JSON.parse(text.slice(at + 1)))
|
|
233
|
-
}, (code, reason) => {
|
|
234
|
-
if (line === l) line = undefined
|
|
235
|
-
if (code === HANDOVER) retry = 0
|
|
236
|
-
if (code === 4000 && reason) fail(new FnError(JSON.parse(reason), 429))
|
|
237
|
-
else done()
|
|
238
|
-
}))
|
|
239
|
-
signal.addEventListener('abort', () => l.close())
|
|
240
|
-
})
|
|
241
|
-
},
|
|
242
|
-
change(body) {
|
|
243
|
-
if (!line) return Promise.reject(new FnError({}, 503))
|
|
244
|
-
line.send(JSON.stringify(body))
|
|
245
|
-
return Promise.resolve()
|
|
246
|
-
},
|
|
247
|
-
}
|
|
123
|
+
const { http, socket } = transports(() => (retry = 0))
|
|
248
124
|
|
|
249
125
|
/**
|
|
250
126
|
* @weft-js/core/test only (`live-fake.ts`). The specifier hides it from the bundler: an `import()` it can see would keep the
|
|
@@ -254,10 +130,22 @@ const FAKE = './live-fake.ts'
|
|
|
254
130
|
const fake: Link = {
|
|
255
131
|
open(body, heard, signal) {
|
|
256
132
|
let up!: () => void
|
|
257
|
-
void track(
|
|
258
|
-
|
|
133
|
+
void track(
|
|
134
|
+
new Promise<void>((ok) => {
|
|
135
|
+
up = ok
|
|
136
|
+
}),
|
|
137
|
+
) // `net.settle()` waits for the connection, not for its end
|
|
138
|
+
return import(/* @vite-ignore */ FAKE)
|
|
139
|
+
.then((m) =>
|
|
140
|
+
m.open(body, heard, signal, {
|
|
141
|
+
up,
|
|
142
|
+
subs,
|
|
143
|
+
closing: (close: () => unknown) => (closeHub = close),
|
|
144
|
+
}),
|
|
145
|
+
)
|
|
146
|
+
.finally(up)
|
|
259
147
|
},
|
|
260
|
-
change: async body => (await import(/* @vite-ignore */ FAKE)).change(body),
|
|
148
|
+
change: async (body) => (await import(/* @vite-ignore */ FAKE)).change(body),
|
|
261
149
|
}
|
|
262
150
|
|
|
263
151
|
/** Two sockets that never answered, and this host has none. */
|
|
@@ -265,12 +153,14 @@ const link = (): Link => (DEV && net() ? fake : noSockets() ? http : socket)
|
|
|
265
153
|
let via: Link = socket
|
|
266
154
|
let closeHub: (() => unknown) | undefined
|
|
267
155
|
|
|
268
|
-
const
|
|
156
|
+
const DEAD = 45_000,
|
|
157
|
+
HIDDEN = 300_000
|
|
269
158
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
159
|
+
interface Live {
|
|
160
|
+
ac: AbortController
|
|
161
|
+
k?: string
|
|
162
|
+
sent: Set<number>
|
|
163
|
+
}
|
|
274
164
|
let conn: Live | undefined
|
|
275
165
|
let running = false
|
|
276
166
|
let attempt = 0
|
|
@@ -297,23 +187,30 @@ function readPage() {
|
|
|
297
187
|
seeded = !!cursor
|
|
298
188
|
}
|
|
299
189
|
|
|
300
|
-
const entry = (s: Sub): SubEntry =>
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
190
|
+
const entry = (s: Sub): SubEntry =>
|
|
191
|
+
s.fn.kind === 'query'
|
|
192
|
+
? [s.n, 'q', s.fn.$id, s.input]
|
|
193
|
+
: [s.n, 's', s.fn.$id, s.input, s.cursor ?? '', s.buffer ?? 0]
|
|
194
|
+
const set = (status: LiveStatus) => {
|
|
195
|
+
rt.status.value = status
|
|
196
|
+
}
|
|
197
|
+
interface Linked {
|
|
198
|
+
m: LiveMode
|
|
199
|
+
d?: 1
|
|
200
|
+
}
|
|
306
201
|
/** The connection is up: how live that is depends on the server's feed. */
|
|
307
202
|
function linked(l?: Linked) {
|
|
308
203
|
rt.mode.value = l?.m ?? 'app'
|
|
309
204
|
set(l?.d ? 'reconnecting' : l?.m === 'poll' || l?.m === 'off' ? 'degraded' : 'live')
|
|
310
205
|
if (DEV && l && l.m !== 'feed' && l.m !== 'external' && !warned) {
|
|
311
206
|
warned = true
|
|
312
|
-
console.warn(
|
|
207
|
+
console.warn(
|
|
208
|
+
`weft: R016 live queries are \`degraded\` (mode \`${l.m}\`): writes from outside the app arrive late or not at all. The server log says why.`,
|
|
209
|
+
)
|
|
313
210
|
}
|
|
314
211
|
}
|
|
315
212
|
let warned = false
|
|
316
|
-
const offline = () => !
|
|
213
|
+
const offline = () => !navigator.onLine
|
|
317
214
|
|
|
318
215
|
function flushPokes() {
|
|
319
216
|
const tags = [...pokes]
|
|
@@ -323,7 +220,8 @@ function flushPokes() {
|
|
|
323
220
|
}
|
|
324
221
|
|
|
325
222
|
function frame(event: string, data: unknown) {
|
|
326
|
-
const d = data as Row &
|
|
223
|
+
const d = data as Row &
|
|
224
|
+
Record<'b' | 'k' | 'c', string> & { l?: Linked; t: string[]; retry: number }
|
|
327
225
|
batch(() => {
|
|
328
226
|
if (event === 'h') {
|
|
329
227
|
if (build && d.b && d.b !== build) rt.stale.value = true
|
|
@@ -334,14 +232,7 @@ function frame(event: string, data: unknown) {
|
|
|
334
232
|
linked(d.l)
|
|
335
233
|
sync() // what changed while connecting
|
|
336
234
|
} else if (event === 'a') {
|
|
337
|
-
|
|
338
|
-
if (!s) return
|
|
339
|
-
s.tags = d[1]
|
|
340
|
-
s.error.value = undefined
|
|
341
|
-
if (DEV && !s.tags.length) console.warn(`weft: W053 \`${s.fn.$id}\` is read with \`.live()\` but declares no \`tags\`: no mutation can ever poke it.\n fix: add \`tags\` to the query`)
|
|
342
|
-
// Data older than the subscription may have missed a write. A page's first connect is covered by its cursor.
|
|
343
|
-
if (!s.acked && !seeded && s.tags.length) void cache.invalidate(...s.tags)
|
|
344
|
-
s.acked = true
|
|
235
|
+
ack(d, byN, () => seeded)
|
|
345
236
|
} else if (event === 'p') {
|
|
346
237
|
cursor = d.c
|
|
347
238
|
for (const t of d.t) pokes.add(t)
|
|
@@ -349,38 +240,24 @@ function frame(event: string, data: unknown) {
|
|
|
349
240
|
else pokeTimer ??= setTimeout(flushPokes, 50 + Math.random() * 250)
|
|
350
241
|
} else if (event === 'R') {
|
|
351
242
|
cursor = d.c
|
|
352
|
-
void cache.invalidate(...[...subs.values()].flatMap(s => s.tags))
|
|
353
|
-
} else if (event === 'm') {
|
|
354
|
-
|
|
355
|
-
if (!s) return
|
|
356
|
-
if (d[1] !== null) s.cursor = d[1]
|
|
357
|
-
s.error.value = undefined
|
|
358
|
-
for (const v of s.views.values()) v.box.value = v.fold ? v.fold(v.box.value as never, d[2] as never) : d[2]
|
|
359
|
-
} else if (event === 'r') {
|
|
360
|
-
const s = byN.get(d[0])
|
|
361
|
-
if (!s) return
|
|
362
|
-
s.cursor = undefined
|
|
363
|
-
for (const v of s.views.values()) v.box.value = v.initial
|
|
364
|
-
} else if (event === 'f') {
|
|
365
|
-
const s = byN.get(d[0])
|
|
366
|
-
if (s) s.done.value = true
|
|
367
|
-
} else if (event === 'e') {
|
|
368
|
-
const s = byN.get(d[0])
|
|
369
|
-
if (s) s.error.value = new FnError(d[1], (d[1] as { code?: string }).code === 'A002' ? 401 : 500)
|
|
243
|
+
void cache.invalidate(...[...subs.values()].flatMap((s) => s.tags))
|
|
244
|
+
} else if (event === 'm' || event === 'r' || event === 'f' || event === 'e') {
|
|
245
|
+
update(event, d, byN)
|
|
370
246
|
} else if (event === 'L') linked(d as unknown as Linked)
|
|
371
247
|
else if (event === 'b') retry = d.retry
|
|
372
248
|
})
|
|
373
249
|
}
|
|
374
250
|
|
|
375
251
|
/** Woken early by `online`, by the tab showing again and by a change to the set. */
|
|
376
|
-
const sleep = (t: number) =>
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
252
|
+
const sleep = (t: number) =>
|
|
253
|
+
new Promise<void>((ok) => {
|
|
254
|
+
const timer = setTimeout(ok, t)
|
|
255
|
+
nudge = () => {
|
|
256
|
+
clearTimeout(timer)
|
|
257
|
+
nudge = undefined
|
|
258
|
+
ok()
|
|
259
|
+
}
|
|
260
|
+
})
|
|
384
261
|
|
|
385
262
|
async function run() {
|
|
386
263
|
running = true
|
|
@@ -402,9 +279,18 @@ async function run() {
|
|
|
402
279
|
alive()
|
|
403
280
|
retry = undefined
|
|
404
281
|
try {
|
|
405
|
-
await (via = link()).open(
|
|
282
|
+
await (via = link()).open(
|
|
283
|
+
{ c: cursor, s: [...subs.values()].filter((s) => !s.done.value).map(entry) },
|
|
284
|
+
frame,
|
|
285
|
+
ac.signal,
|
|
286
|
+
alive,
|
|
287
|
+
)
|
|
406
288
|
} catch (e) {
|
|
407
|
-
if (!ac.signal.aborted && e instanceof FnError)
|
|
289
|
+
if (!ac.signal.aborted && e instanceof FnError)
|
|
290
|
+
for (const s of subs.values())
|
|
291
|
+
untrack(() => {
|
|
292
|
+
s.error.value = e
|
|
293
|
+
})
|
|
408
294
|
}
|
|
409
295
|
clearTimeout(dead)
|
|
410
296
|
conn = undefined
|
|
@@ -430,9 +316,11 @@ function sync() {
|
|
|
430
316
|
if (!subs.size) return conn?.ac.abort()
|
|
431
317
|
if (!running) return void run()
|
|
432
318
|
const live = conn
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
319
|
+
// Between connections the loop sleeps: a changed set wakes it, as the sleep promises.
|
|
320
|
+
if (!live) return nudge?.()
|
|
321
|
+
if (!live.k) return
|
|
322
|
+
const add = [...byN.values()].filter((s) => !live.sent.has(s.n))
|
|
323
|
+
const gone = [...live.sent].filter((n) => !byN.has(n))
|
|
436
324
|
if (!add.length && !gone.length) return
|
|
437
325
|
for (const s of add) live.sent.add(s.n)
|
|
438
326
|
for (const n of gone) live.sent.delete(n)
|
|
@@ -495,5 +383,8 @@ function reset() {
|
|
|
495
383
|
n.reconnects = 0
|
|
496
384
|
n.rt = undefined
|
|
497
385
|
void closeHub?.()
|
|
498
|
-
untrack(() => {
|
|
386
|
+
untrack(() => {
|
|
387
|
+
rt.stale.value = false
|
|
388
|
+
rt.status.value = 'connecting'
|
|
389
|
+
})
|
|
499
390
|
}
|
package/src/media.ts
CHANGED
|
@@ -22,7 +22,14 @@ const record = () =>
|
|
|
22
22
|
audio: zod.boolean().optional(),
|
|
23
23
|
animated: zod.boolean().optional(),
|
|
24
24
|
loop: zod.number().optional(),
|
|
25
|
-
copy: zod
|
|
25
|
+
copy: zod
|
|
26
|
+
.object({
|
|
27
|
+
src: zod.string(),
|
|
28
|
+
type: zod.string(),
|
|
29
|
+
bytes: zod.number().optional(),
|
|
30
|
+
ratio: zod.number().optional(),
|
|
31
|
+
})
|
|
32
|
+
.optional(),
|
|
26
33
|
swap: zod.enum(['intent', 'idle', 'never']).optional(),
|
|
27
34
|
})
|
|
28
35
|
|
package/src/migrations.mjs
CHANGED
|
@@ -6,14 +6,22 @@ import { join } from 'node:path'
|
|
|
6
6
|
|
|
7
7
|
const DIRS = ['drizzle', 'migrations', 'prisma/migrations']
|
|
8
8
|
|
|
9
|
-
export function readMigrations(root, dir = DIRS.find(d => existsSync(join(root, d)))) {
|
|
9
|
+
export function readMigrations(root, dir = DIRS.find((d) => existsSync(join(root, d)))) {
|
|
10
10
|
if (!dir) return []
|
|
11
11
|
const base = join(root, dir)
|
|
12
12
|
const journal = join(base, 'meta', '_journal.json')
|
|
13
|
-
if (existsSync(journal))
|
|
13
|
+
if (existsSync(journal))
|
|
14
|
+
return JSON.parse(readFileSync(journal, 'utf8')).entries.map((e) => ({
|
|
15
|
+
tag: e.tag,
|
|
16
|
+
sql: readFileSync(join(base, `${e.tag}.sql`), 'utf8'),
|
|
17
|
+
}))
|
|
14
18
|
if (!existsSync(base)) return []
|
|
15
|
-
return readdirSync(base)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
return readdirSync(base)
|
|
20
|
+
.sort()
|
|
21
|
+
.flatMap((name) => {
|
|
22
|
+
const file = name.endsWith('.sql') ? join(base, name) : join(base, name, 'migration.sql')
|
|
23
|
+
return existsSync(file)
|
|
24
|
+
? [{ tag: name.replace(/\.sql$/, ''), sql: readFileSync(file, 'utf8') }]
|
|
25
|
+
: []
|
|
26
|
+
})
|
|
19
27
|
}
|