@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
package/src/exec.ts
CHANGED
|
@@ -9,125 +9,74 @@
|
|
|
9
9
|
// bumps the versions of the tags it invalidates).
|
|
10
10
|
|
|
11
11
|
import { shapeTree } from './shape.mjs'
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** Each extension's part of `ctx`, once the app's is there: an extension reads `ctx.user` to say what it adds. */
|
|
82
|
-
function extended(ctx: Context, options: ScopeOptions): Context {
|
|
83
|
-
for (const e of options.extensions ?? []) Object.assign(ctx, e.context?.(ctx))
|
|
84
|
-
return ctx
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** `ctx.media`. A refusal is the user's answer, with its own status; `<Form>` shows it. */
|
|
88
|
-
const mediaApi = (options: ScopeOptions): Context['media'] => ({
|
|
89
|
-
async ingest(file, { name } = {}) {
|
|
90
|
-
const server = await options.media?.()
|
|
91
|
-
if (!server) throw new StartError('M017', '`ctx.media` needs a `media.server` provider', 'add `media()` from `@weft-js/plugin-media` to `plugins` in weft.config.ts', { status: 500 })
|
|
92
|
-
const bytes = file instanceof Uint8Array ? file : new Uint8Array(await file.arrayBuffer())
|
|
93
|
-
try {
|
|
94
|
-
return await server.ingest(bytes, { name: name ?? (file as File).name })
|
|
95
|
-
} catch (e) {
|
|
96
|
-
const { code, message } = e as Error & { code?: string }
|
|
97
|
-
throw code ? new StartError(code, message, undefined, { status: 422 }) : e
|
|
98
|
-
}
|
|
99
|
-
},
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
/** A child scope for one loader: the same entries, its own `tags` collector. */
|
|
103
|
-
export const frame = (scope: Scope): Scope => Object.create(scope, { tags: { value: new Set<string>() } })
|
|
104
|
-
|
|
105
|
-
// ---- the shared tagged cache --------------------------------------------------------------------------------------
|
|
106
|
-
|
|
107
|
-
/** Where `cache: { life }` values live. Memory by default; Nitro storage (fs, KV, Redis) plugs in here. */
|
|
108
|
-
export interface Store {
|
|
109
|
-
get(key: string): Stored | undefined | Promise<Stored | undefined>
|
|
110
|
-
set(key: string, value: Stored): void | Promise<void>
|
|
111
|
-
/** The current version of each tag (0 for one never bumped). */
|
|
112
|
-
versions(tags: string[]): number[] | Promise<number[]>
|
|
113
|
-
bump(tags: string[]): void | Promise<void>
|
|
12
|
+
import {
|
|
13
|
+
enter,
|
|
14
|
+
fns,
|
|
15
|
+
isRedirect,
|
|
16
|
+
keyOf,
|
|
17
|
+
runtime,
|
|
18
|
+
StartError,
|
|
19
|
+
type Answer,
|
|
20
|
+
type Context,
|
|
21
|
+
type Fn,
|
|
22
|
+
type Wire,
|
|
23
|
+
type Life,
|
|
24
|
+
} from './data'
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
createScope,
|
|
28
|
+
frame,
|
|
29
|
+
memoryStore,
|
|
30
|
+
defaultStore,
|
|
31
|
+
type Scope,
|
|
32
|
+
type ScopeEntry,
|
|
33
|
+
type Touched,
|
|
34
|
+
} from './exec/scope'
|
|
35
|
+
import { guard, dbError } from './exec/errors'
|
|
36
|
+
import {
|
|
37
|
+
tracker,
|
|
38
|
+
untouched,
|
|
39
|
+
missedTags,
|
|
40
|
+
reads,
|
|
41
|
+
liveQueries,
|
|
42
|
+
missedPokes,
|
|
43
|
+
missedFeed,
|
|
44
|
+
} from './exec/touch'
|
|
45
|
+
import { validate, formInput, checked, checkTags } from './exec/validation'
|
|
46
|
+
import { landed, shown, said } from './exec/landing'
|
|
47
|
+
export {
|
|
48
|
+
createScope,
|
|
49
|
+
frame,
|
|
50
|
+
memoryStore,
|
|
51
|
+
defaultStore,
|
|
52
|
+
guard,
|
|
53
|
+
dbError,
|
|
54
|
+
liveQueries,
|
|
55
|
+
missedFeed,
|
|
56
|
+
validate,
|
|
57
|
+
checkTags,
|
|
58
|
+
shown,
|
|
59
|
+
said,
|
|
60
|
+
}
|
|
61
|
+
export type {
|
|
62
|
+
Scope,
|
|
63
|
+
ScopeEntry,
|
|
64
|
+
ScopeOptions,
|
|
65
|
+
Touched,
|
|
66
|
+
FormFailure,
|
|
67
|
+
MediaServer,
|
|
68
|
+
Store,
|
|
69
|
+
Stored,
|
|
70
|
+
} from './exec/scope'
|
|
71
|
+
|
|
72
|
+
const LIFE: Record<Exclude<Life, number>, number> = {
|
|
73
|
+
seconds: 1,
|
|
74
|
+
minutes: 60,
|
|
75
|
+
hours: 3600,
|
|
76
|
+
days: 86_400,
|
|
77
|
+
max: Infinity,
|
|
114
78
|
}
|
|
115
|
-
export interface Stored { v: unknown; tags: string[]; versions: number[]; expires: number }
|
|
116
|
-
|
|
117
|
-
export function memoryStore(): Store {
|
|
118
|
-
const values = new Map<string, Stored>()
|
|
119
|
-
const tags = new Map<string, number>()
|
|
120
|
-
return {
|
|
121
|
-
get: k => values.get(k),
|
|
122
|
-
set: (k, v) => void values.set(k, v),
|
|
123
|
-
versions: t => t.map(x => tags.get(x) ?? 0),
|
|
124
|
-
bump: t => { for (const x of t) tags.set(x, (tags.get(x) ?? 0) + 1) },
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const LIFE: Record<Exclude<Life, number>, number> = { seconds: 1, minutes: 60, hours: 3600, days: 86_400, max: Infinity }
|
|
129
79
|
const seconds = (life: Life = 'minutes') => (typeof life === 'number' ? life : LIFE[life])
|
|
130
|
-
export const defaultStore = memoryStore()
|
|
131
80
|
const storeOf = (scope: Scope) => scope.options.store ?? defaultStore
|
|
132
81
|
|
|
133
82
|
// ---- queries --------------------------------------------------------------------------------------------------
|
|
@@ -135,8 +84,8 @@ const storeOf = (scope: Scope) => scope.options.store ?? defaultStore
|
|
|
135
84
|
/** The value now if this request already has it, else the promise: what a query resource's fetcher returns in SSR. */
|
|
136
85
|
export function readQuery(scope: Scope, fn: Fn, input: unknown): unknown {
|
|
137
86
|
const e = fetchEntry(scope, fn, input)
|
|
138
|
-
if (!e.done) return
|
|
139
|
-
if (e.e !== undefined) throw e.e
|
|
87
|
+
if (!e.done) return landed(scope, e)
|
|
88
|
+
if (e.e !== undefined) throw shown(e.e, scope)
|
|
140
89
|
return e.v
|
|
141
90
|
}
|
|
142
91
|
|
|
@@ -151,11 +100,16 @@ export function shapeOf(fn: Fn): unknown {
|
|
|
151
100
|
export const fetchQuery = (scope: Scope, fn: Fn, input: unknown): Promise<ScopeEntry> => {
|
|
152
101
|
const e = fetchEntry(scope, fn, input)
|
|
153
102
|
const tags = scope.tags
|
|
154
|
-
return e.p.then(x => (tags && x.tags.forEach(t => tags.add(t)), x))
|
|
103
|
+
return e.p.then((x) => (tags && x.tags.forEach((t) => tags.add(t)), x))
|
|
155
104
|
}
|
|
156
105
|
|
|
157
106
|
function fetchEntry(scope: Scope, fn: Fn, input: unknown): ScopeEntry {
|
|
158
|
-
if (fn.kind !== 'query')
|
|
107
|
+
if (fn.kind !== 'query')
|
|
108
|
+
throw new StartError(
|
|
109
|
+
'Q009',
|
|
110
|
+
`\`${fn.$id}\` is a mutation, not a query`,
|
|
111
|
+
'call it as a function, from a handler',
|
|
112
|
+
)
|
|
159
113
|
const key = keyOf(fn.$id, input)
|
|
160
114
|
const e = scope.entries.get(key)
|
|
161
115
|
if (e) return e
|
|
@@ -168,17 +122,23 @@ function fetchEntry(scope: Scope, fn: Fn, input: unknown): ScopeEntry {
|
|
|
168
122
|
// Checked before the shared cache, so a value stored for one user is never answered to no one.
|
|
169
123
|
const early = fn.def.auth || cached?.scope ? await scope.ctx() : undefined
|
|
170
124
|
if (early) guard(fn, cached ? shared(early, fn, scope) : early)
|
|
171
|
-
const stored = cached?.scope
|
|
125
|
+
const stored = cached?.scope
|
|
126
|
+
? `${key}@${String((early as { user?: { id?: string | number } } | undefined)?.user?.id ?? '')}`
|
|
127
|
+
: key
|
|
172
128
|
if (cached) {
|
|
173
129
|
const hit = await store.get(stored)
|
|
174
|
-
if (hit && hit.expires > Date.now() && same(hit.versions, await store.versions(hit.tags)))
|
|
130
|
+
if (hit && hit.expires > Date.now() && same(hit.versions, await store.versions(hit.tags)))
|
|
131
|
+
return hit.v
|
|
175
132
|
}
|
|
176
133
|
const versions = cached ? await store.versions(entry.tags) : []
|
|
177
134
|
const ctx = early ?? (await scope.ctx())
|
|
178
135
|
const touched = tracker(scope)
|
|
179
136
|
const added: string[] = []
|
|
180
|
-
const v = await call(scope, fn, valid, cached ? shared(ctx, fn, scope) : ctx,
|
|
181
|
-
|
|
137
|
+
const v = await call(scope, fn, valid, cached ? shared(ctx, fn, scope) : ctx, {
|
|
138
|
+
touched,
|
|
139
|
+
added,
|
|
140
|
+
})
|
|
141
|
+
const fresh = [...new Set(added)].filter((t) => !entry.tags.includes(t))
|
|
182
142
|
if (fresh.length) {
|
|
183
143
|
entry.tags.push(...fresh)
|
|
184
144
|
if (cached) versions.push(...(await store.versions(fresh)))
|
|
@@ -189,15 +149,25 @@ function fetchEntry(scope: Scope, fn: Fn, input: unknown): ScopeEntry {
|
|
|
189
149
|
missedTags(fn, 'read', touched.read, entry.tags, scope)
|
|
190
150
|
reads.set(fn.$id, touched.read)
|
|
191
151
|
}
|
|
192
|
-
if (cached)
|
|
152
|
+
if (cached)
|
|
153
|
+
await store.set(stored, {
|
|
154
|
+
v,
|
|
155
|
+
tags: entry.tags,
|
|
156
|
+
versions,
|
|
157
|
+
expires: Date.now() + seconds(cached.life) * 1000,
|
|
158
|
+
})
|
|
193
159
|
return v
|
|
194
160
|
})().then(
|
|
195
|
-
v => Object.assign(entry, { done: true, v }),
|
|
196
|
-
err => {
|
|
161
|
+
(v) => Object.assign(entry, { done: true, v }),
|
|
162
|
+
(err) => {
|
|
197
163
|
if (isRedirect(err) && scope.started) {
|
|
198
164
|
const message = `\`${fn.$id}\` called \`redirect('${err.href}')\` after the page started streaming: the status is already sent`
|
|
199
165
|
scope.options.onDiagnostic?.({ code: 'Q007', severity: 'error', message })
|
|
200
|
-
err = new StartError(
|
|
166
|
+
err = new StartError(
|
|
167
|
+
'Q007',
|
|
168
|
+
message,
|
|
169
|
+
`call \`${fn.$id.replace(/\..*$/, '')}.fetch()\` in the route's \`load\`, which runs before the first byte`,
|
|
170
|
+
)
|
|
201
171
|
}
|
|
202
172
|
Object.assign(entry, { done: true, e: err })
|
|
203
173
|
throw err
|
|
@@ -221,34 +191,78 @@ export async function tagsOf(fn: Fn, input: unknown, scope: Pick<Scope, 'ctx'>):
|
|
|
221
191
|
const same = (a: number[], b: number[]) => a.length === b.length && a.every((x, i) => x === b[i])
|
|
222
192
|
|
|
223
193
|
/** A body in another process (research 36): what it adds to the tags and reports it touched arrive with the value. */
|
|
224
|
-
async function callImpl(
|
|
225
|
-
|
|
194
|
+
async function callImpl(
|
|
195
|
+
fn: Fn,
|
|
196
|
+
input: unknown,
|
|
197
|
+
ctx: Context,
|
|
198
|
+
scope: Scope,
|
|
199
|
+
track: { touched?: Touched | undefined; added: string[] },
|
|
200
|
+
) {
|
|
201
|
+
const { touched, added } = track
|
|
202
|
+
const {
|
|
203
|
+
value,
|
|
204
|
+
tags,
|
|
205
|
+
touched: tables,
|
|
206
|
+
} = await fn.def.impl!.call({ fn, input, ctx, request: scope.request })
|
|
226
207
|
added.push(...tags)
|
|
227
208
|
if (touched) for (const t of tables) touched[fn.kind === 'query' ? 'read' : 'write'].add(t)
|
|
228
209
|
return value
|
|
229
210
|
}
|
|
230
211
|
|
|
231
|
-
const OWN = new Set([
|
|
212
|
+
const OWN = new Set([
|
|
213
|
+
'$id',
|
|
214
|
+
'auth',
|
|
215
|
+
'cache',
|
|
216
|
+
'errors',
|
|
217
|
+
'impl',
|
|
218
|
+
'input',
|
|
219
|
+
'invalidates',
|
|
220
|
+
'live',
|
|
221
|
+
'optimistic',
|
|
222
|
+
'output',
|
|
223
|
+
'run',
|
|
224
|
+
'shape',
|
|
225
|
+
'tags',
|
|
226
|
+
])
|
|
232
227
|
|
|
233
228
|
/** An option from `FnOptions` is a promise to enforce something: with the plugin gone, the call stops rather than runs open. */
|
|
234
229
|
function claimed(fn: Fn, scope: Scope) {
|
|
235
230
|
for (const k in fn.def) {
|
|
236
|
-
if (OWN.has(k) || scope.options.extensions?.some(e => e.options?.includes(k))) continue
|
|
237
|
-
throw new StartError(
|
|
231
|
+
if (OWN.has(k) || scope.options.extensions?.some((e) => e.options?.includes(k))) continue
|
|
232
|
+
throw new StartError(
|
|
233
|
+
'P101',
|
|
234
|
+
`\`${fn.$id}\` sets \`${k}\`, and no \`server.extension\` enforces it`,
|
|
235
|
+
'add the plugin that provides it to `plugins`, with its `src/<key>.server.ts`',
|
|
236
|
+
{ status: 500 },
|
|
237
|
+
)
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
async function call(
|
|
241
|
+
async function call(
|
|
242
|
+
scope: Scope,
|
|
243
|
+
fn: Fn,
|
|
244
|
+
input: unknown,
|
|
245
|
+
ctx: Context,
|
|
246
|
+
track: { touched?: Touched | undefined; added?: string[] } = {},
|
|
247
|
+
) {
|
|
248
|
+
const { touched } = track
|
|
249
|
+
const added = track.added ?? []
|
|
242
250
|
guard(fn, ctx)
|
|
243
251
|
claimed(fn, scope)
|
|
244
252
|
try {
|
|
245
|
-
for (const e of scope.options.extensions ?? [])
|
|
253
|
+
for (const e of scope.options.extensions ?? [])
|
|
254
|
+
await e.guard?.({ id: fn.$id, kind: fn.kind, def: fn.def }, ctx)
|
|
246
255
|
} catch (e) {
|
|
247
256
|
throw dbError(e)
|
|
248
257
|
}
|
|
249
258
|
const replaced = scope.options.handlers?.get(fn.$id)
|
|
250
259
|
// Awaited inside the frame: a Drizzle query is a thenable, and only runs when something awaits it.
|
|
251
|
-
const run = async () =>
|
|
260
|
+
const run = async () =>
|
|
261
|
+
await (replaced
|
|
262
|
+
? replaced(input, ctx)
|
|
263
|
+
: fn.def.impl
|
|
264
|
+
? callImpl(fn, input, ctx, scope, { touched, added })
|
|
265
|
+
: fn.def.run!({ input, ctx }))
|
|
252
266
|
let v: unknown
|
|
253
267
|
try {
|
|
254
268
|
v = await (touched ? enter(Object.create(scope, { touched: { value: touched } }), run) : run())
|
|
@@ -260,87 +274,19 @@ async function call(scope: Scope, fn: Fn, input: unknown, ctx: Context, touched?
|
|
|
260
274
|
return v
|
|
261
275
|
}
|
|
262
276
|
|
|
263
|
-
/** `auth: 'user'`: A003 when `ctx` has no `user` at all, A002 when no one is signed in. */
|
|
264
|
-
export function guard(fn: Fn, ctx: Context) {
|
|
265
|
-
if (fn.def.auth !== 'user') return
|
|
266
|
-
if (!('user' in ctx)) throw new StartError('A003', `\`${fn.$id}\` has \`auth: 'user'\`, but \`ctx\` has no \`user\``, 'spread `session(auth, request)` into what `src/context.server.ts` returns', { status: 500 })
|
|
267
|
-
if (!(ctx as { user?: unknown }).user) throw new StartError('A002', `\`${fn.$id}\` needs a signed-in user`, 'sign in first, or guard the route with `redirect()` in its `load`', { status: 401 })
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const DOWN = /^(ECONNREFUSED|ECONNRESET|ENOTFOUND|EHOSTUNREACH|ETIMEDOUT|CONNECT_TIMEOUT|CONNECTION_(CLOSED|ENDED|DESTROYED)|PROTOCOL_CONNECTION_LOST|P100[12])$/
|
|
271
|
-
|
|
272
|
-
interface Failure { cause?: Failure | null; answer?: { message?: string; code?: string; status: number }; code?: string | number; message?: string }
|
|
273
|
-
|
|
274
|
-
/** A dropped connection is Q016; a missing table is a migration that wasn't generated or applied (Q014): the driver's error, with the fix. */
|
|
275
|
-
export function dbError(e: unknown): unknown {
|
|
276
|
-
const failure = e as Failure | null | undefined
|
|
277
|
-
const cause = failure?.cause ?? failure
|
|
278
|
-
if (failure?.answer) return new FnError(failure.answer, failure.answer.status)
|
|
279
|
-
if (cause instanceof StartError) return cause // Drizzle wraps what a driver throws, the gate's Q016 included
|
|
280
|
-
if (!(e instanceof StartError) && DOWN.test(String(cause?.code ?? failure?.code ?? ''))) return unavailable(e)
|
|
281
|
-
const m = cause?.message ?? ''
|
|
282
|
-
if (e instanceof StartError || !(/no such (table|column)|has no column named/.test(m) || cause?.code === '42P01' || cause?.code === '42703')) return e
|
|
283
|
-
return new StartError('Q014', `the database doesn't match the schema: ${m}`, 'run `weft db generate`, then reload (new migrations apply on the next request)', { status: 500 })
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
type Obj = Record<PropertyKey, unknown>
|
|
287
|
-
|
|
288
|
-
const tracker = (scope: Scope): Touched | undefined => (scope.options.dev ? { read: new Set(), write: new Set() } : undefined)
|
|
289
|
-
|
|
290
|
-
/** R105 (dev, once per function): a cached Rust query with tags that named no table, so Q011 has nothing to check it by. */
|
|
291
|
-
function untouched(fn: Fn, read: Set<string>, tags: readonly string[], scope: Scope) {
|
|
292
|
-
if (!fn.def.impl || !fn.def.cache || read.size || !tags.length || warned.has(`${fn.$id}:R105`)) return
|
|
293
|
-
warned.add(`${fn.$id}:R105`)
|
|
294
|
-
scope.options.onDiagnostic?.({ code: 'R105', severity: 'warning', message: `\`${fn.$id}\` is cached under ${tags.map(t => `\`${t}\``).join(', ')} and reported no table: nothing can tell whether those tags cover what it reads. Call \`ctx.touch("table")\` in the Rust, or drop \`tags\` if it reads none` })
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/** Q011 (dev, once per function and table): a table the function touched that no tag names (PLAN §4). */
|
|
298
|
-
function missedTags(fn: Fn, verb: 'read' | 'write', tables: Set<string>, tags: readonly string[], scope: Scope) {
|
|
299
|
-
for (const t of tables) {
|
|
300
|
-
const key = `${fn.$id}:${t}`
|
|
301
|
-
if (warned.has(key) || tags.some(x => x === t || x.startsWith(`${t}:`))) continue
|
|
302
|
-
warned.add(key)
|
|
303
|
-
const list = fn.kind === 'query' ? '`tags`' : '`invalidates`'
|
|
304
|
-
scope.options.onDiagnostic?.({ code: 'Q011', severity: 'warning', message: `\`${fn.$id}\` ${verb === 'read' ? 'reads' : 'writes'} table \`${t}\`, but its ${list} don't name it: ${fn.kind === 'query' ? 'a mutation of that table won\'t refresh it' : 'queries of that table stay stale'}. Add \`'${t}'\` to ${list}` })
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
const warned = new Set<string>()
|
|
308
|
-
|
|
309
|
-
/** Dev: the tables each query last read, and the tags of the queries read with `.live()` (the hub fills it). */
|
|
310
|
-
const reads = new Map<string, Set<string>>()
|
|
311
|
-
export const liveQueries = new Map<string, string[]>()
|
|
312
|
-
|
|
313
|
-
/** R003 (dev, once per pair): this mutation wrote what a live query reads, and pokes none of its tags. */
|
|
314
|
-
function missedPokes(fn: Fn, write: Set<string>, tags: string[], scope: Scope) {
|
|
315
|
-
for (const [id, own] of liveQueries) {
|
|
316
|
-
const table = [...(reads.get(id) ?? [])].find(t => write.has(t))
|
|
317
|
-
const key = `${fn.$id}>${id}`
|
|
318
|
-
if (!table || warned.has(key) || own.some(t => tags.includes(t))) continue
|
|
319
|
-
warned.add(key)
|
|
320
|
-
scope.options.onDiagnostic?.({ code: 'R003', severity: 'warning', message: `\`${fn.$id}\` writes table \`${table}\`, which live query \`${id}\` reads, but their tags don't intersect (${JSON.stringify(tags)} and ${JSON.stringify(own)}): open pages won't update. Add one of the query's tags to \`invalidates\`` })
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/** R012 (dev, once per query and table): a live query whose tags won't hear what `changes` reports, or a table it doesn't report. */
|
|
325
|
-
export function missedFeed(fn: Fn, tags: string[], tables: string[], report: ScopeOptions['onDiagnostic']) {
|
|
326
|
-
for (const t of reads.get(fn.$id) ?? []) {
|
|
327
|
-
const key = `${fn.$id}~${t}`
|
|
328
|
-
const named = tags.some(x => x === t || x.startsWith(`${t}:`))
|
|
329
|
-
if (warned.has(key) || (named && tables.includes(t))) continue
|
|
330
|
-
warned.add(key)
|
|
331
|
-
report?.({ code: 'R012', severity: 'warning', message: tables.includes(t) ? `live query \`${fn.$id}\` reads table \`${t}\`, but no tag names it: writes from outside the app won't refresh it. Add \`'${t}'\` to its \`tags\`` : `live query \`${fn.$id}\` reads table \`${t}\`, which \`changes\` doesn't list: only mutations refresh it, not writes from outside the app. Add the table to \`dbChanges\`` })
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
277
|
/** A cached query's ctx: no request, response or user. Its value is shared by every user (Q006). With `scope: 'user'`, one value per user: `ctx.user` is allowed. */
|
|
336
278
|
function shared(ctx: Context, fn: Fn, scope: Scope): Context {
|
|
337
279
|
const own = fn.def.cache?.scope === 'user'
|
|
338
280
|
return new Proxy(ctx, {
|
|
339
281
|
get(t, k, r) {
|
|
340
|
-
const personal = scope.options.extensions?.some(e => e.personal?.includes(String(k)))
|
|
282
|
+
const personal = scope.options.extensions?.some((e) => e.personal?.includes(String(k)))
|
|
341
283
|
if (own && (k === 'user' || k === 'session' || personal)) return Reflect.get(t, k, r)
|
|
342
284
|
if (k === 'request' || k === 'response' || k === 'user' || k === 'session' || personal)
|
|
343
|
-
throw new StartError(
|
|
285
|
+
throw new StartError(
|
|
286
|
+
'Q006',
|
|
287
|
+
`\`${fn.$id}\` is cached (\`cache: { life }\`) and reads \`ctx.${String(k)}\`: its value would be shared by every user`,
|
|
288
|
+
'drop `cache`, or pass the value as input',
|
|
289
|
+
)
|
|
344
290
|
return Reflect.get(t, k, r)
|
|
345
291
|
},
|
|
346
292
|
})
|
|
@@ -349,16 +295,35 @@ function shared(ctx: Context, fn: Fn, scope: Scope): Context {
|
|
|
349
295
|
// ---- mutations --------------------------------------------------------------------------------------------------
|
|
350
296
|
|
|
351
297
|
/** Run a mutation; then refresh the listed queries whose tags it invalidated (single-flight, research 27 §3). */
|
|
352
|
-
export async function runMutation(
|
|
353
|
-
|
|
298
|
+
export async function runMutation(
|
|
299
|
+
scope: Scope,
|
|
300
|
+
fn: Fn,
|
|
301
|
+
input: unknown,
|
|
302
|
+
refresh: [string, unknown][],
|
|
303
|
+
form?: boolean,
|
|
304
|
+
): Promise<Answer> {
|
|
305
|
+
if (fn.kind !== 'mutation')
|
|
306
|
+
throw new StartError(
|
|
307
|
+
'Q009',
|
|
308
|
+
`\`${fn.$id}\` is a query, not a mutation`,
|
|
309
|
+
`\`${fn.$id.replace(/\..*$/, '')}.fetch(input)\``,
|
|
310
|
+
)
|
|
354
311
|
if (scope.request.method === 'GET' || scope.request.method === 'HEAD')
|
|
355
|
-
throw new StartError(
|
|
312
|
+
throw new StartError(
|
|
313
|
+
'Q004',
|
|
314
|
+
`mutation \`${fn.$id}\` called while the server handles a GET (\`load\`, a page render or a query)`,
|
|
315
|
+
'call mutations from handlers or other mutations; GET handlers must be side-effect free',
|
|
316
|
+
{ status: 500 },
|
|
317
|
+
)
|
|
356
318
|
const valid = await (form ? formInput(fn, input) : validate(fn, input))
|
|
357
319
|
const touched = tracker(scope)
|
|
358
320
|
const added: string[] = []
|
|
359
|
-
const v = await call(scope, fn, valid, await scope.ctx(), touched, added)
|
|
321
|
+
const v = await call(scope, fn, valid, await scope.ctx(), { touched, added })
|
|
360
322
|
if (scope.options.dev) await checked(v, fn, scope)
|
|
361
|
-
const declared =
|
|
323
|
+
const declared =
|
|
324
|
+
typeof fn.def.invalidates === 'function'
|
|
325
|
+
? fn.def.invalidates(valid, v)
|
|
326
|
+
: (fn.def.invalidates ?? [])
|
|
362
327
|
await invalidate(scope, [...declared, ...added])
|
|
363
328
|
const tags = [...scope.invalidated]
|
|
364
329
|
if (touched) {
|
|
@@ -376,7 +341,7 @@ export async function runMutation(scope: Scope, fn: Fn, input: unknown, refresh:
|
|
|
376
341
|
} catch {
|
|
377
342
|
continue
|
|
378
343
|
}
|
|
379
|
-
if (!qtags.some(t => tags.includes(t))) continue
|
|
344
|
+
if (!qtags.some((t) => tags.includes(t))) continue
|
|
380
345
|
try {
|
|
381
346
|
const e = await fetchQuery(scope, q, inp)
|
|
382
347
|
c.push([id, inp, e.v, e.tags])
|
|
@@ -389,131 +354,16 @@ export async function runMutation(scope: Scope, fn: Fn, input: unknown, refresh:
|
|
|
389
354
|
export async function invalidate(scope: Scope, tags: string[]) {
|
|
390
355
|
if (!tags.length) return
|
|
391
356
|
for (const t of tags) scope.invalidated.add(t)
|
|
392
|
-
for (const [k, e] of scope.entries)
|
|
357
|
+
for (const [k, e] of scope.entries)
|
|
358
|
+
if (e.tags.some((t) => tags.includes(t))) scope.entries.delete(k)
|
|
393
359
|
await storeOf(scope).bump(tags)
|
|
394
360
|
// After the bump, and after `run`: a refetch reads committed data and misses the shared cache (research 30 §5).
|
|
395
361
|
// A bus that's down costs open pages a refresh, not this request its result.
|
|
396
|
-
await runtime.poke?.(tags, scope.request.headers.get('x-weft-conn')).catch(e =>
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
if (!schema) return input
|
|
404
|
-
const r = await schema['~standard'].validate(input)
|
|
405
|
-
if (!r.issues) return r.value
|
|
406
|
-
throw rejected(fn, r.issues)
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
const plainPath = (i: SchemaIssue) => (i.path ?? []).map(p => (typeof p === 'object' ? p.key : p))
|
|
410
|
-
|
|
411
|
-
function rejected(fn: Fn, list: readonly SchemaIssue[]) {
|
|
412
|
-
const issues = list.map(i => ({ message: i.message, path: i.path && plainPath(i) })) as (SchemaIssue & { path?: PropertyKey[] })[]
|
|
413
|
-
const where = issues.map(i => `${i.path?.length ? i.path.map(String).join('.') + ': ' : ''}${i.message}`).join('; ')
|
|
414
|
-
return new StartError('Q002', `\`${fn.$id}\` rejected its input: ${where}`, 'pass what the schema accepts', { status: 400, issues })
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/** A form's string as the type it may stand for (research 29 §5): the schema says whether it was wrong. */
|
|
418
|
-
function reading(s: string): unknown {
|
|
419
|
-
if (s === '') return undefined
|
|
420
|
-
if (s === 'on' || s === 'true') return true
|
|
421
|
-
if (s === 'false') return false
|
|
422
|
-
return /^-?\d+(\.\d+)?$/.test(s) ? Number(s) : s
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
/**
|
|
426
|
-
* A form sends strings and a schema may want numbers or booleans, and Standard Schema can't say which: each issue
|
|
427
|
-
* on a string tries its obvious reading, then the input is validated again. Fields a reading fixed report nothing; the
|
|
428
|
-
* rest report what the schema says of what was typed (an empty field: its "required", not "expected a string").
|
|
429
|
-
*/
|
|
430
|
-
async function formInput(fn: Fn, input: unknown): Promise<unknown> {
|
|
431
|
-
const schema = fn.def.input
|
|
432
|
-
if (!schema || !input || typeof input !== 'object') return validate(fn, input)
|
|
433
|
-
const value: Obj = { ...input }
|
|
434
|
-
let left: readonly SchemaIssue[] = []
|
|
435
|
-
for (let i = 0; i < 3; i++) {
|
|
436
|
-
const r = await schema['~standard'].validate(value)
|
|
437
|
-
if (!r.issues) return r.value
|
|
438
|
-
left = r.issues
|
|
439
|
-
let changed = false
|
|
440
|
-
for (const issue of r.issues) {
|
|
441
|
-
const path = plainPath(issue)
|
|
442
|
-
if (!path.length) continue
|
|
443
|
-
let at: Obj | undefined = value
|
|
444
|
-
for (const p of path.slice(0, -1)) {
|
|
445
|
-
const inner: unknown = at?.[p]
|
|
446
|
-
if (!inner || typeof inner !== 'object') { at = undefined; break }
|
|
447
|
-
at = at[p] = (Array.isArray(inner) ? [...inner] : { ...inner }) as Obj
|
|
448
|
-
}
|
|
449
|
-
const k = path.at(-1)!
|
|
450
|
-
const typed = at?.[k]
|
|
451
|
-
if (typeof typed !== 'string') continue
|
|
452
|
-
const next = reading(typed)
|
|
453
|
-
if (next === typed) continue
|
|
454
|
-
if (next === undefined) delete at![k]
|
|
455
|
-
else at![k] = next
|
|
456
|
-
changed = true
|
|
457
|
-
}
|
|
458
|
-
if (!changed) break
|
|
459
|
-
}
|
|
460
|
-
const first = await schema['~standard'].validate(input)
|
|
461
|
-
const where = (i: SchemaIssue) => JSON.stringify(plainPath(i))
|
|
462
|
-
throw rejected(fn, left.map(i => first.issues?.find(o => where(o) === where(i)) ?? i))
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/** Dev and tests: the value against `output` (C002), and what JSON would change (Q005). */
|
|
466
|
-
async function checked(v: unknown, fn: Fn, scope: Scope) {
|
|
467
|
-
serializable(v, fn, scope)
|
|
468
|
-
const schema = fn.def.output
|
|
469
|
-
if (!schema || fn.def.impl) return // `impl`: rust.ts checked what came back, on every call
|
|
470
|
-
const r = await schema['~standard'].validate(v)
|
|
471
|
-
if (r.issues) throw new StartError('C002', `\`${fn.$id}\` returned a value its \`output\` schema rejects: ${r.issues.map(i => `${i.path?.map(p => (typeof p === 'object' ? p.key : p)).join('.') ?? ''}: ${i.message}`).join('; ')}`, 'fix `run`, or the schema', { status: 500 })
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/** Q005 (dev): what JSON would change on the way to the client. */
|
|
475
|
-
function serializable(v: unknown, fn: Fn, scope: Scope) {
|
|
476
|
-
const bad = (x: unknown, path: string): string | undefined => {
|
|
477
|
-
if (x === null || typeof x === 'string' || typeof x === 'boolean') return undefined
|
|
478
|
-
if (typeof x === 'number') return Number.isFinite(x) ? undefined : `${path} is ${x}`
|
|
479
|
-
if (x === undefined) return undefined
|
|
480
|
-
if (typeof x !== 'object') return `${path} is a ${typeof x}`
|
|
481
|
-
if (Array.isArray(x)) {
|
|
482
|
-
for (let i = 0; i < x.length; i++) {
|
|
483
|
-
if (x[i] === undefined) return `${path}[${i}] is undefined (JSON sends null)`
|
|
484
|
-
const b = bad(x[i], `${path}[${i}]`)
|
|
485
|
-
if (b) return b
|
|
486
|
-
}
|
|
487
|
-
return undefined
|
|
488
|
-
}
|
|
489
|
-
const proto: unknown = Object.getPrototypeOf(x)
|
|
490
|
-
if (proto !== Object.prototype && proto !== null) return `${path} is a ${(x as { constructor?: { name?: string } }).constructor?.name ?? 'class instance'}`
|
|
491
|
-
for (const k of Object.keys(x)) {
|
|
492
|
-
const b = bad((x as Obj)[k], `${path}.${k}`)
|
|
493
|
-
if (b) return b
|
|
494
|
-
}
|
|
495
|
-
return undefined
|
|
496
|
-
}
|
|
497
|
-
const b = bad(v, 'result')
|
|
498
|
-
if (b) scope.options.onDiagnostic?.({ code: 'Q005', severity: 'warning', message: `\`${fn.$id}\`'s ${b}: the client gets a different value than the server rendered with. Send plain data (ISO strings, arrays, objects)` })
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
/**
|
|
502
|
-
* Q008 (dev, once): each mutation's static `invalidates` against the tags queries declare. A tags function is called
|
|
503
|
-
* with an input whose every field reads `*`, so `todo:${id}` declares `todo:*`.
|
|
504
|
-
*/
|
|
505
|
-
export function checkTags(report: (d: { code: string; severity: string; message: string }) => void) {
|
|
506
|
-
const star: unknown = new Proxy({}, { get: (_, k) => (k === Symbol.toPrimitive ? () => '*' : '*') })
|
|
507
|
-
const declared: string[] = []
|
|
508
|
-
for (const f of fns.values()) {
|
|
509
|
-
if (f.kind !== 'query') continue
|
|
510
|
-
try { declared.push(...(typeof f.def.tags === 'function' ? f.def.tags(star) : f.def.tags ?? [])) } catch {}
|
|
511
|
-
}
|
|
512
|
-
const matches = (tag: string) => declared.some(d => d === tag || (d.includes('*') && new RegExp(`^${d.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^]*')}$`).test(tag)) || (tag.includes('*') && d.startsWith(tag.slice(0, tag.indexOf('*')))))
|
|
513
|
-
for (const f of fns.values()) {
|
|
514
|
-
if (f.kind !== 'mutation' || !f.def.invalidates) continue
|
|
515
|
-
let tags: readonly string[]
|
|
516
|
-
try { tags = typeof f.def.invalidates === 'function' ? f.def.invalidates(star, star) : f.def.invalidates } catch { continue }
|
|
517
|
-
for (const t of tags) if (!matches(t)) report({ code: 'Q008', severity: 'warning', message: `\`${f.$id}\` invalidates \`${t}\`, a tag no query declares: nothing refreshes. Fix the tag, or add it to the query's \`tags\`` })
|
|
518
|
-
}
|
|
362
|
+
await runtime.poke?.(tags, scope.request.headers.get('x-weft-conn')).catch((e) =>
|
|
363
|
+
scope.options.onDiagnostic?.({
|
|
364
|
+
code: 'E_RT',
|
|
365
|
+
severity: 'error',
|
|
366
|
+
message: `poke of ${tags.join(', ')} failed: ${e instanceof Error ? e.message : e}`,
|
|
367
|
+
}),
|
|
368
|
+
)
|
|
519
369
|
}
|