@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.
Files changed (214) hide show
  1. package/README.md +1278 -12
  2. package/crate/Cargo.lock +1347 -0
  3. package/crate/Cargo.toml +72 -0
  4. package/crate/README.md +322 -0
  5. package/crate/rust-toolchain.toml +3 -0
  6. package/crate/src/build/emit.rs +103 -0
  7. package/crate/src/build.rs +367 -0
  8. package/crate/src/ctx.rs +186 -0
  9. package/crate/src/envelope.rs +163 -0
  10. package/crate/src/functions.rs +173 -0
  11. package/crate/src/lib.rs +118 -0
  12. package/crate/src/native.rs +212 -0
  13. package/crate/src/otel.rs +62 -0
  14. package/crate/src/server.rs +203 -0
  15. package/package.json +46 -37
  16. package/src/app.mjs +38 -8
  17. package/src/asset-receipt.d.mts +13 -0
  18. package/src/asset-receipt.mjs +61 -0
  19. package/src/auth/better-auth.ts +215 -17
  20. package/src/auth/drizzle.ts +1 -1
  21. package/src/auth.ts +21 -6
  22. package/src/boot.ts +19 -8
  23. package/src/bus/postgres.ts +32 -19
  24. package/src/bus.ts +7 -4
  25. package/src/{capture.mjs → capture.ts} +34 -1
  26. package/src/changes-sql.d.mts +8 -2
  27. package/src/changes-sql.mjs +26 -10
  28. package/src/changes.ts +165 -66
  29. package/src/channel/connections.ts +216 -0
  30. package/src/channel/socket.ts +396 -0
  31. package/src/channel-fake.ts +19 -4
  32. package/src/channel.ts +120 -492
  33. package/src/client.tsx +9 -2
  34. package/src/contract/describe.mjs +231 -0
  35. package/src/contract.mjs +144 -157
  36. package/src/data/cache.ts +354 -0
  37. package/src/data/definitions.ts +94 -0
  38. package/src/data/draft.ts +104 -0
  39. package/src/data/foundation.ts +95 -0
  40. package/src/data/live-api.ts +22 -0
  41. package/src/data/mutation.ts +138 -0
  42. package/src/data/primitives.ts +135 -0
  43. package/src/data/query.ts +106 -0
  44. package/src/data/transport.ts +121 -0
  45. package/src/data/types.ts +303 -0
  46. package/src/data.ts +69 -894
  47. package/src/db/bun-sqlite.d.ts +4 -1
  48. package/src/db/changes-mysql.ts +61 -21
  49. package/src/db/changes-postgres.ts +197 -118
  50. package/src/db/changes-sqlite.ts +131 -64
  51. package/src/db/core.ts +38 -13
  52. package/src/db/index.ts +20 -4
  53. package/src/db/mysql.ts +22 -6
  54. package/src/db/postgres.ts +49 -13
  55. package/src/db/sqlite.ts +53 -27
  56. package/src/derive.d.mts +4 -1
  57. package/src/derive.mjs +136 -39
  58. package/src/dev-runtime.mjs +8 -4
  59. package/src/exec/errors.ts +57 -0
  60. package/src/exec/landing.ts +66 -0
  61. package/src/exec/scope.ts +174 -0
  62. package/src/exec/touch.ts +80 -0
  63. package/src/exec/validation.ts +176 -0
  64. package/src/exec.ts +191 -341
  65. package/src/extension.ts +8 -2
  66. package/src/first-paint.ts +26 -9
  67. package/src/form/value.ts +15 -0
  68. package/src/form.tsx +132 -43
  69. package/src/hub/channels.ts +102 -0
  70. package/src/hub/connections.ts +147 -0
  71. package/src/hub/delivery.ts +62 -0
  72. package/src/hub/feed.ts +168 -0
  73. package/src/hub/presence.ts +64 -0
  74. package/src/hub/route.ts +158 -0
  75. package/src/hub/subscriptions.ts +212 -0
  76. package/src/hub/topic.ts +98 -0
  77. package/src/hub/tracked.ts +12 -0
  78. package/src/hub/types.ts +190 -0
  79. package/src/hub/view.ts +147 -0
  80. package/src/hub.ts +132 -758
  81. package/src/index.ts +84 -10
  82. package/src/inline.mjs +356 -0
  83. package/src/inspect/compare.mjs +162 -0
  84. package/src/inspect/routes.mjs +154 -0
  85. package/src/inspect/samples.mjs +59 -0
  86. package/src/inspect/url.mjs +2 -0
  87. package/src/inspect.mjs +146 -187
  88. package/src/link/election.ts +178 -0
  89. package/src/link/lines.ts +27 -0
  90. package/src/link.ts +90 -166
  91. package/src/list-drizzle.ts +104 -18
  92. package/src/list.ts +127 -36
  93. package/src/listen.ts +32 -6
  94. package/src/live/api.ts +18 -0
  95. package/src/live/backoff.ts +6 -0
  96. package/src/live/frames.ts +42 -0
  97. package/src/live/read.ts +77 -0
  98. package/src/live/sub-types.ts +24 -0
  99. package/src/live/transport.ts +86 -0
  100. package/src/live/types.ts +52 -0
  101. package/src/live-fake.ts +20 -5
  102. package/src/live-target.mjs +112 -37
  103. package/src/live.ts +116 -225
  104. package/src/media.ts +8 -1
  105. package/src/migrations.mjs +14 -6
  106. package/src/mux.ts +161 -64
  107. package/src/og/DefaultCard.tsx +40 -9
  108. package/src/og/fonts.mjs +16 -8
  109. package/src/og/images.mjs +43 -22
  110. package/src/og/index.mjs +119 -43
  111. package/src/og/native.mjs +11 -4
  112. package/src/range.mjs +15 -3
  113. package/src/router.ts +56 -6
  114. package/src/routes.mjs +73 -22
  115. package/src/run/admission.ts +80 -0
  116. package/src/run/boot.tsx +115 -0
  117. package/src/run/cache-presentation.ts +67 -0
  118. package/src/run/cache.ts +254 -0
  119. package/src/run/helpers.ts +197 -0
  120. package/src/run/listeners.ts +155 -0
  121. package/src/run/navigation.ts +382 -0
  122. package/src/run/page-skeleton.tsx +35 -0
  123. package/src/run/prefetch.ts +128 -0
  124. package/src/run/retain-view.ts +18 -0
  125. package/src/run/shape-preparation.ts +97 -0
  126. package/src/run/shape-styles.ts +72 -0
  127. package/src/run/source-context/pin.ts +124 -0
  128. package/src/run/source-context.ts +311 -0
  129. package/src/run.tsx +50 -178
  130. package/src/rust/common.ts +163 -0
  131. package/src/rust/native.ts +282 -0
  132. package/src/rust.ts +103 -229
  133. package/src/server/card.tsx +65 -0
  134. package/src/server/compression.ts +115 -0
  135. package/src/server/core.ts +70 -0
  136. package/src/server/dispatch.ts +70 -0
  137. package/src/server/form.ts +85 -0
  138. package/src/server/functions.ts +129 -0
  139. package/src/server/handler.ts +39 -0
  140. package/src/server/head.ts +96 -0
  141. package/src/server/media.ts +60 -0
  142. package/src/server/navigation-cache.ts +80 -0
  143. package/src/server/navigation.tsx +85 -0
  144. package/src/server/page-cold.ts +95 -0
  145. package/src/server/page-stream.tsx +181 -0
  146. package/src/server/page-types.ts +51 -0
  147. package/src/server/page-warm.ts +130 -0
  148. package/src/server/page.ts +69 -0
  149. package/src/server/payload.ts +89 -0
  150. package/src/server/prerender.ts +60 -0
  151. package/src/server/preview.tsx +67 -0
  152. package/src/server/render.ts +167 -0
  153. package/src/server/request.ts +98 -0
  154. package/src/server/resolver.ts +228 -0
  155. package/src/server/routes.ts +150 -0
  156. package/src/server/services.ts +69 -0
  157. package/src/server/text.ts +29 -0
  158. package/src/server/types.ts +160 -0
  159. package/src/server.tsx +82 -971
  160. package/src/shape.mjs +30 -6
  161. package/src/share/display.ts +50 -0
  162. package/src/share/network.ts +119 -0
  163. package/src/share/receive.ts +70 -0
  164. package/src/share/state.ts +30 -0
  165. package/src/share/tab-queue.ts +28 -0
  166. package/src/share-server.ts +128 -43
  167. package/src/share.ts +103 -209
  168. package/src/sheets.mjs +30 -18
  169. package/src/sidecar/build.mjs +170 -0
  170. package/src/sidecar/cargo.mjs +164 -0
  171. package/src/sidecar/common.mjs +17 -0
  172. package/src/sidecar/dev-build.mjs +91 -0
  173. package/src/sidecar/dev-watch.mjs +88 -0
  174. package/src/sidecar/manifest.mjs +19 -0
  175. package/src/sidecar/process.mjs +58 -0
  176. package/src/sidecar/refresh.mjs +19 -0
  177. package/src/sidecar.mjs +75 -416
  178. package/src/socket/check.ts +50 -0
  179. package/src/socket/deliver.ts +16 -0
  180. package/src/socket/publish.ts +56 -0
  181. package/src/socket/rooms.ts +41 -0
  182. package/src/socket/route.ts +79 -0
  183. package/src/socket/shared.ts +26 -0
  184. package/src/socket/types.ts +48 -0
  185. package/src/socket.ts +205 -194
  186. package/src/stream.ts +10 -1
  187. package/src/verify/app.ts +39 -0
  188. package/src/verify/cleanup.ts +27 -0
  189. package/src/verify/database.ts +11 -0
  190. package/src/verify/live.ts +63 -0
  191. package/src/verify/notify.ts +35 -0
  192. package/src/verify/skip.ts +19 -0
  193. package/src/verify/support.ts +149 -0
  194. package/src/verify/timing.ts +15 -0
  195. package/src/verify/trigger.ts +14 -0
  196. package/src/verify.ts +77 -176
  197. package/src/view.tsx +96 -11
  198. package/src/vite/asset-bytes.mjs +95 -0
  199. package/src/vite/assets.mjs +117 -0
  200. package/src/vite/config-hooks.mjs +166 -0
  201. package/src/vite/dev-hooks.mjs +103 -0
  202. package/src/vite/dev.mjs +102 -0
  203. package/src/vite/entries.mjs +145 -0
  204. package/src/vite/module-hooks.mjs +288 -0
  205. package/src/vite/route-modules.mjs +289 -0
  206. package/src/vite/server-functions.mjs +222 -0
  207. package/src/vite/shape-entry-css.mjs +187 -0
  208. package/src/vite/shape-override.mjs +226 -0
  209. package/src/vite/shared.mjs +51 -0
  210. package/src/vite/sheets.mjs +92 -0
  211. package/src/vite/state.mjs +89 -0
  212. package/src/vite/syntax.mjs +139 -0
  213. package/src/vite.mjs +152 -853
  214. 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, derived, effect, getOwner, onCleanup, state, untrack, type Derived, type State } from '@weft-js/core'
9
- import type { AuthScope, Context, Fn, LiveApi, LiveMode, LiveOptions, LiveStatus, Skip, StandardSchemaV1 } from './data'
10
- import type { Body, SubEntry, Tracked } from './hub'
11
- import { HANDOVER, noSockets, openLine, type Line } from './link'
12
-
13
- // `data.ts` hands these over (`use`). A static import would close a cycle with its `import('./live')`, and the bundler
14
- // would then keep this chunk, the hub and the split they force in apps that never read `.live`.
15
- let cache: LiveApi['cache'], currentScope: LiveApi['currentScope'], fns: LiveApi['fns'], FnError: LiveApi['FnError'], keyOf: LiveApi['keyOf'], net: LiveApi['net'], rt: LiveApi['rt'], runtime: LiveApi['runtime'], skip: LiveApi['skip'], StartError: LiveApi['StartError'], track: LiveApi['track']
16
- export const use = (api: LiveApi) => void ({ cache, currentScope, fns, FnError, keyOf, net, rt, runtime, skip, StartError, track } = api)
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<M, S extends StandardSchemaV1 | undefined = undefined, const A extends AuthScope | undefined = undefined>(def: StreamDef<S, M, A>): StreamFn<In<S>, M> {
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((input: unknown, options?: StreamOptions<never, unknown>) => read(fn, input, options), { kind: 'stream', $id: id, [FN]: fn }) as unknown as StreamFn<In<S>, M>
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(() => ({ n: ++nth, key, fn, input, refs: 0, tags: [], acked: false, views: new Map(), error: state<unknown>(undefined), done: state(false) }), null)
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
- /** How the client reaches `/_weft/rt`: SSE, a shared socket, or a fake under test. */
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(new Promise<void>(ok => { up = ok })) // `net.settle()` waits for the connection, not for its end
258
- return import(/* @vite-ignore */ FAKE).then(m => m.open(body, heard, signal, up, subs, (close: () => unknown) => (closeHub = close))).finally(up)
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 BASE = 500, CAP = 30_000, DEAD = 45_000, HIDDEN = 300_000
156
+ const DEAD = 45_000,
157
+ HIDDEN = 300_000
269
158
 
270
- /** Full jitter: anywhere from 0 to 0.5 s, 1 s, 2 s … 30 s. */
271
- export const backoff = (attempt: number, random = Math.random) => random() * Math.min(CAP, BASE * 2 ** attempt)
272
-
273
- interface Live { ac: AbortController; k?: string; sent: Set<number> }
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 => (s.fn.kind === 'query' ? [s.n, 'q', s.fn.$id, s.input] : [s.n, 's', s.fn.$id, s.input, s.cursor ?? '', s.buffer ?? 0])
301
- const set = (status: LiveStatus) => { rt.status.value = status }
302
- interface Linked { m: LiveMode; d?: 1 }
303
- /** What's positional in a frame: `[subscription, ...]`. What the rest of the fields mean goes by the event. */
304
- type Row = { 0: number; 1: never; 2: unknown }
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(`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.`)
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 = () => ! navigator.onLine
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 & Record<'b' | 'k' | 'c', string> & { l?: Linked; t: string[]; retry: number }
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
- const s = byN.get(d[0])
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
- const s = byN.get(d[0])
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) => new Promise<void>(ok => {
377
- const timer = setTimeout(ok, t)
378
- nudge = () => {
379
- clearTimeout(timer)
380
- nudge = undefined
381
- ok()
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({ c: cursor, s: [...subs.values()].filter(s => !s.done.value).map(entry) }, frame, ac.signal, alive)
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) for (const s of subs.values()) untrack(() => { s.error.value = e })
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
- if (!live?.k) return
434
- const add = [...byN.values()].filter(s => !live.sent.has(s.n))
435
- const gone = [...live.sent].filter(n => !byN.has(n))
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(() => { rt.stale.value = false; rt.status.value = 'connecting' })
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.object({ src: zod.string(), type: zod.string(), bytes: zod.number().optional(), ratio: zod.number().optional() }).optional(),
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
 
@@ -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)) return JSON.parse(readFileSync(journal, 'utf8')).entries.map(e => ({ tag: e.tag, sql: readFileSync(join(base, `${e.tag}.sql`), 'utf8') }))
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).sort().flatMap(name => {
16
- const file = name.endsWith('.sql') ? join(base, name) : join(base, name, 'migration.sql')
17
- return existsSync(file) ? [{ tag: name.replace(/\.sql$/, ''), sql: readFileSync(file, 'utf8') }] : []
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
  }