@weft-js/start 0.1.0 → 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 +51 -10
- 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 +124 -490
- package/src/client.tsx +9 -2
- package/src/contract/describe.mjs +231 -0
- package/src/contract.mjs +146 -155
- 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 +97 -168
- 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 +113 -35
- 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 -174
- 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 -964
- 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/bus.ts
CHANGED
|
@@ -19,9 +19,9 @@ export function slotBus(plugin: string, load: () => Promise<Bus>): Bus {
|
|
|
19
19
|
const get = () => (bus ??= load())
|
|
20
20
|
return {
|
|
21
21
|
name: plugin.replace(/^.*realtime-/, ''),
|
|
22
|
-
publish: (channel, message) => get().then(b => b.publish(channel, message)),
|
|
23
|
-
subscribe: (channel, on, gap) => get().then(b => b.subscribe(channel, on, gap)),
|
|
24
|
-
close: () => (bus ? bus.then(b => b.close?.()) : Promise.resolve()),
|
|
22
|
+
publish: (channel, message) => get().then((b) => b.publish(channel, message)),
|
|
23
|
+
subscribe: (channel, on, gap) => get().then((b) => b.subscribe(channel, on, gap)),
|
|
24
|
+
close: () => (bus ? bus.then((b) => b.close?.()) : Promise.resolve()),
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -32,7 +32,10 @@ export function memoryBus(): Bus {
|
|
|
32
32
|
name: 'memory',
|
|
33
33
|
publish(channel, message) {
|
|
34
34
|
const list = channels.get(channel)
|
|
35
|
-
if (list)
|
|
35
|
+
if (list)
|
|
36
|
+
queueMicrotask(() => {
|
|
37
|
+
for (const on of list) on(message)
|
|
38
|
+
})
|
|
36
39
|
return Promise.resolve()
|
|
37
40
|
},
|
|
38
41
|
subscribe(channel, on) {
|
|
@@ -12,4 +12,37 @@
|
|
|
12
12
|
const CODE = `(n=>{var d=document,c=navigator.connection,q=window.__wq={q:[],n:n,w(){},d:matchMedia("(prefers-reduced-data: reduce)").matches||!!(c&&c.saveData),p(e){var t=e.target,r=q.r,s=e.submitter;if(t.isConnected){q.r=1;try{e.type=="submit"?t.requestSubmit(s&&s.isConnected?s:void 0):e.type=="reset"?t.reset():t.dispatchEvent(new e.constructor(e.type,e))}finally{q.r=r}}}},o=t=>t.closest&&t.closest("[wk]"),h=e=>{var k=!q.r&&o(e.target);if(k&&(!k.$k||q.q.length)&&!((e.metaKey||e.ctrlKey||e.shiftKey)&&e.type=="click"&&e.target.closest("a"))){q.q.push(e);/^(click|submit|reset)$/.test(e.type)&&e.preventDefault();q.w(k)}},l=e=>{var k=o(e.target);k&&!k.$k&&!q.d&&q.w(k,1)};"click dblclick pointerdown pointerup mousedown mouseup keydown keyup input change submit reset focus blur focusin focusout".split(" ").forEach(t=>d.addEventListener(t,h,!0));"pointerover touchstart".split(" ").forEach(t=>d.addEventListener(t,l,{capture:!0,passive:!0}))})`
|
|
13
13
|
|
|
14
14
|
/** The script's text. `now`: the page has work that can't wait, so its code loads straight away. */
|
|
15
|
-
export const capture = now => `${CODE}(${now ? 1 : 0})`
|
|
15
|
+
export const capture = (now: boolean) => `${CODE}(${now ? 1 : 0})`
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A page that runs no client until it's needed follows its links with full loads, so the browser prefetches the one the
|
|
19
|
+
* pointer rests on (`moderate`). Not a prerender, which would run the next page's code unseen. An island's links go
|
|
20
|
+
* through the router once its code comes, so they're left out, as is `/_weft/` and anything under `data-preload="off"`;
|
|
21
|
+
* `start()` takes the rules down when the router takes over, so nothing is fetched twice.
|
|
22
|
+
*/
|
|
23
|
+
const RULES = JSON.stringify({
|
|
24
|
+
prefetch: [
|
|
25
|
+
{
|
|
26
|
+
source: 'document',
|
|
27
|
+
where: {
|
|
28
|
+
and: [
|
|
29
|
+
{ href_matches: '/*' },
|
|
30
|
+
{ not: { href_matches: '/_weft/*' } },
|
|
31
|
+
{
|
|
32
|
+
not: {
|
|
33
|
+
selector_matches:
|
|
34
|
+
'[wk] a, a[wk], [data-preload="off"] a, a[data-preload="off"], a[download]',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
eagerness: 'moderate',
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
/** The rules, for a page whose code waits (`now` false, nothing streaming). */
|
|
45
|
+
export const speculate = (shape: { now: boolean; streaming?: boolean }) =>
|
|
46
|
+
shape.now || shape.streaming
|
|
47
|
+
? ''
|
|
48
|
+
: `<script type="speculationrules" id="weft-speculate">${RULES}</script>\n`
|
package/src/changes-sql.d.mts
CHANGED
|
@@ -9,6 +9,12 @@ export const LOG_ROWS: number
|
|
|
9
9
|
/** Between statements of generated SQL, as Drizzle splits them. */
|
|
10
10
|
export const BREAK: string
|
|
11
11
|
/** The trigger statements for these tables, one by one. */
|
|
12
|
-
export function changeStatements(
|
|
12
|
+
export function changeStatements(
|
|
13
|
+
dialect: string,
|
|
14
|
+
tables: readonly { name: string; pk?: string[] }[],
|
|
15
|
+
): string[]
|
|
13
16
|
/** The same statements as one migration. */
|
|
14
|
-
export function changeSql(
|
|
17
|
+
export function changeSql(
|
|
18
|
+
dialect: string,
|
|
19
|
+
tables: readonly { name: string; pk?: string[] }[],
|
|
20
|
+
): string
|
package/src/changes-sql.mjs
CHANGED
|
@@ -14,10 +14,10 @@ export const MAX_IDS = 32
|
|
|
14
14
|
export const LOG_ROWS = 4096
|
|
15
15
|
export const BREAK = '\n--> statement-breakpoint\n'
|
|
16
16
|
|
|
17
|
-
const pgName = n => `"${n.replaceAll('"', '""')}"`
|
|
18
|
-
const myName = n => `\`${n.replaceAll('`', '``')}\``
|
|
19
|
-
const literal = s => `'${s.replaceAll("'", "''")}'`
|
|
20
|
-
const safe = t => t.replace(/\W/g, '_')
|
|
17
|
+
const pgName = (n) => `"${n.replaceAll('"', '""')}"`
|
|
18
|
+
const myName = (n) => `\`${n.replaceAll('`', '``')}\``
|
|
19
|
+
const literal = (s) => `'${s.replaceAll("'", "''")}'`
|
|
20
|
+
const safe = (t) => t.replace(/\W/g, '_')
|
|
21
21
|
|
|
22
22
|
function postgres(tables) {
|
|
23
23
|
const fn = `CREATE OR REPLACE FUNCTION weft_changes_notify() RETURNS trigger AS $weft$
|
|
@@ -48,7 +48,12 @@ $weft$ LANGUAGE plpgsql`
|
|
|
48
48
|
`DROP TRIGGER IF EXISTS weft_changes_${suffix} ON ${on}`,
|
|
49
49
|
`CREATE TRIGGER weft_changes_${suffix} AFTER ${event} ON ${on} ${rows ? `REFERENCING ${rows} TABLE AS weft_rows ` : ''}FOR EACH STATEMENT EXECUTE FUNCTION weft_changes_notify(${args})`,
|
|
50
50
|
]
|
|
51
|
-
return [
|
|
51
|
+
return [
|
|
52
|
+
...trigger('ins', 'INSERT', 'NEW'),
|
|
53
|
+
...trigger('upd', 'UPDATE', 'NEW'),
|
|
54
|
+
...trigger('del', 'DELETE', 'OLD'),
|
|
55
|
+
...trigger('trn', 'TRUNCATE'),
|
|
56
|
+
]
|
|
52
57
|
})
|
|
53
58
|
return [fn, ...list]
|
|
54
59
|
}
|
|
@@ -56,7 +61,8 @@ $weft$ LANGUAGE plpgsql`
|
|
|
56
61
|
function sqlite(tables) {
|
|
57
62
|
const log = `CREATE TABLE IF NOT EXISTS ${LOG} (seq INTEGER PRIMARY KEY AUTOINCREMENT, tbl TEXT NOT NULL, pk TEXT)`
|
|
58
63
|
const list = tables.flatMap(({ name, pk }) => {
|
|
59
|
-
const id =
|
|
64
|
+
const id = (row) =>
|
|
65
|
+
pk.length ? pk.map((c) => `${row}.${pgName(c)}`).join(` || ':' || `) : 'NULL'
|
|
60
66
|
const trigger = (suffix, event, row) => [
|
|
61
67
|
`DROP TRIGGER IF EXISTS weft_changes_${safe(name)}_${suffix}`,
|
|
62
68
|
`CREATE TRIGGER weft_changes_${safe(name)}_${suffix} AFTER ${event} ON ${pgName(name)} BEGIN
|
|
@@ -64,7 +70,11 @@ function sqlite(tables) {
|
|
|
64
70
|
DELETE FROM ${LOG} WHERE seq <= last_insert_rowid() - ${LOG_ROWS};
|
|
65
71
|
END`,
|
|
66
72
|
]
|
|
67
|
-
return [
|
|
73
|
+
return [
|
|
74
|
+
...trigger('ins', 'INSERT', 'NEW'),
|
|
75
|
+
...trigger('upd', 'UPDATE', 'NEW'),
|
|
76
|
+
...trigger('del', 'DELETE', 'OLD'),
|
|
77
|
+
]
|
|
68
78
|
})
|
|
69
79
|
return [log, ...list]
|
|
70
80
|
}
|
|
@@ -72,12 +82,17 @@ END`,
|
|
|
72
82
|
function mysql(tables) {
|
|
73
83
|
const log = `CREATE TABLE IF NOT EXISTS ${LOG} (seq BIGINT AUTO_INCREMENT PRIMARY KEY, tbl VARCHAR(64) NOT NULL, pk VARCHAR(255), at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY at_idx (at))`
|
|
74
84
|
const list = tables.flatMap(({ name, pk }) => {
|
|
75
|
-
const id =
|
|
85
|
+
const id = (row) =>
|
|
86
|
+
pk.length ? `CONCAT_WS(':', ${pk.map((c) => `${row}.${myName(c)}`).join(', ')})` : 'NULL'
|
|
76
87
|
const trigger = (suffix, event, row) => [
|
|
77
88
|
`DROP TRIGGER IF EXISTS weft_changes_${safe(name)}_${suffix}`,
|
|
78
89
|
`CREATE TRIGGER weft_changes_${safe(name)}_${suffix} AFTER ${event} ON ${myName(name)} FOR EACH ROW INSERT INTO ${LOG} (tbl, pk) VALUES (${literal(name)}, ${id(row)})`,
|
|
79
90
|
]
|
|
80
|
-
return [
|
|
91
|
+
return [
|
|
92
|
+
...trigger('ins', 'INSERT', 'NEW'),
|
|
93
|
+
...trigger('upd', 'UPDATE', 'NEW'),
|
|
94
|
+
...trigger('del', 'DELETE', 'OLD'),
|
|
95
|
+
]
|
|
81
96
|
})
|
|
82
97
|
return [log, ...list]
|
|
83
98
|
}
|
|
@@ -96,4 +111,5 @@ export function changeStatements(dialect, tables) {
|
|
|
96
111
|
}
|
|
97
112
|
|
|
98
113
|
/** A migration file, in drizzle-kit's format. */
|
|
99
|
-
export const changeSql = (dialect, tables) =>
|
|
114
|
+
export const changeSql = (dialect, tables) =>
|
|
115
|
+
`-- weft: report changes to ${tables.map((t) => t.name).join(', ')} (\`weft db live\`). Ids only, never a row.\n${changeStatements(dialect, tables).join(`;${BREAK}`)};\n`
|
package/src/changes.ts
CHANGED
|
@@ -20,7 +20,10 @@ import type { DbHandle } from './db/core'
|
|
|
20
20
|
export type { LiveMode } from './data'
|
|
21
21
|
|
|
22
22
|
/** What changed: never what it changed to. No `id`: the whole table. */
|
|
23
|
-
export interface Change {
|
|
23
|
+
export interface Change {
|
|
24
|
+
table: string
|
|
25
|
+
id?: string
|
|
26
|
+
}
|
|
24
27
|
|
|
25
28
|
/** What a source learned about this database, by trying it. `feed` sees every change; `poll` only estimates. */
|
|
26
29
|
export interface Probe {
|
|
@@ -64,7 +67,10 @@ export interface Source {
|
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
/** A table as `Source.defs` reports it. */
|
|
67
|
-
export interface TableDef {
|
|
70
|
+
export interface TableDef {
|
|
71
|
+
name: string
|
|
72
|
+
pk: string[]
|
|
73
|
+
}
|
|
68
74
|
|
|
69
75
|
/** `dbChanges`: how to listen, and how a change turns into tags. */
|
|
70
76
|
export interface ChangeOptions {
|
|
@@ -79,12 +85,25 @@ export interface ChangeOptions {
|
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
const NAME = Symbol.for('drizzle:Name')
|
|
82
|
-
const nameOf = (t: unknown): string =>
|
|
88
|
+
const nameOf = (t: unknown): string =>
|
|
89
|
+
typeof t === 'string'
|
|
90
|
+
? t
|
|
91
|
+
: ((t as Record<symbol, string> | null)?.[NAME] ??
|
|
92
|
+
(() => {
|
|
93
|
+
throw new StartError(
|
|
94
|
+
'R017',
|
|
95
|
+
`\`dbChanges\` got something that isn't a table: ${String(t)}`,
|
|
96
|
+
'pass Drizzle tables, or their names',
|
|
97
|
+
)
|
|
98
|
+
})())
|
|
83
99
|
|
|
84
100
|
const CORE = { sqlite: 'sqlite-core', postgresql: 'pg-core', mysql: 'mysql-core' }
|
|
85
101
|
|
|
86
102
|
/** A table's primary key columns, from its Drizzle definition. */
|
|
87
|
-
interface Key {
|
|
103
|
+
interface Key {
|
|
104
|
+
name: string
|
|
105
|
+
primary?: boolean
|
|
106
|
+
}
|
|
88
107
|
|
|
89
108
|
async function pkOf(table: unknown, dialect: DbHandle['dialect']): Promise<string[]> {
|
|
90
109
|
const { getTableConfig } = await import(/* @vite-ignore */ `drizzle-orm/${CORE[dialect]}`)
|
|
@@ -94,9 +113,18 @@ async function pkOf(table: unknown, dialect: DbHandle['dialect']): Promise<strin
|
|
|
94
113
|
}
|
|
95
114
|
|
|
96
115
|
/** `realtime({ changes: dbChanges(db, [todos, notes]) })`: these tables' writes reach `.live`, whoever made them. */
|
|
97
|
-
export function dbChanges(
|
|
116
|
+
export function dbChanges(
|
|
117
|
+
db: { $weft?: DbHandle },
|
|
118
|
+
tables: unknown[],
|
|
119
|
+
options: ChangeOptions = {},
|
|
120
|
+
): Source {
|
|
98
121
|
const handle = db?.$weft
|
|
99
|
-
if (!handle?.changes)
|
|
122
|
+
if (!handle?.changes)
|
|
123
|
+
throw new StartError(
|
|
124
|
+
'R017',
|
|
125
|
+
'`dbChanges` needs a client from `@weft-js/start/db/sqlite`, `/postgres` or `/mysql`',
|
|
126
|
+
'for MongoDB use `mongoChanges` from `@weft-js/db/mongo`; for another client, write a `Source`',
|
|
127
|
+
)
|
|
100
128
|
const names = tables.map(nameOf)
|
|
101
129
|
let source: Promise<Omit<Source, 'name' | 'tables'>> | undefined
|
|
102
130
|
const load = () => (source ??= handle.changes!(names, options))
|
|
@@ -106,15 +134,25 @@ export function dbChanges(db: { $weft?: DbHandle }, tables: unknown[], options:
|
|
|
106
134
|
tables: names,
|
|
107
135
|
external: options.listener === 'external',
|
|
108
136
|
tagged: options.tagged,
|
|
109
|
-
defs: () =>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
137
|
+
defs: () =>
|
|
138
|
+
Promise.all(
|
|
139
|
+
tables.map(async (t) => ({
|
|
140
|
+
name: nameOf(t),
|
|
141
|
+
pk: typeof t === 'string' ? [] : await pkOf(t, handle.dialect),
|
|
142
|
+
})),
|
|
143
|
+
),
|
|
144
|
+
probe: () => load().then((s) => s.probe()),
|
|
145
|
+
start: (sink, probe) => load().then((s) => s.start(sink, probe)),
|
|
146
|
+
settle: (tags) => load().then((s) => s.settle?.(tags)),
|
|
113
147
|
}
|
|
114
148
|
}
|
|
115
149
|
|
|
116
150
|
/** Set by a platform, read at run time: this process lives for one request. */
|
|
117
|
-
export function functionHost(
|
|
151
|
+
export function functionHost(
|
|
152
|
+
env: Record<string, string | undefined> = (
|
|
153
|
+
globalThis as unknown as { process?: { env: Record<string, string | undefined> } }
|
|
154
|
+
).process?.env ?? {},
|
|
155
|
+
): string | undefined {
|
|
118
156
|
if (env.VERCEL) return 'Vercel'
|
|
119
157
|
if (env.NETLIFY || env.NETLIFY_LOCAL) return 'Netlify'
|
|
120
158
|
if (env.AWS_LAMBDA_FUNCTION_NAME) return 'AWS Lambda'
|
|
@@ -125,7 +163,11 @@ export function functionHost(env: Record<string, string | undefined> = (globalTh
|
|
|
125
163
|
}
|
|
126
164
|
|
|
127
165
|
/** What a change sends to live queries, as tags to refetch. */
|
|
128
|
-
export interface Poke {
|
|
166
|
+
export interface Poke {
|
|
167
|
+
t: string[]
|
|
168
|
+
/** Tag prefixes: `todos:` for every row of a table-level change. */ p?: string[]
|
|
169
|
+
/** Tags a barrier covered: a subscription with one of them is poked already. */ k?: string[]
|
|
170
|
+
}
|
|
129
171
|
|
|
130
172
|
/** Where a feed's pokes go: this instance, and the rest through the bus. */
|
|
131
173
|
export interface FeedOut {
|
|
@@ -145,23 +187,116 @@ export interface Feed {
|
|
|
145
187
|
readonly running: boolean
|
|
146
188
|
}
|
|
147
189
|
|
|
148
|
-
const unref = <T
|
|
190
|
+
const unref = <T>(t: T) => ((t as { unref?(): void } | null | undefined)?.unref?.(), t)
|
|
191
|
+
|
|
192
|
+
function feedHandle(
|
|
193
|
+
source: Source,
|
|
194
|
+
known: Set<string>,
|
|
195
|
+
sink: Sink,
|
|
196
|
+
cancel: () => void,
|
|
197
|
+
barrier: number,
|
|
198
|
+
): Feed {
|
|
199
|
+
// stopped → starting → running → stopped. A start that resolves after stop() (its turn has passed) is stopped at
|
|
200
|
+
// once, so a listener can't outlive the feed.
|
|
201
|
+
let phase: 'stopped' | 'starting' | 'running' = 'stopped'
|
|
202
|
+
let turn = 0
|
|
203
|
+
let stopping: (() => unknown) | undefined
|
|
204
|
+
return {
|
|
205
|
+
get running() {
|
|
206
|
+
return phase !== 'stopped'
|
|
207
|
+
},
|
|
208
|
+
covers(tag) {
|
|
209
|
+
const at = tag.indexOf(':')
|
|
210
|
+
return known.has(at < 0 ? tag : tag.slice(0, at))
|
|
211
|
+
},
|
|
212
|
+
async start(probe) {
|
|
213
|
+
if (phase !== 'stopped') return
|
|
214
|
+
phase = 'starting'
|
|
215
|
+
const mine = ++turn
|
|
216
|
+
let stop: (() => unknown) | undefined
|
|
217
|
+
try {
|
|
218
|
+
stop = await source.start(sink, probe)
|
|
219
|
+
} catch (e) {
|
|
220
|
+
if (turn === mine) phase = 'stopped'
|
|
221
|
+
throw e
|
|
222
|
+
}
|
|
223
|
+
if (turn !== mine) return void (await stop?.())
|
|
224
|
+
stopping = stop
|
|
225
|
+
phase = 'running'
|
|
226
|
+
},
|
|
227
|
+
async stop() {
|
|
228
|
+
cancel()
|
|
229
|
+
turn++
|
|
230
|
+
phase = 'stopped'
|
|
231
|
+
const stop = stopping
|
|
232
|
+
stopping = undefined
|
|
233
|
+
await stop?.()
|
|
234
|
+
},
|
|
235
|
+
async settle(tags) {
|
|
236
|
+
const ours = tags.filter((t) => this.covers(t))
|
|
237
|
+
if (!ours.length || !source.settle) return
|
|
238
|
+
let late: ReturnType<typeof setTimeout> | undefined
|
|
239
|
+
await Promise.race([
|
|
240
|
+
source.settle(ours),
|
|
241
|
+
new Promise<void>((ok) => {
|
|
242
|
+
late = unref(setTimeout(ok, barrier))
|
|
243
|
+
}),
|
|
244
|
+
]).catch(() => {})
|
|
245
|
+
clearTimeout(late)
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function addChanges(
|
|
251
|
+
list: Change[],
|
|
252
|
+
o: {
|
|
253
|
+
known: Set<string>
|
|
254
|
+
source: Source
|
|
255
|
+
pending: Set<string>
|
|
256
|
+
covered: Set<string>
|
|
257
|
+
prefixes: Set<string>
|
|
258
|
+
out: FeedOut
|
|
259
|
+
arm: () => void
|
|
260
|
+
},
|
|
261
|
+
) {
|
|
262
|
+
const { known, source, pending, covered, prefixes, out, arm } = o
|
|
263
|
+
const seen: string[] = []
|
|
264
|
+
for (const c of list) {
|
|
265
|
+
if (!known.has(c.table)) continue
|
|
266
|
+
for (const t of source.tagged?.(c) ??
|
|
267
|
+
(c.id === undefined ? [c.table] : [c.table, `${c.table}:${c.id}`])) {
|
|
268
|
+
pending.add(t)
|
|
269
|
+
covered.delete(t) // a change after the barrier: no poke covers it
|
|
270
|
+
seen.push(t)
|
|
271
|
+
}
|
|
272
|
+
if (c.id === undefined) prefixes.add(`${c.table}:`)
|
|
273
|
+
}
|
|
274
|
+
if (!seen.length) return
|
|
275
|
+
out.bump?.(seen)
|
|
276
|
+
arm()
|
|
277
|
+
}
|
|
149
278
|
|
|
150
279
|
/** Batches a source's changes into pokes, held and throttled so a burst of writes is one refetch. */
|
|
151
|
-
export function createFeed(
|
|
280
|
+
export function createFeed(
|
|
281
|
+
source: Source,
|
|
282
|
+
out: FeedOut,
|
|
283
|
+
{ hold = source.hold ?? 25, throttle = 250, barrier = 1000 } = {},
|
|
284
|
+
): Feed {
|
|
152
285
|
const known = new Set(source.tables)
|
|
153
|
-
let pending = new Set<string>(),
|
|
286
|
+
let pending = new Set<string>(),
|
|
287
|
+
prefixes = new Set<string>(),
|
|
288
|
+
covered = new Set<string>()
|
|
154
289
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
155
290
|
let last = 0
|
|
156
291
|
let first: number | undefined
|
|
157
|
-
let stopping: (() => unknown) | undefined
|
|
158
|
-
let running = false
|
|
159
292
|
let isDown = false
|
|
160
293
|
|
|
161
294
|
const flush = () => {
|
|
162
295
|
timer = first = undefined
|
|
163
296
|
last = Date.now()
|
|
164
|
-
const t = [...pending],
|
|
297
|
+
const t = [...pending],
|
|
298
|
+
p = [...prefixes],
|
|
299
|
+
k = [...covered]
|
|
165
300
|
pending = new Set()
|
|
166
301
|
prefixes = new Set()
|
|
167
302
|
covered = new Set()
|
|
@@ -172,27 +307,13 @@ export function createFeed(source: Source, out: FeedOut, { hold = source.hold ??
|
|
|
172
307
|
const now = Date.now()
|
|
173
308
|
first ??= now
|
|
174
309
|
clearTimeout(timer)
|
|
175
|
-
timer = unref(
|
|
310
|
+
timer = unref(
|
|
311
|
+
setTimeout(flush, Math.max(Math.min(hold, first + throttle - now), last + throttle - now)),
|
|
312
|
+
)
|
|
176
313
|
}
|
|
177
314
|
|
|
178
|
-
const tagsOf = (change: Change) => source.tagged?.(change) ?? (change.id === undefined ? [change.table] : [change.table, `${change.table}:${change.id}`])
|
|
179
|
-
|
|
180
315
|
const sink: Sink = {
|
|
181
|
-
changes(list) {
|
|
182
|
-
const seen: string[] = []
|
|
183
|
-
for (const c of list) {
|
|
184
|
-
if (!known.has(c.table)) continue
|
|
185
|
-
for (const t of tagsOf(c)) {
|
|
186
|
-
pending.add(t)
|
|
187
|
-
covered.delete(t) // a change after the barrier: no poke covers it
|
|
188
|
-
seen.push(t)
|
|
189
|
-
}
|
|
190
|
-
if (c.id === undefined) prefixes.add(`${c.table}:`)
|
|
191
|
-
}
|
|
192
|
-
if (!seen.length) return
|
|
193
|
-
out.bump?.(seen)
|
|
194
|
-
arm()
|
|
195
|
-
},
|
|
316
|
+
changes: (list) => addChanges(list, { known, source, pending, covered, prefixes, out, arm }),
|
|
196
317
|
settled(tags) {
|
|
197
318
|
for (const t of tags) {
|
|
198
319
|
if (pending.delete(t)) covered.add(t)
|
|
@@ -204,7 +325,7 @@ export function createFeed(source: Source, out: FeedOut, { hold = source.hold ??
|
|
|
204
325
|
covered = new Set()
|
|
205
326
|
},
|
|
206
327
|
lost() {
|
|
207
|
-
sink.changes(source.tables.map(table => ({ table })))
|
|
328
|
+
sink.changes(source.tables.map((table) => ({ table })))
|
|
208
329
|
},
|
|
209
330
|
down(why) {
|
|
210
331
|
if (isDown) return
|
|
@@ -218,36 +339,14 @@ export function createFeed(source: Source, out: FeedOut, { hold = source.hold ??
|
|
|
218
339
|
},
|
|
219
340
|
}
|
|
220
341
|
|
|
221
|
-
return
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
},
|
|
227
|
-
async start(probe) {
|
|
228
|
-
if (running) return
|
|
229
|
-
running = true
|
|
230
|
-
try {
|
|
231
|
-
stopping = await source.start(sink, probe)
|
|
232
|
-
} catch (e) {
|
|
233
|
-
running = false
|
|
234
|
-
throw e
|
|
235
|
-
}
|
|
236
|
-
},
|
|
237
|
-
async stop() {
|
|
342
|
+
return feedHandle(
|
|
343
|
+
source,
|
|
344
|
+
known,
|
|
345
|
+
sink,
|
|
346
|
+
() => {
|
|
238
347
|
clearTimeout(timer)
|
|
239
348
|
timer = undefined
|
|
240
|
-
running = false
|
|
241
|
-
const stop = stopping
|
|
242
|
-
stopping = undefined
|
|
243
|
-
await stop?.()
|
|
244
349
|
},
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (!ours.length || !source.settle) return
|
|
248
|
-
let late: ReturnType<typeof setTimeout> | undefined
|
|
249
|
-
await Promise.race([source.settle(ours), new Promise<void>(ok => { late = unref(setTimeout(ok, barrier)) })]).catch(() => {})
|
|
250
|
-
clearTimeout(late)
|
|
251
|
-
},
|
|
252
|
-
}
|
|
350
|
+
barrier,
|
|
351
|
+
)
|
|
253
352
|
}
|