@weft-js/start 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +16 -0
  2. package/README.md +41 -2
  3. package/package.json +107 -4
  4. package/src/app.mjs +93 -0
  5. package/src/auth/better-auth.ts +58 -0
  6. package/src/auth/drizzle.ts +17 -0
  7. package/src/auth.ts +47 -0
  8. package/src/boot.ts +44 -0
  9. package/src/builtin.tsx +19 -0
  10. package/src/bus/postgres.ts +91 -0
  11. package/src/bus.ts +48 -0
  12. package/src/capture.d.mts +1 -0
  13. package/src/capture.mjs +15 -0
  14. package/src/changes-sql.d.mts +14 -0
  15. package/src/changes-sql.mjs +99 -0
  16. package/src/changes.ts +253 -0
  17. package/src/channel-fake.ts +55 -0
  18. package/src/channel.ts +622 -0
  19. package/src/client.tsx +20 -0
  20. package/src/contract.mjs +312 -0
  21. package/src/data.ts +929 -0
  22. package/src/db/bun-sqlite.d.ts +10 -0
  23. package/src/db/changes-mysql.ts +86 -0
  24. package/src/db/changes-postgres.ts +159 -0
  25. package/src/db/changes-sqlite.ts +116 -0
  26. package/src/db/core.ts +63 -0
  27. package/src/db/index.ts +27 -0
  28. package/src/db/mysql.ts +67 -0
  29. package/src/db/postgres.ts +65 -0
  30. package/src/db/sqlite.ts +139 -0
  31. package/src/derive.d.mts +16 -0
  32. package/src/derive.mjs +140 -0
  33. package/src/dev-runtime.mjs +42 -0
  34. package/src/exec.ts +519 -0
  35. package/src/extension.ts +38 -0
  36. package/src/first-paint.ts +57 -0
  37. package/src/form.tsx +134 -0
  38. package/src/gate.css +6 -0
  39. package/src/hub.ts +822 -0
  40. package/src/index.ts +106 -0
  41. package/src/inline.mjs +2 -0
  42. package/src/inspect.mjs +280 -0
  43. package/src/link.ts +461 -0
  44. package/src/list-drizzle.ts +50 -0
  45. package/src/list.ts +224 -0
  46. package/src/listen.ts +28 -0
  47. package/src/live-fake.ts +42 -0
  48. package/src/live-target.mjs +147 -0
  49. package/src/live.ts +499 -0
  50. package/src/media.ts +43 -0
  51. package/src/migrations.mjs +19 -0
  52. package/src/mux-session.ts +6 -0
  53. package/src/mux.ts +135 -0
  54. package/src/og/DefaultCard.tsx +60 -0
  55. package/src/og/fonts.mjs +64 -0
  56. package/src/og/images.mjs +54 -0
  57. package/src/og/index.mjs +146 -0
  58. package/src/og/native.mjs +29 -0
  59. package/src/range.mjs +19 -0
  60. package/src/router.ts +149 -0
  61. package/src/routes.mjs +142 -0
  62. package/src/run.tsx +202 -0
  63. package/src/rust.ts +333 -0
  64. package/src/server.tsx +1021 -0
  65. package/src/shape.d.mts +3 -0
  66. package/src/shape.mjs +73 -0
  67. package/src/share-server.ts +208 -0
  68. package/src/share.ts +503 -0
  69. package/src/sheets.mjs +75 -0
  70. package/src/sidecar.mjs +503 -0
  71. package/src/socket.ts +389 -0
  72. package/src/stream.ts +13 -0
  73. package/src/verify.ts +251 -0
  74. package/src/view.tsx +47 -0
  75. package/src/vite.mjs +899 -0
package/src/data.ts ADDED
@@ -0,0 +1,929 @@
1
+ // Server functions and the client data cache (research 27). `query`/`mutation` definitions live in server modules
2
+ // (`src/server/**`, `*.server.ts`); the `weft:start` plugin gives each an id and, for the client, strips everything
3
+ // but `optimistic`. One definition is then:
4
+ //
5
+ // - on the server: `q.fetch(input)` runs `run` through the request cache (exec.ts), at most once per request;
6
+ // - in a component or scope: `q(input)` is a `Query` (a Resource) reading the client cache. Its fetcher reads the
7
+ // entry's value, so a new server value or an optimistic layer updates every reader in place;
8
+ // - a mutation `m(input)` POSTs, applies `optimistic` layers at once, and settles with the refreshed queries the
9
+ // server sends back in the same response (single-flight).
10
+ //
11
+ // The cache entry's value is `base` with each pending layer applied. New bases are structurally shared with the old
12
+ // one, so unchanged rows keep their identity and `For` keeps them. This module ships to the client; the executor
13
+ // (exec.ts) is the server's, and the in-process fake transport @weft-js/core/test turns on loads it on demand. Realtime
14
+ // (research 30) costs a query app one method here: `q.live()` imports live.ts on its first call.
15
+
16
+ import { DEV, createRoot, derived, effect, getOwner, onCleanup, resource, state, untrack, type Derived, type Resource, type State } from '@weft-js/core'
17
+ import { fill, mask } from '@weft-js/core/shape'
18
+ import { client } from './router'
19
+ import type { MediaAsset } from '@weft-js/core/media'
20
+ import type * as Exec from './exec'
21
+ import type { Scope } from './exec'
22
+ import type { ServerExtension } from './extension'
23
+
24
+ // ---- definitions ------------------------------------------------------------------------------------------------
25
+
26
+ /** Here and not in verify.ts: that file needs `postgres`, and an app without it still reads this type. */
27
+ export interface VerifyOptions {
28
+ /** `true` or a target's name, as `verify: true` and `verify: 'staging'` mean, for when the rest of this is needed too. */
29
+ target?: true | string
30
+ /** A running staging app. Fine in code. */
31
+ url?: string
32
+ /** A database, from the environment: `process.env.STAGING_DATABASE_URL`. */
33
+ db?: string
34
+ /** The query's input. */
35
+ input?: unknown
36
+ /** Columns of the test row: whose it is, so the signed-in test user can see it. The rest are made up. */
37
+ row?: Record<string, unknown>
38
+ /** Name of the env var with a cookie for a user who can see the row. Default `WEFT_STAGING_COOKIE`. */
39
+ cookie?: string
40
+ /** Name of the env var with a cookie for a user who can't. Default `WEFT_STAGING_OTHER_COOKIE`; without it, a signed-out client. */
41
+ otherCookie?: string
42
+ /** Ms each link may take. Default 5000. */
43
+ timeout?: number
44
+ }
45
+
46
+ /** The Standard Schema interface (standardschema.dev): Zod, Valibot, ArkType and Drizzle's derived schemas. */
47
+ export interface StandardSchemaV1<I = unknown, O = I> {
48
+ readonly '~standard': {
49
+ readonly version: 1
50
+ readonly vendor: string
51
+ readonly validate: (value: unknown) => SchemaResult<O> | Promise<SchemaResult<O>>
52
+ readonly types?: { readonly input: I; readonly output: O } | undefined
53
+ }
54
+ }
55
+ /** What a Standard Schema validator answers. */
56
+ export type SchemaResult<O> = { readonly value: O; readonly issues?: undefined } | { readonly issues: readonly SchemaIssue[] }
57
+ /** One failure from a schema, with the path to the field when there is one. */
58
+ export interface SchemaIssue { readonly message: string; readonly path?: readonly (PropertyKey | { readonly key: PropertyKey })[] | undefined }
59
+
60
+ /**
61
+ * What `run` and `load` get. Augment it with what `src/context.server.ts` returns:
62
+ * `declare module '@weft-js/start' { interface Context { db: Db; user: User | null } }`.
63
+ */
64
+ export interface Context {
65
+ request: Request
66
+ /** Headers merged into the response (cookies a sign-in sets). */
67
+ response: { headers: Headers }
68
+ /** Uploads and CMS images (research 29 §6): `ingest` stores the original and returns the record `z.media()` validates. */
69
+ media: { ingest(file: Blob | Uint8Array, options?: { name?: string }): Promise<MediaAsset> }
70
+ }
71
+
72
+ /** The address the server saw (never one a client sent): Better Auth's rate limits key on it. */
73
+ export const CLIENT_IP = 'x-weft-client-ip'
74
+
75
+ /** Options a plugin adds to `query` and `mutation`, by augmenting this; its extension's `guard` reads them. Empty here. */
76
+ export interface FnOptions {}
77
+
78
+ /** `'minutes'` etc. or seconds: how long the shared server cache keeps a value (research 27 §4). */
79
+ export type Life = 'seconds' | 'minutes' | 'hours' | 'days' | 'max' | number
80
+
81
+ /** `auth: 'user'` narrows `ctx.user` to non-null in `run` (A002 answers a call with no session). */
82
+ export type AuthScope = 'user'
83
+ type Ctx<A> = A extends 'user' ? (Context extends { user: infer U } ? Omit<Context, 'user'> & { user: NonNullable<U> } : Context) : Context
84
+
85
+ type In<S> = S extends StandardSchemaV1<infer I, unknown> ? I : void
86
+ type Out<S> = S extends StandardSchemaV1<unknown, infer O> ? O : undefined
87
+ type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T
88
+ type Tags<A extends unknown[]> = readonly string[] | ((...args: A) => readonly string[])
89
+ /** Read before `run`, for one user's tag: `tags: (_, ctx) => [`entitlements:${ctx.user.id}`]`. */
90
+ type QueryTags<I, A> = Tags<[input: I, ctx: Ctx<A>]>
91
+
92
+ /** What an `Impl` is called with. */
93
+ export interface ImplCall { fn: Fn; input: unknown; ctx: Context; request: Request }
94
+
95
+ /** A server function's body in Rust: `remote('rust://search')` or `native('media')`, from `@weft-js/start/rust` (research 36, 37). */
96
+ export interface Impl { readonly kind: string; readonly target: string; readonly service: string; call(c: ImplCall): Promise<{ value: unknown; tags: string[]; touched: string[] }> }
97
+
98
+ /** Errors a function raises by name, with the data each carries: a Rust function's become an enum. */
99
+ export type Errors = Record<string, StandardSchemaV1>
100
+
101
+ /** `query()` with a `run` on this server. */
102
+ export interface QueryDef<S, O, A = undefined> extends FnOptions {
103
+ input?: S
104
+ /** The result's shape: checked in dev and tests (C002), and the contract's `output`. */
105
+ output?: StandardSchemaV1
106
+ /** `'user'`: refuse a call with no session (A002). The contract's auth scope. */
107
+ auth?: A
108
+ /** An example result, for what `output` can't say: sizes a pending view (research 31 §2). */
109
+ shape?: O extends (infer R)[] ? DeepPartial<R>[] : DeepPartial<O>
110
+ /** What invalidates it: the client cache, the request cache and the shared cache all go by these. */
111
+ tags?: QueryTags<Out<S>, A>
112
+ /** `required: true`: `weft check` and `weft build` fail on a target with no real change feed, and the server won't start without one (R013, R015). */
113
+ live?: {
114
+ required?: boolean
115
+ /** A real round trip against a target that isn't production, in `weft check` and `weft build`. `true` finds `WEFT_STAGING_URL` or `STAGING_DATABASE_URL`; a string names a target in `live.targets`. */
116
+ verify?: true | string | VerifyOptions
117
+ }
118
+ /** Turn on the shared server cache (across requests). Omit it and the query runs once per request. */
119
+ cache?: { life?: Life; /** One cached value per signed-in user: `ctx.user` is readable. */ scope?: 'user' }
120
+ /** Named failures, for the client as `FnError.code` and `.data`. */
121
+ errors?: Errors
122
+ run: (args: { input: Out<S>; ctx: Ctx<A> }) => O | Promise<O>
123
+ impl?: undefined
124
+ /** Set by the `weft:start` plugin: `<export>.<file hash>`. */
125
+ $id?: string
126
+ }
127
+
128
+ /** The same declaration with its body elsewhere: `output` is required, because it's the whole contract for what comes back. */
129
+ export type ImplQueryDef<S, OS extends StandardSchemaV1, A = undefined> = Omit<QueryDef<S, Out<OS>, A>, 'run' | 'impl' | 'output'> & { output: OS; impl: Impl; run?: undefined }
130
+ /** `mutation()` with `impl` and an `output` schema: no `run` here. */
131
+ export type ImplMutationDef<S, OS extends StandardSchemaV1, A = undefined> = Omit<MutationDef<S, Out<OS>, A>, 'run' | 'impl' | 'output'> & { output: OS; impl: Impl; run?: undefined }
132
+
133
+ /** `mutation()` with a `run` on this server. */
134
+ export interface MutationDef<S, O, A = undefined> extends FnOptions {
135
+ input?: S
136
+ output?: StandardSchemaV1
137
+ auth?: A
138
+ /** Tags whose queries the response refreshes (single-flight) and whose cached values it drops. */
139
+ invalidates?: Tags<[input: Out<S>, result: Awaited<O>]>
140
+ /** Client only: predict the result. Each call's layers are re-applied on new values until it settles. */
141
+ optimistic?: (args: { input: In<S> }, cache: OptimisticCache) => void
142
+ errors?: Errors
143
+ run: (args: { input: Out<S>; ctx: Ctx<A> }) => O | Promise<O>
144
+ impl?: undefined
145
+ $id?: string
146
+ }
147
+
148
+ /** The typed "no query" input: `getTodo(() => (id.value ? { id: id.value } : skip))`. */
149
+ export const skip: unique symbol = Symbol.for('weft.skip') as never
150
+ /** The marker a live query's input takes to subscribe to nothing yet. */
151
+ export type Skip = typeof skip
152
+
153
+ type Arg<I> = I | (() => I | Skip)
154
+
155
+ /** What `query()` returns: call it for a resource, or `.live()` for one that stays current. */
156
+ export interface Query<I, O> {
157
+ /** The live value, read in a component or a defineState setup (Q001 elsewhere: use `fetch`). */
158
+ (...args: [I] extends [void] ? [input?: undefined | (() => undefined | Skip)] : [input: Arg<I>]): Resource<O>
159
+ /** The value as a promise: in `load`, handlers, server code and tests. Through the request or client cache. */
160
+ fetch(...args: [I] extends [void] ? [] : [input: I]): Promise<O>
161
+ /** A placeholder result to size a pending view by (last seen, else declared), or undefined when nothing is known. */
162
+ shape(...args: [I] extends [void] ? [] : [input: I]): O | undefined
163
+ /**
164
+ * The same value, refetched whenever a mutation anywhere invalidates one of its tags. `.live` is the connection's
165
+ * status. `keep`: how long the subscription outlives its last reader (`'30s'`, `'session'`).
166
+ */
167
+ live(...args: [I] extends [void] ? [input?: undefined | (() => undefined | Skip), options?: LiveOptions] : [input: Arg<I>, options?: LiveOptions]): LiveQuery<O>
168
+ readonly kind: 'query'
169
+ readonly $id: string
170
+ }
171
+
172
+ /** What `mutation()` returns: call it, or read its `pending` and `error`. */
173
+ export interface Mutation<I, O> {
174
+ (...args: [I] extends [void] ? [] : [input: I]): Promise<O>
175
+ /** A call is in flight. */
176
+ readonly pending: Derived<boolean>
177
+ /** The last call's failure, until the next call succeeds. */
178
+ readonly error: Derived<unknown>
179
+ readonly kind: 'mutation'
180
+ readonly $id: string
181
+ }
182
+
183
+ /** `degraded`: connected, and writes from outside the app arrive late or not at all. `.liveMode` says which. */
184
+ export type LiveStatus = 'connecting' | 'live' | 'degraded' | 'reconnecting' | 'offline'
185
+ /**
186
+ * How writes reach this page. `app`: mutations only, no `changes` configured. `feed`: the database reports every change.
187
+ * `external`: a separate listener does, through the bus. `poll`: an estimate, every few seconds. `off`: mutations only, though `changes` asks for more.
188
+ */
189
+ export type LiveMode = 'app' | 'feed' | 'external' | 'poll' | 'off'
190
+ /** `.live()`: how long it's kept, and whether it refuses to run without a real feed. */
191
+ export interface LiveOptions {
192
+ keep?: string | number
193
+ /** Fail `weft check`, `weft build` and server startup rather than run without a real change feed (R013, R015). */
194
+ required?: boolean
195
+ }
196
+ /** A resource that also says how live it is. */
197
+ export type LiveQuery<O> = Resource<O> & { readonly live: Derived<LiveStatus>; readonly liveMode: Derived<LiveMode> }
198
+
199
+ /** Any definition, as the executor and transports see it. */
200
+ export interface Fn {
201
+ kind: 'query' | 'mutation' | 'stream' | 'channel' | 'shared'
202
+ $id: string
203
+ def: {
204
+ input?: StandardSchemaV1; output?: StandardSchemaV1; message?: StandardSchemaV1; shape?: unknown; auth?: AuthScope; run?: (args: { input: unknown; ctx: Context; signal?: AbortSignal; lastEventId?: unknown }) => unknown; impl?: Impl; errors?: Errors; tags?: Tags<[input: unknown, ctx?: Context]>; cache?: { life?: Life; scope?: 'user' }; invalidates?: Tags<[unknown, unknown]>; optimistic?: (args: { input: unknown }, cache: OptimisticCache) => void
205
+ /** `channel` and `sharedState` (research 32). */
206
+ params?: StandardSchemaV1; authorize?: unknown; left?: (args: { id: string; ctx: Context; params: unknown; room: unknown }) => unknown; recheck?: (args: { id: string; ctx: Context; reply: unknown }) => unknown; client?: Record<string, StandardSchemaV1>; server?: Record<string, StandardSchemaV1>; on?: Record<string, (args: never) => unknown>; limits?: { perSecond?: number }; key?: StandardSchemaV1; value?: StandardSchemaV1; initial?: unknown
207
+ }
208
+ }
209
+
210
+ /** Every definition by id (the server answers `/_weft/fn/<id>` from it). */
211
+ export const fns = new Map<string, Fn>()
212
+ let auto = 0
213
+
214
+ // ---- errors and redirects -----------------------------------------------------------------------------------
215
+
216
+ /** A diagnostic thrown by @weft-js/start: `code` (Q001…) and a fix in the message, like weft's StateError. */
217
+ export class StartError extends Error {
218
+ /** The HTTP status, when `extra` sets one. */
219
+ declare status?: number
220
+ /** Field by field, when `extra` sets them. */
221
+ declare issues?: SchemaIssue[]
222
+ /** What a declared error carries. */
223
+ declare data?: unknown
224
+ /** `raise`: this is a failure the function declares, so its message reaches the client. */
225
+ declare typed?: boolean
226
+ constructor(public code: string, message: string, fix?: string, public extra: Record<string, unknown> = {}) {
227
+ super(`${code}: ${message}${fix ? `\n fix: ${fix}` : ''}`)
228
+ Object.assign(this, extra)
229
+ }
230
+ }
231
+
232
+ /** A failed server function call, as the client sees it. */
233
+ export class FnError extends Error {
234
+ code?: string
235
+ status: number
236
+ issues?: SchemaIssue[]
237
+ /** What a declared error (`errors`) carries. */
238
+ data?: unknown
239
+ constructor(e: { message?: string; code?: string; issues?: SchemaIssue[] | undefined; data?: unknown }, status: number) {
240
+ super(e.message ?? `server function failed (${status})`)
241
+ this.status = status
242
+ Object.assign(this, { code: e.code, issues: e.issues, data: e.data })
243
+ }
244
+ }
245
+
246
+ declare const redirectBrand: unique symbol
247
+ /** What `redirect()` throws: a loader or a function's way to send the page elsewhere. */
248
+ export interface Redirect {
249
+ readonly isRedirect: true
250
+ readonly href: string
251
+ readonly status: number
252
+ readonly [redirectBrand]?: true
253
+ }
254
+
255
+ /**
256
+ * Send the request elsewhere: in `load` (a page answers 307 with `location`, client navigation follows) or in a
257
+ * `run` (the client router navigates). It throws, so `(await db.get(id)) ?? redirect('/todos')` types as the row.
258
+ */
259
+ export function redirect(href: string, status: 301 | 302 | 303 | 307 | 308 = 307): never {
260
+ // oxlint-disable-next-line typescript/only-throw-error -- control flow, not a failure: a stack costs and nothing reads it
261
+ throw { isRedirect: true, href, status } as Redirect
262
+ }
263
+
264
+ /** Whether a thrown value is a `redirect()`. */
265
+ export const isRedirect = (v: unknown): v is Redirect => !!v && typeof v === 'object' && (v as Partial<Redirect>).isRedirect === true
266
+
267
+ /**
268
+ * Fail with an error the function declares in `errors`. The client sees it as `FnError.code` and `.data`, whether `run`
269
+ * or a Rust `impl` raised it.
270
+ */
271
+ export function raise(code: string, data?: unknown): never {
272
+ throw new StartError(code, `failed: ${code}`, undefined, { status: 400, data, typed: true })
273
+ }
274
+
275
+ /** Refuse a value the schema can't judge (`email` is taken): `<Form>` shows it next to the field, as a schema issue. */
276
+ export function invalid(fields: Record<string, string>): never {
277
+ throw new StartError('Q002', Object.keys(fields).join(', '), undefined, { status: 422, issues: Object.entries(fields).map(([k, message]) => ({ message, path: k.split('.') })) })
278
+ }
279
+
280
+ /** Issues as a form shows them: by field (`a.b`), and the ones that name no field. */
281
+ export function fieldsOf(issues: readonly SchemaIssue[] = []) {
282
+ const errors: Record<string, string> = {}
283
+ let message: string | undefined
284
+ for (const i of issues) {
285
+ const key = i.path?.map(p => (typeof p === 'object' ? p.key : p)).join('.')
286
+ if (key) errors[key] ??= i.message
287
+ else message ??= i.message
288
+ }
289
+ return { errors, message }
290
+ }
291
+
292
+ type Tree = Record<string, unknown>
293
+
294
+ const UNSAFE = new Set(['__proto__', 'constructor', 'prototype'])
295
+
296
+ /** A form's fields as a mutation's input, on both sides (research 29 §5): `a.b` nests, `x[]` and repeats are arrays. */
297
+ export function decodeForm(data: FormData): Record<string, unknown> {
298
+ const out: Tree = {}
299
+ for (const [name, value] of data) {
300
+ if (name[0] === '$' || (typeof value !== 'string' && !value.size)) continue // `$fn`; a file input left empty
301
+ const list = name.endsWith('[]')
302
+ const path = (list ? name.slice(0, -2) : name).split('.')
303
+ if (path.some(p => UNSAFE.has(p))) continue
304
+ let at = out
305
+ for (const p of path.slice(0, -1)) {
306
+ const next = at[p]
307
+ at = (next && typeof next === 'object' ? next : (at[p] = {})) as Tree
308
+ }
309
+ const k = path.at(-1)!
310
+ // Own keys only: a field named `toString` is a field, not the method every object inherits.
311
+ const had = Object.hasOwn(at, k)
312
+ if (list) ((had ? at[k] : (at[k] = [])) as unknown[]).push(value)
313
+ else if (had) at[k] = [at[k]].flat().concat(value)
314
+ else at[k] = value
315
+ }
316
+ return out
317
+ }
318
+
319
+ // ---- keys -------------------------------------------------------------------------------------------------------
320
+
321
+ /** JSON with sorted object keys: the same input gives the same key on both sides (no input: `''`; JSON sends null). */
322
+ export function stable(v: unknown): string {
323
+ if (v == null) return ''
324
+ return JSON.stringify(v, (_, x: Record<string, unknown>) => (x && typeof x === 'object' && !Array.isArray(x) ? Object.fromEntries(Object.keys(x).sort().map(k => [k, x[k]])) : x))
325
+ }
326
+
327
+ /** The cache key of one call: the function's id and its input in a stable order. */
328
+ export const keyOf = (id: string, input: unknown) => `${id}:${stable(input)}`
329
+
330
+ // ---- runtime hooks ------------------------------------------------------------------------------------------------
331
+
332
+ /** One cached entry as the server sends it: `[id, input, value, tags]` (page payload, navigation, refreshes). */
333
+ export type Wire = [id: string, input: unknown, value: unknown, tags: string[]]
334
+
335
+ /** What the transports answer (research 27 §3). */
336
+ export interface Answer { v?: unknown; t?: string[]; c?: Wire[]; r?: string; /** `r` is a full load: the session changed, so nothing cached may survive. */ x?: 1 }
337
+
338
+ /** How a client call reaches the server: `fetch` by default, a fake under test. */
339
+ export interface Transport {
340
+ query(id: string, input: unknown): Promise<Answer>
341
+ /** `form`: the input came from a form, so strings may be coerced; a FormData is sent as is (it holds files). */
342
+ mutate(id: string, input: unknown, refresh: [string, unknown][], form?: true | FormData): Promise<Answer>
343
+ }
344
+
345
+ /**
346
+ * Set by the server half (server.tsx): the request in progress and the executor. `navigate` overrides the client
347
+ * router's (tests). Unset in plain tests.
348
+ */
349
+ export const runtime: {
350
+ scope?: (() => Scope | undefined) | undefined
351
+ /** Run `fn` with `scope` current (the request's AsyncLocalStorage). */
352
+ enter?: (<T>(scope: unknown, fn: () => T) => T) | undefined
353
+ exec?: typeof Exec
354
+ transport?: Transport
355
+ navigate?: ((href: string, replace?: boolean) => void) | undefined
356
+ /** Set by the server's first request (and in dev when `drizzle/` grows): what each database applies before its first statement (db/core.ts). */
357
+ migrations?: { list: { tag: string; sql: string }[]; report(e: unknown): void } | undefined
358
+ /** The gates of clients whose statements can't be held (Prisma's): the server waits on them per request, and a failure doesn't fail it. */
359
+ dbs?: Set<() => Promise<void> | undefined>
360
+ /** Fan invalidated tags out to live subscribers (hub.ts). `origin`: the connection that needs no poke. */
361
+ poke?: ((tags: string[], origin?: string | null) => Promise<void>) | undefined
362
+ /** This tab's realtime connection: sent with mutations, so the tab doesn't poke itself. */
363
+ conn?: string | undefined
364
+ /** `room.in(params).publish`: socket.ts, on a server or under the fake. */
365
+ channels?: { publish(fn: Fn, params: unknown, name: string, message: unknown, options?: { exceptSelf?: boolean; from?: string }): Promise<void>; set(fn: Fn, key: unknown, value: unknown): Promise<void> }
366
+ } = {}
367
+
368
+ /** The realtime connection's status and "this tab is older than the server" (research 30 §2). */
369
+ export const rt = createRoot(() => ({ status: state<LiveStatus>('connecting'), mode: state<LiveMode>('app'), stale: state(false) }), null)
370
+ /** The server runs a different build than this tab loaded: offer a reload. */
371
+ export const stale: Derived<boolean> = rt.stale
372
+
373
+ const go = (href: string, replace?: boolean) => (runtime.navigate ? runtime.navigate(href, replace) : void client.navigate?.(href, { replace }))
374
+
375
+ /** @weft-js/core/test's controls, when it's loaded; the first look registers the cache reset it runs after each test. */
376
+ let registered = false
377
+ /** The test controls, once `@weft-js/core/test` has registered them. */
378
+ export const net = () => {
379
+ const n = Reflect.get(globalThis, Symbol.for('weft.net')) as NetControl | undefined
380
+ if (n && !registered) {
381
+ registered = true
382
+ n.resets.push(() => {
383
+ cache.clear()
384
+ callId = 0
385
+ })
386
+ }
387
+ return n
388
+ }
389
+
390
+ /** @weft-js/core/test's fake-transport controls (research 27 §7). */
391
+ export interface NetControl {
392
+ latency: number
393
+ ctx?: object
394
+ /** Server extensions for this test (`net.extension`). */
395
+ extensions: ServerExtension[]
396
+ online(): boolean
397
+ fails: Map<string, unknown[]>
398
+ handlers: Map<string, (input: unknown, ctx: Context) => unknown>
399
+ log: { id: string; kind: string; input: unknown }[]
400
+ inflight: Set<Promise<unknown>>
401
+ als: { run<T>(store: unknown, fn: () => T): T; getStore(): unknown }
402
+ resets: (() => unknown)[]
403
+ /** Subscriptions an owner holds: open at the end of a test, they're T012. */
404
+ open: Map<object, [what: string, close: () => void]>
405
+ /** The realtime fake, while one is connected: `net.push`, `net.poke`, `net.drop`. */
406
+ rt?: { push(id: string, msg: unknown): number; poke(tags: string[]): Promise<void>; drop(): void; subscriptions(): { kind: string; id: string; input: unknown }[] } | undefined
407
+ /** The channel fake, while a socket is open. */
408
+ ws?: { publish(id: string, params: unknown, name: string, msg: unknown): Promise<void>; drop(): void; recheck(): Promise<void> } | undefined
409
+ /** What this client's sockets sent, across reconnects. */
410
+ sent: { id: string; name: string; msg: unknown }[]
411
+ reconnects: number
412
+ }
413
+
414
+ // ---- definitions --------------------------------------------------------------------------------------------------
415
+
416
+ const oneBody = (def: { run?: unknown; impl?: unknown; $id?: string }) => {
417
+ if (def.run && def.impl) throw new StartError('R103', `\`${def.$id ?? 'a function'}\` has both \`run\` and \`impl\``, 'keep one: `impl` replaces the body')
418
+ }
419
+
420
+ /** A server function that reads, with its output typed by a schema. */
421
+ export function query<OS extends StandardSchemaV1, S extends StandardSchemaV1 | undefined = undefined, const A extends AuthScope | undefined = undefined>(def: ImplQueryDef<S, OS, A>): Query<In<S>, Out<OS>>
422
+ /** A server function that reads, with its output typed by `run`. */
423
+ export function query<O, S extends StandardSchemaV1 | undefined = undefined, const A extends AuthScope | undefined = undefined>(def: QueryDef<S, O, A>): Query<In<S>, Awaited<O>>
424
+ /** A server function that reads: cached by input on the client, and tagged for what invalidates it. */
425
+ export function query(def: unknown): unknown {
426
+ const d = def as Fn['def'] & { $id?: string }
427
+ oneBody(d)
428
+ const id = d.$id ?? `q${++auto}`
429
+ const fn: Fn = { kind: 'query', $id: id, def: d }
430
+ fns.set(id, fn)
431
+ const q = (input?: unknown) => use(fn, input)
432
+ q.fetch = (input?: unknown) => fetchQuery(fn, input)
433
+ q.shape = (input?: unknown) => shapeOf(fn, input)
434
+ q.live = (input?: unknown, options?: LiveOptions) => liveQuery(fn, input, options)
435
+ return Object.assign(q, { kind: 'query', $id: id, [FN]: fn })
436
+ }
437
+
438
+ /** A server function that writes, with its output typed by a schema. */
439
+ export function mutation<OS extends StandardSchemaV1, S extends StandardSchemaV1 | undefined = undefined, const A extends AuthScope | undefined = undefined>(def: ImplMutationDef<S, OS, A>): Mutation<In<S>, Out<OS>>
440
+ /** A server function that writes, with its output typed by `run`. */
441
+ export function mutation<O, S extends StandardSchemaV1 | undefined = undefined, const A extends AuthScope | undefined = undefined>(def: MutationDef<S, O, A>): Mutation<In<S>, Awaited<O>>
442
+ /** A server function that writes, and invalidates the queries that read what it changed. */
443
+ export function mutation(def: unknown): unknown {
444
+ const d = def as Fn['def'] & { $id?: string }
445
+ oneBody(d)
446
+ const id = d.$id ?? `m${++auto}`
447
+ const fn: Fn = { kind: 'mutation', $id: id, def: d }
448
+ fns.set(id, fn)
449
+ // Module-level boxes, read on the client only (a server render never shows a call in flight).
450
+ const { calls, error } = createRoot(() => ({ calls: state(0), error: state<unknown>(undefined) }), null)
451
+ const pending = createRoot(() => derived(() => calls.value > 0), null)
452
+ const m = (input?: unknown) => mutate(fn, input, calls, error)
453
+ return Object.assign(m, { kind: 'mutation', $id: id, pending, error: { get value() { return error.value } }, [FN]: fn, [SUBMIT]: (input: unknown, form: true | FormData) => mutate(fn, input, calls, error, form) })
454
+ }
455
+
456
+ const FN = Symbol.for('weft.fn')
457
+ /** `<Form>`'s call: a mutation handle's `m(input)` with the input marked as coming from a form. */
458
+ export const SUBMIT = Symbol.for('weft.submit')
459
+ /** The definition behind a query or mutation handle (also accepts the definition). */
460
+ export const fnOf = (h: unknown): Fn => ((h as Record<symbol, Fn> | null)?.[FN] ?? h) as Fn
461
+
462
+ // ---- server and client calls ---------------------------------------------------------------------------------
463
+
464
+ const server = () => runtime.scope?.() ?? (net()?.als.getStore() as Scope | undefined)
465
+ /** The request scope in progress, on the server or under the fake transport. */
466
+ export const currentScope = server
467
+ /** Run `fn` with `scope` current. */
468
+ export const enter = <T>(scope: unknown, fn: () => T): T => (runtime.enter ? runtime.enter(scope, fn) : net() ? net()!.als.run(scope, fn) : fn())
469
+
470
+ function fetchQuery(fn: Fn, input: unknown): Promise<unknown> {
471
+ const scope = server()
472
+ if (scope) return runtime.exec!.fetchQuery(scope, fn, input).then(e => e.v)
473
+ if (runtime.exec && !net()) return runtime.exec.fetchQuery(runtime.exec.createScope(new Request('http://localhost/')), fn, input).then(e => e.v)
474
+ const e = entry(fn, input)
475
+ return untrack(() => fresh(e)) ? Promise.resolve(untrack(() => e.value.value)) : load(e)
476
+ }
477
+
478
+ function use(fn: Fn, input: unknown): Resource<unknown> {
479
+ if (!getOwner())
480
+ throw DEV ? new StartError('Q001', `\`${fn.$id}()\` (a live query) was called outside a component or scope`, `\`await ${name(fn)}.fetch(input)\` in \`load\`, handlers and server code`) : new StartError('Q001', fn.$id)
481
+ const read = () => (typeof input === 'function' ? (input as () => unknown)() : input)
482
+ return resource(() => {
483
+ const arg = read()
484
+ if (arg === skip) return undefined
485
+ const scope = server()
486
+ if (scope) return runtime.exec!.readQuery(scope, fn, arg)
487
+ const e = entry(fn, arg)
488
+ e.readers++
489
+ onCleanup(() => void e.readers--)
490
+ const v = e.value.value // tracked: a new base or layer re-runs this, and the resource settles to it in place
491
+ if (fresh(e)) return v
492
+ return load(e)
493
+ }, {
494
+ data: false,
495
+ shape: {
496
+ of: fn,
497
+ read() {
498
+ const arg = read()
499
+ return arg === skip ? undefined : shapeOf(fn, arg)
500
+ },
501
+ },
502
+ })
503
+ }
504
+
505
+ /** Research 31 §2: this entry's value or the query's last seen, masked; else the declared shape. */
506
+ function shapeOf(fn: Fn, input: unknown): unknown {
507
+ const limit: number | undefined = (input as { limit?: number } | undefined)?.limit
508
+ const here = entries.get(keyOf(fn.$id, input))
509
+ const e = server() ? undefined : here && untrack(() => here.has.value) ? here : lastSeen.get(fn.$id)
510
+ if (e) return mask(untrack(() => e.value.value), limit)
511
+ const tree = runtime.exec ? runtime.exec.shapeOf(fn) : fn.def.shape
512
+ return tree === undefined ? undefined : fill(tree, limit)
513
+ }
514
+
515
+ /** `q()` plus a subscription: its tags come from the server, and a poke is a local `cache.invalidate`. */
516
+ function liveQuery(fn: Fn, input: unknown, options?: LiveOptions): LiveQuery<unknown> {
517
+ const r = use(fn, input)
518
+ const scope = server()
519
+ if (scope) scope.live = true
520
+ else
521
+ effect(() => {
522
+ const arg = typeof input === 'function' ? (input as () => unknown)() : input
523
+ if (arg === skip) return
524
+ let gone = false, stop: (() => void) | undefined
525
+ const held = import('./live').then(m => {
526
+ m.use(liveApi)
527
+ if (!gone) stop = m.hold(fn, arg, options)
528
+ })
529
+ void track(held)
530
+ onCleanup(() => {
531
+ gone = true
532
+ stop?.()
533
+ })
534
+ }, () => {})
535
+ return Object.assign(r, { live: rt.status, liveMode: rt.mode })
536
+ }
537
+
538
+ const name = (fn: Fn) => fn.$id.replace(/\..*$/, '')
539
+
540
+ // ---- the client cache -----------------------------------------------------------------------------------------
541
+
542
+ interface Layer { m: number; apply: (draft: unknown) => unknown; waiting?: boolean }
543
+ interface Entry {
544
+ key: string
545
+ id: string
546
+ input: unknown
547
+ base: State<unknown>
548
+ has: State<boolean>
549
+ stale: State<boolean>
550
+ layers: State<Layer[]>
551
+ value: Derived<unknown>
552
+ tags: string[]
553
+ readers: number
554
+ p?: Promise<unknown> | undefined
555
+ /** Invalidated while `p` was in flight: its answer may predate the write. */
556
+ again?: boolean
557
+ }
558
+
559
+ const entries = new Map<string, Entry>()
560
+ /** Each query's latest entry: a skeleton for other args takes its shape (research 31 §4). */
561
+ const lastSeen = new Map<string, Entry>()
562
+ /** Objects an optimistic layer made (`cache.isOptimistic(row)`). */
563
+ let made = new WeakSet<object>()
564
+
565
+ function entry(fn: Fn, input: unknown): Entry {
566
+ if (!registered) net()
567
+ const key = keyOf(fn.$id, input)
568
+ let e = entries.get(key)
569
+ if (!e) {
570
+ // Owned by no component: the entry outlives its readers (the next reader finds it seeded).
571
+ e = createRoot(() => {
572
+ const base = state<unknown>(undefined), has = state(false), dated = state(false), layers = state<Layer[]>([])
573
+ const value = derived(() => {
574
+ let v = base.value
575
+ if (!has.value) return v
576
+ for (const l of layers.value) v = produce(v, l.apply)
577
+ return v
578
+ })
579
+ return { key, id: fn.$id, input, base, has, stale: dated, layers, value, tags: [], readers: 0 }
580
+ }, null)
581
+ entries.set(key, e)
582
+ }
583
+ return e
584
+ }
585
+
586
+ const fresh = (e: Entry) => e.has.value && !e.stale.value
587
+
588
+ function load(e: Entry): Promise<unknown> {
589
+ return (e.p ??= transport().query(e.id, e.input).then(
590
+ a => {
591
+ e.p = undefined
592
+ if (a.r !== undefined) {
593
+ go(a.r, true)
594
+ redirect(a.r)
595
+ }
596
+ setBase(e, a.v, a.t ?? [])
597
+ if (e.again) {
598
+ e.again = false
599
+ untrack(() => { e.stale.value = true }) // readers refetch: this answer may be older than the poke
600
+ }
601
+ return untrack(() => e.value.value)
602
+ },
603
+ err => {
604
+ e.p = undefined
605
+ throw err
606
+ },
607
+ ))
608
+ }
609
+
610
+ /** A server value lands: shared with the old base, layers of settled calls dropped, readers updated in place. */
611
+ function setBase(e: Entry, v: unknown, tags: string[]) {
612
+ untrack(() => {
613
+ e.tags = tags
614
+ e.base.value = share(e.base.value, v)
615
+ e.has.value = true
616
+ e.stale.value = false
617
+ lastSeen.set(e.id, e)
618
+ if (e.layers.value.some(l => l.waiting)) e.layers.value = e.layers.value.filter(l => !l.waiting)
619
+ })
620
+ }
621
+
622
+ /** Seed the cache from the server (the page's `#weft-cache`, a navigation's `c`, a mutation's refreshes). */
623
+ export function seed(list: Wire[] | null | undefined) {
624
+ for (const [id, input, v, tags] of list ?? []) setBase(entry(fns.get(id) ?? ({ $id: id } as Fn), input), v, tags)
625
+ }
626
+
627
+
628
+ /** Mark entries with any of `tags` stale: entries with readers refetch now, the rest on their next read. */
629
+ function staleTags(tags: string[], except: Set<string> = new Set()) {
630
+ if (!tags.length) return
631
+ untrack(() => {
632
+ for (const e of entries.values()) {
633
+ if (except.has(e.key) || !e.tags.some(t => tags.includes(t))) continue
634
+ e.stale.value = true
635
+ if (e.p) e.again = true
636
+ }
637
+ })
638
+ client.invalidated?.(tags)
639
+ }
640
+
641
+ async function mutate(fn: Fn, input: unknown, calls: State<number>, error: State<unknown>, form?: true | FormData): Promise<unknown> {
642
+ const scope = server()
643
+ if (scope) return (await runtime.exec!.runMutation(scope, fn, input, [], !!form)).v
644
+ const m = ++callId
645
+ const touched = new Set<Entry>()
646
+ untrack(() => calls.value++)
647
+ if (fn.def.optimistic) untrack(() => fn.def.optimistic!({ input }, overlay(m, touched)))
648
+ const refresh = [...entries.values()].filter(e => e.readers > 0).map(e => [e.id, e.input] as [string, unknown])
649
+ const drop = (e: Entry) => { e.layers.value = e.layers.value.filter(l => l.m !== m) }
650
+ try {
651
+ const a = await transport().mutate(fn.$id, input, refresh, form)
652
+ let out: unknown
653
+ untrack(() => {
654
+ const refreshed = new Set<string>()
655
+ for (const w of a.c ?? []) {
656
+ const e = entry(fns.get(w[0]) ?? ({ $id: w[0] } as Fn), w[1])
657
+ setBase(e, w[2], w[3])
658
+ refreshed.add(e.key)
659
+ }
660
+ for (const e of touched) {
661
+ if (refreshed.has(e.key)) drop(e)
662
+ else {
663
+ // S011: the server sent no fresh value for an entry this call predicted. Keep the layer until the entry's
664
+ // next value (refetched now) instead of flashing back to the old one.
665
+ if (DEV && !e.tags.some(t => a.t?.includes(t)))
666
+ console.warn(`weft: S011 \`${fn.$id}\`'s optimistic updated \`${e.key}\` (tags ${JSON.stringify(e.tags)}), but its invalidated tags ${JSON.stringify(a.t ?? [])} don't cover it: the prediction had no fresh value to land on.\n fix: add one of its tags to \`invalidates\``)
667
+ e.layers.value = e.layers.value.map(l => (l.m === m ? { ...l, waiting: true } : l))
668
+ e.stale.value = true
669
+ if (!e.readers) load(e).catch(() => {})
670
+ }
671
+ }
672
+ staleTags(a.t ?? [], refreshed)
673
+ error.value = undefined
674
+ out = a.v
675
+ })
676
+ if (a.r !== undefined) { if (a.x) location.assign(a.r); else go(a.r) }
677
+ return out
678
+ } catch (err) {
679
+ untrack(() => {
680
+ for (const e of touched) drop(e) // only this call's prediction goes
681
+ error.value = err
682
+ })
683
+ throw err
684
+ } finally {
685
+ untrack(() => calls.value--)
686
+ }
687
+ }
688
+ let callId = 0
689
+
690
+ /** The `cache` an `optimistic` gets: `update` and `set` add this call's layers instead of writing the base. */
691
+ function overlay(m: number, touched: Set<Entry>): OptimisticCache {
692
+ const push = (q: unknown, input: unknown, apply: Layer['apply']) => {
693
+ const e = entry(fnOf(q), input)
694
+ touched.add(e)
695
+ e.layers.value = [...e.layers.value, { m, apply }]
696
+ }
697
+ return {
698
+ ...cache,
699
+ update: (q, input, recipe) => push(q, input, recipe as Layer['apply']),
700
+ set: (q, input, value) => push(q, input, () => value),
701
+ }
702
+ }
703
+
704
+ /** A query handle, whatever its input: what the cache needs of one. */
705
+ export type QueryRef<O> = Pick<Query<void, O>, 'kind' | '$id'> & { fetch(...args: never[]): Promise<O> }
706
+
707
+ /** The cache as an `optimistic` sees it. */
708
+ export interface OptimisticCache {
709
+ /** Change a query's cached value with a draft (copy-on-write: untouched rows keep their identity). */
710
+ update<O>(q: QueryRef<O>, input: unknown, recipe: (draft: O) => O | void): void
711
+ set<O>(q: QueryRef<O>, input: unknown, value: O): void
712
+ get<O>(q: QueryRef<O>, input?: unknown): O | undefined
713
+ /** The value (a row) was made by a pending optimistic layer. */
714
+ isOptimistic(value: unknown): boolean
715
+ }
716
+
717
+ /**
718
+ * The client cache. In `optimistic`, `update`/`set` add layers; elsewhere they write the base (until the next server
719
+ * value). `invalidate` marks tags stale (readers refetch).
720
+ */
721
+ export const cache: OptimisticCache & { invalidate(...tags: string[]): Promise<void> | undefined; clear(): void } = {
722
+ update(q, input, recipe) {
723
+ const e = entry(fnOf(q), input)
724
+ if (untrack(() => e.has.value)) setBase(e, produce(untrack(() => e.base.value), recipe as Layer['apply']), e.tags)
725
+ },
726
+ set(q, input, value) {
727
+ const e = entry(fnOf(q), input)
728
+ setBase(e, value, e.tags)
729
+ },
730
+ get<O>(q: QueryRef<O>, input?: unknown) {
731
+ const e = entries.get(keyOf(fnOf(q).$id, input))
732
+ return e && (untrack(() => e.value.value) as O)
733
+ },
734
+ isOptimistic: v => !!v && typeof v === 'object' && made.has(v),
735
+ invalidate(...tags) {
736
+ const scope = server()
737
+ if (scope) return runtime.exec!.invalidate(scope, tags)
738
+ staleTags(tags)
739
+ return undefined
740
+ },
741
+ clear() {
742
+ entries.clear()
743
+ lastSeen.clear()
744
+ made = new WeakSet()
745
+ },
746
+ }
747
+
748
+ if ((import.meta as ImportMeta & { hot?: unknown }).hot) client.refreshed = () => untrack(() => { for (const e of entries.values()) e.stale.value = true })
749
+
750
+ /** Server: drop `tags` for this response and the shared cache. Client: refetch what reads them. */
751
+ export const invalidate = (...tags: string[]) => cache.invalidate(...tags)
752
+
753
+ // ---- transports ---------------------------------------------------------------------------------------------------
754
+
755
+ function transport(): Transport {
756
+ if (runtime.transport) return runtime.transport
757
+ if (net()) return inProcess
758
+ return http
759
+ }
760
+
761
+ const answer = async (res: Response): Promise<Answer> => {
762
+ const body = await res.json().catch(() => ({ e: { message: `server function answered ${res.status}` } }))
763
+ if (body.e) throw new FnError(body.e, res.status)
764
+ return body
765
+ }
766
+
767
+ /** The browser's: GET for queries, POST + JSON + `x-weft` for mutations (research 27 §3). */
768
+ export const http: Transport = {
769
+ query: (id, input) => fetch(`/_weft/fn/${id}${input == null ? '' : `?i=${encodeURIComponent(stable(input))}`}`, { headers: { accept: 'application/json' } }).then(answer),
770
+ mutate(id, input, f, form) {
771
+ const headers: Record<string, string> = { accept: 'application/json', 'x-weft': '1', ...(runtime.conn && { 'x-weft-conn': runtime.conn }) }
772
+ if (form instanceof FormData) form.set('$r', JSON.stringify(f))
773
+ else headers['content-type'] = 'application/json'
774
+ return fetch(`/_weft/fn/${id}`, { method: 'POST', headers, body: form instanceof FormData ? form : JSON.stringify({ i: input, f, ...(form && { c: 1 }) }) }).then(answer)
775
+ },
776
+ }
777
+
778
+ /**
779
+ * @weft-js/core/test's fake transport: the server's executor in process, with the controls @weft-js/core/test's `net` sets (latency,
780
+ * failures, replaced functions, offline), and a JSON round trip both ways so values arrive as they would over HTTP.
781
+ */
782
+ export const fakeScope = (exec: typeof Exec, n: NetControl, request: Request) =>
783
+ exec.createScope(request, { context: () => n.ctx ?? {}, handlers: n.handlers, dev: true, extensions: n.extensions, onDiagnostic: d => console.warn(`weft: ${d.message.startsWith(d.code) ? '' : d.code + ' '}${d.message}`) })
784
+
785
+ type Call = (exec: typeof Exec, scope: Scope, fn: Fn) => Promise<Answer>
786
+
787
+ const inProcess: Transport = {
788
+ query: (id, input) => fake(id, 'query', input, (exec, scope, fn) => exec.fetchQuery(scope, fn, input).then(e => ({ v: e.v, t: e.tags }))),
789
+ mutate: (id, input, f, form) => fake(id, 'mutation', input, (exec, scope, fn) => exec.runMutation(scope, fn, input, f, !!form)),
790
+ }
791
+
792
+ function fake(id: string, kind: string, input: unknown, call: Call): Promise<Answer> {
793
+ return track(fakeCall(net()!, id, kind, input, call))
794
+ }
795
+
796
+ /** `net.settle()` waits for `p`. */
797
+ export function track<T>(p: Promise<T>): Promise<T> {
798
+ const n = net()
799
+ if (!n) return p
800
+ n.inflight.add(p)
801
+ const over = () => void n.inflight.delete(p)
802
+ p.then(over, over)
803
+ return p
804
+ }
805
+
806
+ async function fakeCall(n: NetControl, id: string, kind: string, input: unknown, call: Call): Promise<Answer> {
807
+ n.log.push({ id, kind, input })
808
+ const exec = (runtime.exec ??= await import('./exec'))
809
+ if (n.latency) await new Promise(ok => { setTimeout(ok, n.latency) })
810
+ if (!n.online()) throw Object.assign(new Error(`offline: ${id} wasn't sent`), { name: 'OfflineError' })
811
+ const fails = n.fails.get(id)
812
+ if (fails?.length) throw fails.shift()
813
+ const fn = fns.get(id)
814
+ if (!fn) throw new FnError({ code: 'Q009', message: `Q009: no server function \`${id}\`` }, 404)
815
+ const scope = fakeScope(exec, n, new Request(`http://localhost/_weft/fn/${id}`, { method: kind === 'query' ? 'GET' : 'POST', headers: runtime.conn ? { 'x-weft-conn': runtime.conn } : {} }))
816
+ try {
817
+ return JSON.parse(JSON.stringify(await n.als.run(scope, () => call(exec, scope, fn))))
818
+ } catch (e) {
819
+ if (isRedirect(e)) return { r: e.href }
820
+ throw e instanceof StartError ? new FnError({ code: e.code, message: e.message, issues: e.issues, data: e.data }, e.status ?? 500) : e
821
+ }
822
+ }
823
+
824
+ // ---- structural sharing and drafts ------------------------------------------------------------------------------
825
+
826
+ type Obj = Record<PropertyKey, unknown>
827
+
828
+ const plain = (v: unknown): v is Obj =>
829
+ !!v && typeof v === 'object' && (Array.isArray(v) || Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null)
830
+
831
+ /** `next`, with every subtree deep-equal to `prev`'s replaced by `prev`'s (TanStack's replaceEqualDeep). */
832
+ export function share<T>(prev: unknown, next: T): T {
833
+ if (prev === next || !plain(prev) || !plain(next) || Array.isArray(prev) !== Array.isArray(next)) return next
834
+ const keys = Object.keys(next)
835
+ let same = Object.keys(prev).length === keys.length
836
+ const out = (Array.isArray(next) ? [] : {}) as Obj
837
+ for (const k of keys) {
838
+ out[k] = share(prev[k], next[k])
839
+ if (out[k] !== prev[k]) same = false
840
+ }
841
+ return (same ? prev : out) as T
842
+ }
843
+
844
+ interface Draft { b: Obj; c?: Obj; px: Obj; done?: Obj }
845
+
846
+ /**
847
+ * Run `recipe` on a copy-on-write draft of `base`: only objects it writes (and their parents) are copied. A recipe
848
+ * may instead return a new value. The copies are remembered for `cache.isOptimistic`.
849
+ */
850
+ export function produce<T>(base: T, recipe: (draft: T) => T | void): T {
851
+ if (!plain(base)) {
852
+ const r = recipe(base)
853
+ return r === undefined ? base : r
854
+ }
855
+ const nodes = new Map<object, Draft>()
856
+ const byProxy = new WeakMap<object, Draft>()
857
+ const unwrap = (v: unknown) => (v && typeof v === 'object' && byProxy.has(v) ? byProxy.get(v)!.b : v)
858
+ const cur = (n: Draft) => n.c ?? n.b
859
+ const own = (n: Draft) => (n.c ??= (Array.isArray(n.b) ? n.b.slice() : { ...n.b }) as Obj)
860
+ const node = (b: Obj): Draft => {
861
+ let n = nodes.get(b)
862
+ if (n) return n
863
+ const self: Draft = n = { b, px: b }
864
+ n.px = new Proxy((Array.isArray(b) ? [] : {}) as Obj, {
865
+ get: (_, k) => {
866
+ const v = cur(self)[k]
867
+ return plain(v) ? node(v).px : v
868
+ },
869
+ set: (_, k, v) => {
870
+ v = unwrap(v)
871
+ if (plain(v) && !nodes.has(v)) made.add(v) // a new object (a pushed row) is optimistic too
872
+ own(self)[k] = v
873
+ return true
874
+ },
875
+ deleteProperty: (_, k) => (delete own(self)[k], true),
876
+ has: (_, k) => k in cur(self),
877
+ ownKeys: () => Reflect.ownKeys(cur(self)),
878
+ getOwnPropertyDescriptor: (t, k) => {
879
+ const d = Reflect.getOwnPropertyDescriptor(cur(self), k)
880
+ if (d && k === 'length' && Array.isArray(t)) return { ...d, configurable: false }
881
+ return d && { ...d, configurable: true, value: (self.px)[k] }
882
+ },
883
+ })
884
+ nodes.set(b, n)
885
+ byProxy.set(n.px, n)
886
+ return n
887
+ }
888
+ // The result: copies where something changed, the original objects everywhere else.
889
+ const fin = (n: Draft): Obj => {
890
+ if (n.done !== undefined) return n.done
891
+ n.done = n.b // cycles stop here
892
+ const src = cur(n)
893
+ let out = n.c
894
+ for (const k of Object.keys(src)) {
895
+ const v = src[k]
896
+ if (!plain(v) || !nodes.has(v)) continue
897
+ const f = fin(nodes.get(v)!)
898
+ if (f !== v) (out ??= own(n))[k] = f
899
+ }
900
+ if (out) made.add(out)
901
+ return (n.done = out ?? n.b)
902
+ }
903
+ const deep = (v: unknown): unknown => {
904
+ if (v && typeof v === 'object' && byProxy.has(v)) return fin(byProxy.get(v)!)
905
+ if (Array.isArray(v)) return v.map(deep)
906
+ if (plain(v)) return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, deep(x)]))
907
+ return v
908
+ }
909
+ const root = node(base)
910
+ const r = recipe(root.px as T)
911
+ return (r === undefined || r === root.px ? fin(root) : deep(r)) as T
912
+ }
913
+
914
+ /** What `live.ts` reads from here; see its header. */
915
+ export const liveApi = { cache, currentScope, fakeScope, fns, FnError, keyOf, net, rt, runtime, skip, StartError, track }
916
+ /** What `live.ts` is handed, instead of a static import that would close a cycle. */
917
+ export type LiveApi = typeof liveApi
918
+
919
+ // ---- start-up ----------------------------------------------------------------------------------------------------
920
+
921
+ // In the browser, as the first route chunk that uses a query loads (before hydration): the page's entries, and
922
+ // whatever a navigation brought before this module loaded.
923
+ if (typeof document !== 'undefined' && !runtime.scope) {
924
+ client.seed = seed
925
+ seed(client.seeds)
926
+ client.seeds = undefined
927
+ const el = document.getElementById('weft-cache')
928
+ if (el) seed(JSON.parse(el.textContent))
929
+ }