@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/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
|
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { Box } from '@weft-js/core/internal'
|
|
2
|
+
import {
|
|
3
|
+
DEV,
|
|
4
|
+
collect as boxes,
|
|
5
|
+
derived,
|
|
6
|
+
effect,
|
|
7
|
+
getOwner,
|
|
8
|
+
onCleanup,
|
|
9
|
+
state,
|
|
10
|
+
untrack,
|
|
11
|
+
type Derived,
|
|
12
|
+
type State,
|
|
13
|
+
} from '@weft-js/core'
|
|
14
|
+
import { currentScope, FnError, skip, StartError, track, type Fn } from '../data'
|
|
15
|
+
import type { ChannelStatus, ConnectOptions, Connection, SharedBox } from '../channel'
|
|
16
|
+
import {
|
|
17
|
+
acquire,
|
|
18
|
+
hold,
|
|
19
|
+
listOf,
|
|
20
|
+
post,
|
|
21
|
+
rejoin,
|
|
22
|
+
type Handle,
|
|
23
|
+
type Join,
|
|
24
|
+
type ListSpec,
|
|
25
|
+
} from './socket'
|
|
26
|
+
|
|
27
|
+
const NONE: never[] = []
|
|
28
|
+
type View = Join | undefined
|
|
29
|
+
|
|
30
|
+
/** The part `connect` and `shared` have in common: the current join, as the argument moves. */
|
|
31
|
+
function follow(
|
|
32
|
+
fn: Fn,
|
|
33
|
+
arg: unknown,
|
|
34
|
+
options: ConnectOptions | undefined,
|
|
35
|
+
handle: Handle,
|
|
36
|
+
joined?: (j: Join) => void,
|
|
37
|
+
): State<View> {
|
|
38
|
+
const view = state<View>(undefined)
|
|
39
|
+
effect(
|
|
40
|
+
() => {
|
|
41
|
+
const params = typeof arg === 'function' ? (arg as () => unknown)() : arg
|
|
42
|
+
if (params === skip) return undefined
|
|
43
|
+
return untrack(() => {
|
|
44
|
+
const j = acquire(fn, params)
|
|
45
|
+
j.handles.add(handle)
|
|
46
|
+
joined?.(j)
|
|
47
|
+
const stop = hold(j, options?.keep)
|
|
48
|
+
onCleanup(() => {
|
|
49
|
+
j.handles.delete(handle)
|
|
50
|
+
stop()
|
|
51
|
+
})
|
|
52
|
+
return j
|
|
53
|
+
})
|
|
54
|
+
},
|
|
55
|
+
(now) => {
|
|
56
|
+
view.value = now
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
return view
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function owned(fn: Fn) {
|
|
63
|
+
if (!getOwner())
|
|
64
|
+
throw new StartError(
|
|
65
|
+
'W051',
|
|
66
|
+
DEV
|
|
67
|
+
? `\`${fn.$id}.connect()\` was called outside a component or scope: nothing would close it`
|
|
68
|
+
: fn.$id,
|
|
69
|
+
DEV
|
|
70
|
+
? 'call it in a component, a `defineState` setup or `createRoot(dispose => …)`'
|
|
71
|
+
: undefined,
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** `@weft-js/start/share`'s way in: a join that nothing on the page declared, so a tab shares this socket and its lifecycle. */
|
|
76
|
+
export const connectBuiltIn = ($id: string) =>
|
|
77
|
+
connect({ kind: 'channel', $id, def: {} }, undefined, { keep: 0 })
|
|
78
|
+
|
|
79
|
+
export function connect(
|
|
80
|
+
fn: Fn,
|
|
81
|
+
arg: unknown,
|
|
82
|
+
options?: ConnectOptions,
|
|
83
|
+
): Connection<Record<string, never>, Record<string, never>> {
|
|
84
|
+
owned(fn)
|
|
85
|
+
if (currentScope()) {
|
|
86
|
+
const closed = { value: 'connecting' as ChannelStatus }
|
|
87
|
+
return {
|
|
88
|
+
status: closed,
|
|
89
|
+
room: { value: undefined },
|
|
90
|
+
error: undefined,
|
|
91
|
+
resets: { value: 0 },
|
|
92
|
+
collect: () => ({ value: NONE }),
|
|
93
|
+
on: () => () => {},
|
|
94
|
+
onReset: () => () => {},
|
|
95
|
+
send: () =>
|
|
96
|
+
Promise.reject(new FnError({ message: 'a channel has no socket on the server' }, 500)),
|
|
97
|
+
retry() {},
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const handle: Handle = { on: new Map(), resets: new Set() }
|
|
101
|
+
const wanted: ListSpec[] = []
|
|
102
|
+
const view = follow(fn, arg, options, handle, (j) => {
|
|
103
|
+
for (const w of wanted) listOf(j, w)
|
|
104
|
+
})
|
|
105
|
+
return {
|
|
106
|
+
status: derived(() => view.value?.status.value ?? 'closed'),
|
|
107
|
+
room: derived(() => view.value?.key),
|
|
108
|
+
get error() {
|
|
109
|
+
return view.value?.error.value
|
|
110
|
+
},
|
|
111
|
+
resets: derived(() => view.value?.resets.value ?? 0),
|
|
112
|
+
collect(name, { max = 200, key } = {}) {
|
|
113
|
+
const spec: ListSpec = { name, max, key }
|
|
114
|
+
wanted.push(spec)
|
|
115
|
+
const had = untrack(() => view.value)
|
|
116
|
+
if (had) listOf(had, spec)
|
|
117
|
+
return derived(() => {
|
|
118
|
+
const j = view.value
|
|
119
|
+
return (j ? listOf(j, spec).box.value : NONE) as never[]
|
|
120
|
+
})
|
|
121
|
+
},
|
|
122
|
+
on(name, listener) {
|
|
123
|
+
const set = handle.on.get(name) ?? new Set()
|
|
124
|
+
handle.on.set(name, set)
|
|
125
|
+
set.add(listener)
|
|
126
|
+
const stop = () => void set.delete(listener)
|
|
127
|
+
onCleanup(stop)
|
|
128
|
+
return stop
|
|
129
|
+
},
|
|
130
|
+
onReset(listener) {
|
|
131
|
+
handle.resets.add(listener)
|
|
132
|
+
const stop = () => void handle.resets.delete(listener)
|
|
133
|
+
onCleanup(stop)
|
|
134
|
+
return stop
|
|
135
|
+
},
|
|
136
|
+
send(name, message) {
|
|
137
|
+
const j = untrack(() => view.value)
|
|
138
|
+
return j
|
|
139
|
+
? post(j, name, message)
|
|
140
|
+
: Promise.reject(new FnError({ message: 'this channel is closed' }, 400))
|
|
141
|
+
},
|
|
142
|
+
retry() {
|
|
143
|
+
const j = untrack(() => view.value)
|
|
144
|
+
if (j?.refused) rejoin(j)
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** `Box.w` is the one way a write gets in, so a subclass sees every write, and `Instance` never touches it. */
|
|
150
|
+
class Shared<T> extends Box<T> {
|
|
151
|
+
declare remote?: boolean
|
|
152
|
+
declare pending: number
|
|
153
|
+
declare base: { v: T; at: number }
|
|
154
|
+
declare view: State<View>
|
|
155
|
+
declare statusOf: Derived<ChannelStatus>
|
|
156
|
+
declare problem: State<unknown>
|
|
157
|
+
get status() {
|
|
158
|
+
return this.statusOf
|
|
159
|
+
}
|
|
160
|
+
get error() {
|
|
161
|
+
return this.problem.value
|
|
162
|
+
}
|
|
163
|
+
/** A server value: applied unless a local write is still on its way. */
|
|
164
|
+
apply(v: { v: T; at: number }) {
|
|
165
|
+
if (v.at >= this.base.at) this.base = v
|
|
166
|
+
if (!this.pending) this.quiet(this.base.v)
|
|
167
|
+
}
|
|
168
|
+
quiet(v: T) {
|
|
169
|
+
this.remote = true
|
|
170
|
+
this.w(v)
|
|
171
|
+
this.remote = false
|
|
172
|
+
}
|
|
173
|
+
override w(v: T) {
|
|
174
|
+
if (this.remote) return super.w(v)
|
|
175
|
+
const j = untrack(() => this.view.value)
|
|
176
|
+
if (!j) return super.w(v)
|
|
177
|
+
super.w(v)
|
|
178
|
+
this.pending++
|
|
179
|
+
this.problem.value = undefined
|
|
180
|
+
void track(
|
|
181
|
+
post(j, '$set', v)
|
|
182
|
+
.then(
|
|
183
|
+
() => {
|
|
184
|
+
this.pending--
|
|
185
|
+
},
|
|
186
|
+
(e) => {
|
|
187
|
+
this.pending--
|
|
188
|
+
this.problem.value = e
|
|
189
|
+
},
|
|
190
|
+
)
|
|
191
|
+
.finally(() => {
|
|
192
|
+
if (!this.pending) this.quiet(this.base.v)
|
|
193
|
+
}),
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function shared(fn: Fn, arg: unknown, options?: ConnectOptions): SharedBox<unknown> {
|
|
199
|
+
owned(fn)
|
|
200
|
+
const initial = fn.def.initial
|
|
201
|
+
const [b] = boxes(() => new Shared<unknown>(initial))
|
|
202
|
+
b.pending = 0
|
|
203
|
+
b.base = { v: initial, at: 0 }
|
|
204
|
+
b.problem = state<unknown>(undefined)
|
|
205
|
+
if (currentScope()) {
|
|
206
|
+
b.statusOf = { value: 'connecting' }
|
|
207
|
+
b.view = state<View>(undefined)
|
|
208
|
+
return b
|
|
209
|
+
}
|
|
210
|
+
const handle: Handle = { on: new Map(), resets: new Set(), value: (v) => b.apply(v) }
|
|
211
|
+
b.view = follow(fn, arg, options, handle, () => {
|
|
212
|
+
b.base = { v: initial, at: 0 }
|
|
213
|
+
})
|
|
214
|
+
b.statusOf = derived(() => b.view.value?.status.value ?? 'closed')
|
|
215
|
+
return b
|
|
216
|
+
}
|