@weft-js/start 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -3
- package/crate/Cargo.lock +1347 -0
- package/crate/Cargo.toml +72 -0
- package/crate/rust-toolchain.toml +3 -0
- package/crate/src/build/emit.rs +103 -0
- package/crate/src/build.rs +367 -0
- package/crate/src/ctx.rs +186 -0
- package/crate/src/envelope.rs +163 -0
- package/crate/src/functions.rs +173 -0
- package/crate/src/lib.rs +118 -0
- package/crate/src/native.rs +212 -0
- package/crate/src/otel.rs +62 -0
- package/crate/src/server.rs +203 -0
- package/package.json +46 -37
- package/src/app.mjs +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
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { createRoot, derived, state, untrack, type Derived, type State } from '@weft-js/core'
|
|
2
|
+
import { client, peers } from '../router'
|
|
3
|
+
import { fnOf, fns, go, net, registered, runtime, server } from './foundation'
|
|
4
|
+
import { FnError, isRedirect, keyOf, redirect } from './primitives'
|
|
5
|
+
import { isMade, produce, resetMade, share } from './draft'
|
|
6
|
+
import { transport } from './transport'
|
|
7
|
+
import type { Fn, OptimisticCache, QueryRef, Wire } from './types'
|
|
8
|
+
|
|
9
|
+
export interface Layer {
|
|
10
|
+
m: number
|
|
11
|
+
apply: (draft: unknown) => unknown
|
|
12
|
+
waiting?: boolean
|
|
13
|
+
}
|
|
14
|
+
export interface Entry {
|
|
15
|
+
key: string
|
|
16
|
+
id: string
|
|
17
|
+
input: unknown
|
|
18
|
+
base: State<unknown>
|
|
19
|
+
has: State<boolean>
|
|
20
|
+
stale: State<boolean>
|
|
21
|
+
layers: State<Layer[]>
|
|
22
|
+
value: Derived<unknown>
|
|
23
|
+
tags: string[]
|
|
24
|
+
readers: number
|
|
25
|
+
p?: Promise<unknown> | undefined
|
|
26
|
+
/** The `clock` when `p` left. */
|
|
27
|
+
since: number
|
|
28
|
+
/** The `clock` when something newer than any read in flight reached the entry. */
|
|
29
|
+
gen: number
|
|
30
|
+
/** Which overlapping run of calls last landed a reply here. */
|
|
31
|
+
run: number
|
|
32
|
+
/** A value was turned away while layers were on it: it reads again once they're done. */
|
|
33
|
+
held?: boolean
|
|
34
|
+
/** When a server value last landed. */
|
|
35
|
+
at: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const entries = new Map<string, Entry>()
|
|
39
|
+
/** Orders what reaches an entry: a read or a navigation that left before `gen` is older than what's there. */
|
|
40
|
+
export let clock = 0
|
|
41
|
+
/** When each tag was last invalidated: a value read before that is out of date, whether or not the cache held it then. */
|
|
42
|
+
const written = new Map<string, number>()
|
|
43
|
+
const passed = (tags: string[], since: number) => tags.some((t) => written.get(t)! > since)
|
|
44
|
+
/** Each query's latest entry: a skeleton for other args takes its shape (research 31 §4). */
|
|
45
|
+
export const lastSeen = new Map<string, Entry>()
|
|
46
|
+
|
|
47
|
+
export function entry(fn: Fn, input: unknown): Entry {
|
|
48
|
+
if (!registered) net()
|
|
49
|
+
const key = keyOf(fn.$id, input)
|
|
50
|
+
let e = entries.get(key)
|
|
51
|
+
if (e) {
|
|
52
|
+
entries.delete(key) // the map is in order of use: `evict` starts from its far end
|
|
53
|
+
entries.set(key, e)
|
|
54
|
+
} else {
|
|
55
|
+
// Owned by no component: the entry outlives its readers (the next reader finds it seeded).
|
|
56
|
+
e = createRoot(() => {
|
|
57
|
+
const base = state<unknown>(undefined),
|
|
58
|
+
has = state(false),
|
|
59
|
+
dated = state(false),
|
|
60
|
+
layers = state<Layer[]>([])
|
|
61
|
+
const value = derived(() => {
|
|
62
|
+
let v = base.value
|
|
63
|
+
if (!has.value) return v
|
|
64
|
+
for (const l of layers.value) v = produce(v, l.apply)
|
|
65
|
+
return v
|
|
66
|
+
})
|
|
67
|
+
return {
|
|
68
|
+
key,
|
|
69
|
+
id: fn.$id,
|
|
70
|
+
input,
|
|
71
|
+
base,
|
|
72
|
+
has,
|
|
73
|
+
stale: dated,
|
|
74
|
+
layers,
|
|
75
|
+
value,
|
|
76
|
+
tags: [],
|
|
77
|
+
readers: 0,
|
|
78
|
+
since: 0,
|
|
79
|
+
gen: 0,
|
|
80
|
+
run: 0,
|
|
81
|
+
at: 0,
|
|
82
|
+
}
|
|
83
|
+
}, null)
|
|
84
|
+
entries.set(key, e)
|
|
85
|
+
if (entries.size > ceiling) evict(e)
|
|
86
|
+
}
|
|
87
|
+
return e
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const ENTRIES = 1000
|
|
91
|
+
/** When to look again: a quarter past what's left, so a page with thousands of readers doesn't walk them all per entry. */
|
|
92
|
+
let ceiling = ENTRIES
|
|
93
|
+
/** A search box writes an entry per keystroke for the tab's life. What nothing reads goes, least recently used first. `fresh` is about to get its reader. */
|
|
94
|
+
function evict(fresh: Entry) {
|
|
95
|
+
for (const e of entries.values()) {
|
|
96
|
+
if (entries.size <= ENTRIES) break
|
|
97
|
+
if (
|
|
98
|
+
e !== fresh &&
|
|
99
|
+
!e.readers &&
|
|
100
|
+
!e.p &&
|
|
101
|
+
!untrack(() => e.layers.value.length) &&
|
|
102
|
+
lastSeen.get(e.id) !== e
|
|
103
|
+
)
|
|
104
|
+
entries.delete(e.key)
|
|
105
|
+
}
|
|
106
|
+
ceiling = Math.max(ENTRIES, entries.size + (entries.size >> 2))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const fresh = (e: Entry) => e.has.value && !e.stale.value
|
|
110
|
+
|
|
111
|
+
/** A prediction no reply has confirmed yet. */
|
|
112
|
+
export const predicted = (e: Entry) => e.layers.value.some((l) => !l.waiting)
|
|
113
|
+
|
|
114
|
+
export function load(e: Entry): Promise<unknown> {
|
|
115
|
+
if (e.p && e.gen <= e.since) return e.p
|
|
116
|
+
// What's read now may or may not hold the calls in flight, so it can't go under their layers: it waits for them.
|
|
117
|
+
if (untrack(() => e.has.value && predicted(e))) {
|
|
118
|
+
e.held = true
|
|
119
|
+
return Promise.resolve(untrack(() => e.value.value))
|
|
120
|
+
}
|
|
121
|
+
const since = (e.since = clock)
|
|
122
|
+
const p: Promise<unknown> = transport()
|
|
123
|
+
.query(e.id, e.input)
|
|
124
|
+
.then(
|
|
125
|
+
(a) => {
|
|
126
|
+
if (e.p === p) e.p = undefined
|
|
127
|
+
tries = 0
|
|
128
|
+
if (a.r !== undefined) {
|
|
129
|
+
go(a.r, true)
|
|
130
|
+
redirect(a.r)
|
|
131
|
+
}
|
|
132
|
+
const tags = a.t ?? []
|
|
133
|
+
if (untrack(() => !e.has.value) && passed(tags, since)) return load(e) // a first load's tags come with its answer
|
|
134
|
+
if (!land(e, a.v, tags, since)) {
|
|
135
|
+
if (e.p) return e.p
|
|
136
|
+
if (untrack(() => e.stale.value)) return load(e)
|
|
137
|
+
}
|
|
138
|
+
return untrack(() => e.value.value)
|
|
139
|
+
},
|
|
140
|
+
(err) => {
|
|
141
|
+
if (e.p === p) e.p = undefined
|
|
142
|
+
untrack(() => {
|
|
143
|
+
if (!e.has.value) return
|
|
144
|
+
if (err instanceof FnError && (err.status === 401 || err.status === 403)) forget(e)
|
|
145
|
+
else if (!isRedirect(err) && (!(err instanceof FnError) || err.status >= 500)) retry()
|
|
146
|
+
})
|
|
147
|
+
throw err
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
return (e.p = p)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The server refused this session, so the value may not be this user's any more: readers get the first-load error. */
|
|
154
|
+
function forget(e: Entry) {
|
|
155
|
+
e.has.value = false
|
|
156
|
+
e.base.value = undefined
|
|
157
|
+
if (lastSeen.get(e.id) === e) lastSeen.delete(e.id)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** How often a failed read is tried again, doubling, for as long as something on screen is out of date. */
|
|
161
|
+
const RETRY = 1000,
|
|
162
|
+
RETRIES = 30_000
|
|
163
|
+
let again: ReturnType<typeof setTimeout> | undefined
|
|
164
|
+
let tries = 0
|
|
165
|
+
|
|
166
|
+
function retry() {
|
|
167
|
+
if (again === undefined) again = setTimeout(owed, Math.min(RETRIES, RETRY * 2 ** tries++))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Read what readers are owed, once the tab can use it. `online` and a returning tab come here at once. */
|
|
171
|
+
export function owed() {
|
|
172
|
+
clearTimeout(again)
|
|
173
|
+
again = undefined
|
|
174
|
+
// Off a browser `onLine` isn't there, and that isn't offline.
|
|
175
|
+
if (globalThis.document?.hidden || (navigator as { onLine?: boolean }).onLine === false) return // `back` or `online` comes here
|
|
176
|
+
untrack(() => {
|
|
177
|
+
for (const e of entries.values())
|
|
178
|
+
if (e.readers && e.has.value && e.stale.value && !e.p && !e.held && !predicted(e))
|
|
179
|
+
load(e).catch(() => {})
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** A read or a navigation's value lands, unless something newer got here after it left, or layers are on the entry. */
|
|
184
|
+
function land(e: Entry, v: unknown, tags: string[], since: number): boolean {
|
|
185
|
+
return untrack(() => {
|
|
186
|
+
if (e.gen > since) return false
|
|
187
|
+
if (e.has.value && predicted(e)) return !(e.held = true)
|
|
188
|
+
setBase(e, v, tags)
|
|
189
|
+
return true
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Mark the entry out of date: a read already out is older than what it has to tell. */
|
|
194
|
+
export function outdate(e: Entry) {
|
|
195
|
+
e.gen = ++clock
|
|
196
|
+
e.stale.value = true
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The layers that held a value off are gone: read once, now that every call behind them has answered. */
|
|
200
|
+
export function relieve(e: Entry) {
|
|
201
|
+
if (!e.held || predicted(e)) return
|
|
202
|
+
e.held = false
|
|
203
|
+
outdate(e)
|
|
204
|
+
load(e).catch(() => {})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** A server value lands: shared with the old base, layers of settled calls dropped, readers updated in place. */
|
|
208
|
+
export function setBase(e: Entry, v: unknown, tags: string[]) {
|
|
209
|
+
untrack(() => {
|
|
210
|
+
e.gen = ++clock
|
|
211
|
+
e.at = performance.now()
|
|
212
|
+
e.tags = tags
|
|
213
|
+
e.base.value = share(e.base.value, v)
|
|
214
|
+
e.has.value = true
|
|
215
|
+
e.stale.value = false
|
|
216
|
+
lastSeen.set(e.id, e)
|
|
217
|
+
if (e.layers.value.some((l) => l.waiting))
|
|
218
|
+
e.layers.value = e.layers.value.filter((l) => !l.waiting)
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Seed the cache from the server (the page's `#weft-cache`, a navigation's `c`). `since`: when that navigation left. */
|
|
223
|
+
export function seed(list: Wire[] | null | undefined, since = Infinity) {
|
|
224
|
+
for (const [id, input, v, tags] of list ?? []) {
|
|
225
|
+
const e = entry(fns.get(id) ?? ({ $id: id } as Fn), input)
|
|
226
|
+
if (land(e, v, tags, since) && passed(tags, since)) outdate(e)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Mark entries with any of `tags` stale: entries with readers refetch now, the rest on their next read. */
|
|
231
|
+
export function staleTags(tags: string[], except: Set<string> = new Set()) {
|
|
232
|
+
if (!tags.length) return
|
|
233
|
+
++clock
|
|
234
|
+
for (const t of tags) {
|
|
235
|
+
written.delete(t) // in order of use, like `entries`
|
|
236
|
+
written.set(t, clock)
|
|
237
|
+
}
|
|
238
|
+
for (const t of written.keys()) {
|
|
239
|
+
if (written.size <= ENTRIES) break
|
|
240
|
+
written.delete(t)
|
|
241
|
+
}
|
|
242
|
+
untrack(() => {
|
|
243
|
+
for (const e of entries.values())
|
|
244
|
+
if (!except.has(e.key) && e.tags.some((t) => tags.includes(t))) outdate(e)
|
|
245
|
+
})
|
|
246
|
+
owed() // one a failed read left stale has nothing else to wake it
|
|
247
|
+
client.invalidated?.(tags)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The client cache. In `optimistic`, `update`/`set` add layers; elsewhere they write the base (until the next server
|
|
252
|
+
* value). `invalidate` marks tags stale (readers refetch).
|
|
253
|
+
*/
|
|
254
|
+
export const cache: OptimisticCache & {
|
|
255
|
+
invalidate(...tags: string[]): Promise<void> | undefined
|
|
256
|
+
clear(): void
|
|
257
|
+
} = {
|
|
258
|
+
update(q, input, recipe) {
|
|
259
|
+
const e = entry(fnOf(q), input)
|
|
260
|
+
if (untrack(() => e.has.value))
|
|
261
|
+
setBase(
|
|
262
|
+
e,
|
|
263
|
+
produce(
|
|
264
|
+
untrack(() => e.base.value),
|
|
265
|
+
recipe as Layer['apply'],
|
|
266
|
+
),
|
|
267
|
+
e.tags,
|
|
268
|
+
)
|
|
269
|
+
},
|
|
270
|
+
set(q, input, value) {
|
|
271
|
+
const e = entry(fnOf(q), input)
|
|
272
|
+
setBase(e, value, e.tags)
|
|
273
|
+
},
|
|
274
|
+
get<O>(q: QueryRef<O>, input?: unknown) {
|
|
275
|
+
const e = entries.get(keyOf(fnOf(q).$id, input))
|
|
276
|
+
return e && (untrack(() => e.value.value) as O)
|
|
277
|
+
},
|
|
278
|
+
isOptimistic: (v) => !!v && typeof v === 'object' && isMade(v),
|
|
279
|
+
invalidate(...tags) {
|
|
280
|
+
const scope = server()
|
|
281
|
+
if (scope) return runtime.exec!.invalidate(scope, tags)
|
|
282
|
+
staleTags(tags)
|
|
283
|
+
return undefined
|
|
284
|
+
},
|
|
285
|
+
clear() {
|
|
286
|
+
entries.clear()
|
|
287
|
+
lastSeen.clear()
|
|
288
|
+
written.clear()
|
|
289
|
+
ceiling = ENTRIES
|
|
290
|
+
resetMade()
|
|
291
|
+
},
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function resetRetry() {
|
|
295
|
+
clearTimeout(again)
|
|
296
|
+
again = undefined
|
|
297
|
+
tries = 0
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Server: drop `tags` for this response and the shared cache. Client: refetch what reads them. */
|
|
301
|
+
export const invalidate = (...tags: string[]) => cache.invalidate(...tags)
|
|
302
|
+
|
|
303
|
+
/** How old a value can be when its tab comes back and still be left alone. */
|
|
304
|
+
const FOCUS = 30_000
|
|
305
|
+
|
|
306
|
+
if ((import.meta as ImportMeta & { hot?: unknown }).hot)
|
|
307
|
+
client.refreshed = () =>
|
|
308
|
+
untrack(() => {
|
|
309
|
+
for (const e of entries.values()) outdate(e)
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
// ---- start-up ----------------------------------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
// In the browser, as the first route chunk that uses a query loads (before hydration): the page's entries, and
|
|
315
|
+
// whatever a navigation brought before this module loaded.
|
|
316
|
+
if (typeof document !== 'undefined' && !runtime.scope) {
|
|
317
|
+
client.seed = seed
|
|
318
|
+
client.now = () => clock
|
|
319
|
+
seed(client.seeds)
|
|
320
|
+
client.seeds = undefined
|
|
321
|
+
const el = document.getElementById('weft-cache')
|
|
322
|
+
if (el) seed(JSON.parse(el.textContent))
|
|
323
|
+
// A stream's chunks say what landed: here is everything so far, and each one's script pushes the next.
|
|
324
|
+
// They were read for this page, so anything written here since is newer.
|
|
325
|
+
const landed = ((globalThis as { $wc?: { c: Wire[] }[] }).$wc ||= []),
|
|
326
|
+
since = clock
|
|
327
|
+
for (const l of landed) seed(l.c, since)
|
|
328
|
+
landed.push = (...l) => {
|
|
329
|
+
for (const x of l) seed(x.c, since)
|
|
330
|
+
return Array.prototype.push.apply(landed, l)
|
|
331
|
+
}
|
|
332
|
+
const later: string[] = []
|
|
333
|
+
const back = () => {
|
|
334
|
+
if (document.hidden) return
|
|
335
|
+
const old = performance.now() - FOCUS
|
|
336
|
+
// Showing and focus can come apart: one already stale is being read, and marking it again would throw that read away.
|
|
337
|
+
untrack(() => {
|
|
338
|
+
for (const e of entries.values())
|
|
339
|
+
if (e.readers && e.has.value && e.at < old && !e.stale.value) outdate(e)
|
|
340
|
+
})
|
|
341
|
+
if (later.length) staleTags(later.splice(0))
|
|
342
|
+
owed()
|
|
343
|
+
}
|
|
344
|
+
// A hidden tab reads nothing until it shows: every open tab refetching on every write is what this would cost otherwise.
|
|
345
|
+
if (peers)
|
|
346
|
+
peers.onmessage = (m) => {
|
|
347
|
+
if (!m.data) return // the session changed: run.tsx reloads
|
|
348
|
+
if (document.hidden) later.push(...m.data)
|
|
349
|
+
else staleTags(m.data)
|
|
350
|
+
}
|
|
351
|
+
addEventListener('focus', back)
|
|
352
|
+
addEventListener('online', owed)
|
|
353
|
+
document.addEventListener('visibilitychange', back)
|
|
354
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createRoot, derived, state } from '@weft-js/core'
|
|
2
|
+
import { FN, SUBMIT, fns, nextAuto } from './foundation'
|
|
3
|
+
import { mutate } from './mutation'
|
|
4
|
+
import { StartError } from './primitives'
|
|
5
|
+
import { fetchQuery, liveQuery, shapeOf, use } from './query'
|
|
6
|
+
import type {
|
|
7
|
+
AuthScope,
|
|
8
|
+
Fn,
|
|
9
|
+
ImplMutationDef,
|
|
10
|
+
ImplQueryDef,
|
|
11
|
+
In,
|
|
12
|
+
Mutation,
|
|
13
|
+
MutationDef,
|
|
14
|
+
Out,
|
|
15
|
+
Query,
|
|
16
|
+
QueryDef,
|
|
17
|
+
StandardSchemaV1,
|
|
18
|
+
LiveOptions,
|
|
19
|
+
} from './types'
|
|
20
|
+
|
|
21
|
+
const oneBody = (def: { run?: unknown; impl?: unknown; $id?: string }) => {
|
|
22
|
+
if (def.run && def.impl)
|
|
23
|
+
throw new StartError(
|
|
24
|
+
'R103',
|
|
25
|
+
`\`${def.$id ?? 'a function'}\` has both \`run\` and \`impl\``,
|
|
26
|
+
'keep one: `impl` replaces the body',
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A server function that reads, with its output typed by a schema. */
|
|
31
|
+
export function query<
|
|
32
|
+
OS extends StandardSchemaV1,
|
|
33
|
+
S extends StandardSchemaV1 | undefined = undefined,
|
|
34
|
+
const A extends AuthScope | undefined = undefined,
|
|
35
|
+
>(def: ImplQueryDef<S, OS, A>): Query<In<S>, Out<OS>>
|
|
36
|
+
/** A server function that reads, with its output typed by `run`. */
|
|
37
|
+
export function query<
|
|
38
|
+
O,
|
|
39
|
+
S extends StandardSchemaV1 | undefined = undefined,
|
|
40
|
+
const A extends AuthScope | undefined = undefined,
|
|
41
|
+
>(def: QueryDef<S, O, A>): Query<In<S>, Awaited<O>>
|
|
42
|
+
/** A server function that reads: cached by input on the client, and tagged for what invalidates it. */
|
|
43
|
+
export function query(def: unknown): unknown {
|
|
44
|
+
const d = def as Fn['def'] & { $id?: string }
|
|
45
|
+
oneBody(d)
|
|
46
|
+
const id = d.$id ?? `q${nextAuto()}`
|
|
47
|
+
const fn: Fn = { kind: 'query', $id: id, def: d }
|
|
48
|
+
fns.set(id, fn)
|
|
49
|
+
const q = (input?: unknown) => use(fn, input)
|
|
50
|
+
q.fetch = (input?: unknown) => fetchQuery(fn, input)
|
|
51
|
+
q.shape = (input?: unknown) => shapeOf(fn, input)
|
|
52
|
+
q.live = (input?: unknown, options?: LiveOptions) => liveQuery(fn, input, options)
|
|
53
|
+
return Object.assign(q, { kind: 'query', $id: id, [FN]: fn })
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A server function that writes, with its output typed by a schema. */
|
|
57
|
+
export function mutation<
|
|
58
|
+
OS extends StandardSchemaV1,
|
|
59
|
+
S extends StandardSchemaV1 | undefined = undefined,
|
|
60
|
+
const A extends AuthScope | undefined = undefined,
|
|
61
|
+
>(def: ImplMutationDef<S, OS, A>): Mutation<In<S>, Out<OS>>
|
|
62
|
+
/** A server function that writes, with its output typed by `run`. */
|
|
63
|
+
export function mutation<
|
|
64
|
+
O,
|
|
65
|
+
S extends StandardSchemaV1 | undefined = undefined,
|
|
66
|
+
const A extends AuthScope | undefined = undefined,
|
|
67
|
+
>(def: MutationDef<S, O, A>): Mutation<In<S>, Awaited<O>>
|
|
68
|
+
/** A server function that writes, and invalidates the queries that read what it changed. */
|
|
69
|
+
export function mutation(def: unknown): unknown {
|
|
70
|
+
const d = def as Fn['def'] & { $id?: string }
|
|
71
|
+
oneBody(d)
|
|
72
|
+
const id = d.$id ?? `m${nextAuto()}`
|
|
73
|
+
const fn: Fn = { kind: 'mutation', $id: id, def: d }
|
|
74
|
+
fns.set(id, fn)
|
|
75
|
+
// Module-level boxes, read on the client only (a server render never shows a call in flight).
|
|
76
|
+
const { calls, error } = createRoot(
|
|
77
|
+
() => ({ calls: state(0), error: state<unknown>(undefined) }),
|
|
78
|
+
null,
|
|
79
|
+
)
|
|
80
|
+
const pending = createRoot(() => derived(() => calls.value > 0), null)
|
|
81
|
+
const m = (input?: unknown) => mutate(fn, input, calls, error)
|
|
82
|
+
return Object.assign(m, {
|
|
83
|
+
kind: 'mutation',
|
|
84
|
+
$id: id,
|
|
85
|
+
pending,
|
|
86
|
+
error: {
|
|
87
|
+
get value() {
|
|
88
|
+
return error.value
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
[FN]: fn,
|
|
92
|
+
[SUBMIT]: (input: unknown, form: true | FormData) => mutate(fn, input, calls, error, form),
|
|
93
|
+
})
|
|
94
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
let made = new WeakSet<object>()
|
|
2
|
+
export const resetMade = () => {
|
|
3
|
+
made = new WeakSet()
|
|
4
|
+
}
|
|
5
|
+
export const isMade = (value: object) => made.has(value)
|
|
6
|
+
|
|
7
|
+
type Obj = Record<PropertyKey, unknown>
|
|
8
|
+
|
|
9
|
+
const plain = (v: unknown): v is Obj =>
|
|
10
|
+
!!v &&
|
|
11
|
+
typeof v === 'object' &&
|
|
12
|
+
(Array.isArray(v) ||
|
|
13
|
+
Object.getPrototypeOf(v) === Object.prototype ||
|
|
14
|
+
Object.getPrototypeOf(v) === null)
|
|
15
|
+
|
|
16
|
+
/** `next`, with every subtree deep-equal to `prev`'s replaced by `prev`'s (TanStack's replaceEqualDeep). */
|
|
17
|
+
export function share<T>(prev: unknown, next: T): T {
|
|
18
|
+
if (prev === next || !plain(prev) || !plain(next) || Array.isArray(prev) !== Array.isArray(next))
|
|
19
|
+
return next
|
|
20
|
+
const keys = Object.keys(next)
|
|
21
|
+
let same = Object.keys(prev).length === keys.length
|
|
22
|
+
const out = (Array.isArray(next) ? [] : {}) as Obj
|
|
23
|
+
for (const k of keys) {
|
|
24
|
+
out[k] = share(prev[k], next[k])
|
|
25
|
+
if (out[k] !== prev[k]) same = false
|
|
26
|
+
}
|
|
27
|
+
return (same ? prev : out) as T
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface Draft {
|
|
31
|
+
b: Obj
|
|
32
|
+
c?: Obj
|
|
33
|
+
px: Obj
|
|
34
|
+
done?: Obj
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Run `recipe` on a copy-on-write draft of `base`: only objects it writes (and their parents) are copied. A recipe
|
|
39
|
+
* may instead return a new value. The copies are remembered for `cache.isOptimistic`.
|
|
40
|
+
*/
|
|
41
|
+
export function produce<T>(base: T, recipe: (draft: T) => T | void): T {
|
|
42
|
+
if (!plain(base)) {
|
|
43
|
+
const r = recipe(base)
|
|
44
|
+
return r === undefined ? base : r
|
|
45
|
+
}
|
|
46
|
+
const nodes = new Map<object, Draft>()
|
|
47
|
+
const byProxy = new WeakMap<object, Draft>()
|
|
48
|
+
const unwrap = (v: unknown) =>
|
|
49
|
+
v && typeof v === 'object' && byProxy.has(v) ? byProxy.get(v)!.b : v
|
|
50
|
+
const cur = (n: Draft) => n.c ?? n.b
|
|
51
|
+
const own = (n: Draft) => (n.c ??= (Array.isArray(n.b) ? n.b.slice() : { ...n.b }) as Obj)
|
|
52
|
+
const node = (b: Obj): Draft => {
|
|
53
|
+
let n = nodes.get(b)
|
|
54
|
+
if (n) return n
|
|
55
|
+
const self: Draft = (n = { b, px: b })
|
|
56
|
+
n.px = new Proxy((Array.isArray(b) ? [] : {}) as Obj, {
|
|
57
|
+
get: (_, k) => {
|
|
58
|
+
const v = cur(self)[k]
|
|
59
|
+
return plain(v) ? node(v).px : v
|
|
60
|
+
},
|
|
61
|
+
set: (_, k, v) => {
|
|
62
|
+
v = unwrap(v)
|
|
63
|
+
if (plain(v) && !nodes.has(v)) made.add(v) // a new object (a pushed row) is optimistic too
|
|
64
|
+
own(self)[k] = v
|
|
65
|
+
return true
|
|
66
|
+
},
|
|
67
|
+
deleteProperty: (_, k) => (delete own(self)[k], true),
|
|
68
|
+
has: (_, k) => k in cur(self),
|
|
69
|
+
ownKeys: () => Reflect.ownKeys(cur(self)),
|
|
70
|
+
getOwnPropertyDescriptor: (t, k) => {
|
|
71
|
+
const d = Reflect.getOwnPropertyDescriptor(cur(self), k)
|
|
72
|
+
if (d && k === 'length' && Array.isArray(t)) return { ...d, configurable: false }
|
|
73
|
+
return d && { ...d, configurable: true, value: self.px[k] }
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
nodes.set(b, n)
|
|
77
|
+
byProxy.set(n.px, n)
|
|
78
|
+
return n
|
|
79
|
+
}
|
|
80
|
+
// The result: copies where something changed, the original objects everywhere else.
|
|
81
|
+
const fin = (n: Draft): Obj => {
|
|
82
|
+
if (n.done !== undefined) return n.done
|
|
83
|
+
n.done = n.b // cycles stop here
|
|
84
|
+
const src = cur(n)
|
|
85
|
+
let out = n.c
|
|
86
|
+
for (const k of Object.keys(src)) {
|
|
87
|
+
const v = src[k]
|
|
88
|
+
if (!plain(v) || !nodes.has(v)) continue
|
|
89
|
+
const f = fin(nodes.get(v)!)
|
|
90
|
+
if (f !== v) (out ??= own(n))[k] = f
|
|
91
|
+
}
|
|
92
|
+
if (out) made.add(out)
|
|
93
|
+
return (n.done = out ?? n.b)
|
|
94
|
+
}
|
|
95
|
+
const deep = (v: unknown): unknown => {
|
|
96
|
+
if (v && typeof v === 'object' && byProxy.has(v)) return fin(byProxy.get(v)!)
|
|
97
|
+
if (Array.isArray(v)) return v.map(deep)
|
|
98
|
+
if (plain(v)) return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, deep(x)]))
|
|
99
|
+
return v
|
|
100
|
+
}
|
|
101
|
+
const root = node(base)
|
|
102
|
+
const r = recipe(root.px as T)
|
|
103
|
+
return (r === undefined || r === root.px ? fin(root) : deep(r)) as T
|
|
104
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createRoot, state, type Derived } from '@weft-js/core'
|
|
2
|
+
import { client } from '../router'
|
|
3
|
+
import type * as Exec from '../exec'
|
|
4
|
+
import type { Scope } from '../exec'
|
|
5
|
+
import type { Fn, LiveStatus, LiveMode, NetControl, Transport } from './types'
|
|
6
|
+
|
|
7
|
+
/** The typed "no query" input: `getTodo(() => (id.value ? { id: id.value } : skip))`. */
|
|
8
|
+
export const skip: unique symbol = Symbol.for('weft.skip') as never
|
|
9
|
+
/** The marker a live query's input takes to subscribe to nothing yet. */
|
|
10
|
+
export type Skip = typeof skip
|
|
11
|
+
|
|
12
|
+
/** Every definition by id (the server answers `/_weft/fn/<id>` from it). */
|
|
13
|
+
export const fns = new Map<string, Fn>()
|
|
14
|
+
let auto = 0
|
|
15
|
+
export const nextAuto = () => ++auto
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Set by the server half (server.tsx): the request in progress and the executor. `navigate` overrides the client
|
|
19
|
+
* router's (tests). Unset in plain tests.
|
|
20
|
+
*/
|
|
21
|
+
export const runtime: {
|
|
22
|
+
scope?: (() => Scope | undefined) | undefined
|
|
23
|
+
/** Run `fn` with `scope` current (the request's AsyncLocalStorage). */
|
|
24
|
+
enter?: (<T>(scope: unknown, fn: () => T) => T) | undefined
|
|
25
|
+
exec?: typeof Exec
|
|
26
|
+
transport?: Transport
|
|
27
|
+
navigate?: ((href: string, replace?: boolean) => void) | undefined
|
|
28
|
+
/** Set by the server's first request (and in dev when `drizzle/` grows): what each database applies before its first statement (db/core.ts). */
|
|
29
|
+
migrations?: { list: { tag: string; sql: string }[]; report(e: unknown): void } | undefined
|
|
30
|
+
/** 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. */
|
|
31
|
+
dbs?: Set<() => Promise<void> | undefined>
|
|
32
|
+
/** Fan invalidated tags out to live subscribers (hub.ts). `origin`: the connection that needs no poke. */
|
|
33
|
+
poke?: ((tags: string[], origin?: string | null) => Promise<void>) | undefined
|
|
34
|
+
/** This tab's realtime connection: sent with mutations, so the tab doesn't poke itself. */
|
|
35
|
+
conn?: string | undefined
|
|
36
|
+
/** `room.in(params).publish`: socket.ts, on a server or under the fake. */
|
|
37
|
+
channels?: {
|
|
38
|
+
publish(
|
|
39
|
+
fn: Fn,
|
|
40
|
+
params: unknown,
|
|
41
|
+
name: string,
|
|
42
|
+
message: unknown,
|
|
43
|
+
options?: { exceptSelf?: boolean; from?: string },
|
|
44
|
+
): Promise<void>
|
|
45
|
+
set(fn: Fn, key: unknown, value: unknown): Promise<void>
|
|
46
|
+
}
|
|
47
|
+
} = {}
|
|
48
|
+
|
|
49
|
+
/** The realtime connection's status and "this tab is older than the server" (research 30 §2). */
|
|
50
|
+
export const rt = createRoot(
|
|
51
|
+
() => ({
|
|
52
|
+
status: state<LiveStatus>('connecting'),
|
|
53
|
+
mode: state<LiveMode>('app'),
|
|
54
|
+
stale: state(false),
|
|
55
|
+
}),
|
|
56
|
+
null,
|
|
57
|
+
)
|
|
58
|
+
/** The server runs a different build than this tab loaded: offer a reload. */
|
|
59
|
+
export const stale: Derived<boolean> = rt.stale
|
|
60
|
+
|
|
61
|
+
export const go = (href: string, replace?: boolean) =>
|
|
62
|
+
runtime.navigate ? runtime.navigate(href, replace) : void client.navigate?.(href, { replace })
|
|
63
|
+
|
|
64
|
+
/** @weft-js/core/test's controls, when it's loaded; the first look registers the cache reset it runs after each test. */
|
|
65
|
+
export let registered = false
|
|
66
|
+
let reset = () => {}
|
|
67
|
+
export const configureReset = (fn: () => void) => {
|
|
68
|
+
reset = fn
|
|
69
|
+
}
|
|
70
|
+
/** The test controls, once `@weft-js/core/test` has registered them. */
|
|
71
|
+
export const net = () => {
|
|
72
|
+
const n = Reflect.get(globalThis, Symbol.for('weft.net')) as NetControl | undefined
|
|
73
|
+
if (n && !registered) {
|
|
74
|
+
registered = true
|
|
75
|
+
n.resets.push(() => {
|
|
76
|
+
reset()
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
return n
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const FN = Symbol.for('weft.fn')
|
|
83
|
+
/** `<Form>`'s call: a mutation handle's `m(input)` with the input marked as coming from a form. */
|
|
84
|
+
export const SUBMIT = Symbol.for('weft.submit')
|
|
85
|
+
/** The definition behind a query or mutation handle (also accepts the definition). */
|
|
86
|
+
export const fnOf = (h: unknown): Fn => ((h as Record<symbol, Fn> | null)?.[FN] ?? h) as Fn
|
|
87
|
+
|
|
88
|
+
// ---- server and client calls ---------------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
export const server = () => runtime.scope?.() ?? (net()?.als.getStore() as Scope | undefined)
|
|
91
|
+
/** The request scope in progress, on the server or under the fake transport. */
|
|
92
|
+
export const currentScope = server
|
|
93
|
+
/** Run `fn` with `scope` current. */
|
|
94
|
+
export const enter = <T>(scope: unknown, fn: () => T): T =>
|
|
95
|
+
runtime.enter ? runtime.enter(scope, fn) : net() ? net()!.als.run(scope, fn) : fn()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { cache } from './cache'
|
|
2
|
+
import { currentScope, fns, net, rt, runtime, skip } from './foundation'
|
|
3
|
+
import { FnError, keyOf, StartError } from './primitives'
|
|
4
|
+
import { fakeScope, track } from './transport'
|
|
5
|
+
|
|
6
|
+
/** What `live.ts` reads from here; see its header. */
|
|
7
|
+
export const liveApi = {
|
|
8
|
+
cache,
|
|
9
|
+
currentScope,
|
|
10
|
+
fakeScope,
|
|
11
|
+
fns,
|
|
12
|
+
FnError,
|
|
13
|
+
keyOf,
|
|
14
|
+
net,
|
|
15
|
+
rt,
|
|
16
|
+
runtime,
|
|
17
|
+
skip,
|
|
18
|
+
StartError,
|
|
19
|
+
track,
|
|
20
|
+
}
|
|
21
|
+
/** What `live.ts` is handed, instead of a static import that would close a cycle. */
|
|
22
|
+
export type LiveApi = typeof liveApi
|