@weft-js/start 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/README.md +1278 -12
  2. package/crate/Cargo.lock +1347 -0
  3. package/crate/Cargo.toml +72 -0
  4. package/crate/README.md +322 -0
  5. package/crate/rust-toolchain.toml +3 -0
  6. package/crate/src/build/emit.rs +103 -0
  7. package/crate/src/build.rs +367 -0
  8. package/crate/src/ctx.rs +186 -0
  9. package/crate/src/envelope.rs +163 -0
  10. package/crate/src/functions.rs +173 -0
  11. package/crate/src/lib.rs +118 -0
  12. package/crate/src/native.rs +212 -0
  13. package/crate/src/otel.rs +62 -0
  14. package/crate/src/server.rs +203 -0
  15. package/package.json +46 -37
  16. package/src/app.mjs +38 -8
  17. package/src/asset-receipt.d.mts +13 -0
  18. package/src/asset-receipt.mjs +61 -0
  19. package/src/auth/better-auth.ts +215 -17
  20. package/src/auth/drizzle.ts +1 -1
  21. package/src/auth.ts +21 -6
  22. package/src/boot.ts +19 -8
  23. package/src/bus/postgres.ts +32 -19
  24. package/src/bus.ts +7 -4
  25. package/src/{capture.mjs → capture.ts} +34 -1
  26. package/src/changes-sql.d.mts +8 -2
  27. package/src/changes-sql.mjs +26 -10
  28. package/src/changes.ts +165 -66
  29. package/src/channel/connections.ts +216 -0
  30. package/src/channel/socket.ts +396 -0
  31. package/src/channel-fake.ts +19 -4
  32. package/src/channel.ts +120 -492
  33. package/src/client.tsx +9 -2
  34. package/src/contract/describe.mjs +231 -0
  35. package/src/contract.mjs +144 -157
  36. package/src/data/cache.ts +354 -0
  37. package/src/data/definitions.ts +94 -0
  38. package/src/data/draft.ts +104 -0
  39. package/src/data/foundation.ts +95 -0
  40. package/src/data/live-api.ts +22 -0
  41. package/src/data/mutation.ts +138 -0
  42. package/src/data/primitives.ts +135 -0
  43. package/src/data/query.ts +106 -0
  44. package/src/data/transport.ts +121 -0
  45. package/src/data/types.ts +303 -0
  46. package/src/data.ts +69 -894
  47. package/src/db/bun-sqlite.d.ts +4 -1
  48. package/src/db/changes-mysql.ts +61 -21
  49. package/src/db/changes-postgres.ts +197 -118
  50. package/src/db/changes-sqlite.ts +131 -64
  51. package/src/db/core.ts +38 -13
  52. package/src/db/index.ts +20 -4
  53. package/src/db/mysql.ts +22 -6
  54. package/src/db/postgres.ts +49 -13
  55. package/src/db/sqlite.ts +53 -27
  56. package/src/derive.d.mts +4 -1
  57. package/src/derive.mjs +136 -39
  58. package/src/dev-runtime.mjs +8 -4
  59. package/src/exec/errors.ts +57 -0
  60. package/src/exec/landing.ts +66 -0
  61. package/src/exec/scope.ts +174 -0
  62. package/src/exec/touch.ts +80 -0
  63. package/src/exec/validation.ts +176 -0
  64. package/src/exec.ts +191 -341
  65. package/src/extension.ts +8 -2
  66. package/src/first-paint.ts +26 -9
  67. package/src/form/value.ts +15 -0
  68. package/src/form.tsx +132 -43
  69. package/src/hub/channels.ts +102 -0
  70. package/src/hub/connections.ts +147 -0
  71. package/src/hub/delivery.ts +62 -0
  72. package/src/hub/feed.ts +168 -0
  73. package/src/hub/presence.ts +64 -0
  74. package/src/hub/route.ts +158 -0
  75. package/src/hub/subscriptions.ts +212 -0
  76. package/src/hub/topic.ts +98 -0
  77. package/src/hub/tracked.ts +12 -0
  78. package/src/hub/types.ts +190 -0
  79. package/src/hub/view.ts +147 -0
  80. package/src/hub.ts +132 -758
  81. package/src/index.ts +84 -10
  82. package/src/inline.mjs +356 -0
  83. package/src/inspect/compare.mjs +162 -0
  84. package/src/inspect/routes.mjs +154 -0
  85. package/src/inspect/samples.mjs +59 -0
  86. package/src/inspect/url.mjs +2 -0
  87. package/src/inspect.mjs +146 -187
  88. package/src/link/election.ts +178 -0
  89. package/src/link/lines.ts +27 -0
  90. package/src/link.ts +90 -166
  91. package/src/list-drizzle.ts +104 -18
  92. package/src/list.ts +127 -36
  93. package/src/listen.ts +32 -6
  94. package/src/live/api.ts +18 -0
  95. package/src/live/backoff.ts +6 -0
  96. package/src/live/frames.ts +42 -0
  97. package/src/live/read.ts +77 -0
  98. package/src/live/sub-types.ts +24 -0
  99. package/src/live/transport.ts +86 -0
  100. package/src/live/types.ts +52 -0
  101. package/src/live-fake.ts +20 -5
  102. package/src/live-target.mjs +112 -37
  103. package/src/live.ts +116 -225
  104. package/src/media.ts +8 -1
  105. package/src/migrations.mjs +14 -6
  106. package/src/mux.ts +161 -64
  107. package/src/og/DefaultCard.tsx +40 -9
  108. package/src/og/fonts.mjs +16 -8
  109. package/src/og/images.mjs +43 -22
  110. package/src/og/index.mjs +119 -43
  111. package/src/og/native.mjs +11 -4
  112. package/src/range.mjs +15 -3
  113. package/src/router.ts +56 -6
  114. package/src/routes.mjs +73 -22
  115. package/src/run/admission.ts +80 -0
  116. package/src/run/boot.tsx +115 -0
  117. package/src/run/cache-presentation.ts +67 -0
  118. package/src/run/cache.ts +254 -0
  119. package/src/run/helpers.ts +197 -0
  120. package/src/run/listeners.ts +155 -0
  121. package/src/run/navigation.ts +382 -0
  122. package/src/run/page-skeleton.tsx +35 -0
  123. package/src/run/prefetch.ts +128 -0
  124. package/src/run/retain-view.ts +18 -0
  125. package/src/run/shape-preparation.ts +97 -0
  126. package/src/run/shape-styles.ts +72 -0
  127. package/src/run/source-context/pin.ts +124 -0
  128. package/src/run/source-context.ts +311 -0
  129. package/src/run.tsx +50 -178
  130. package/src/rust/common.ts +163 -0
  131. package/src/rust/native.ts +282 -0
  132. package/src/rust.ts +103 -229
  133. package/src/server/card.tsx +65 -0
  134. package/src/server/compression.ts +115 -0
  135. package/src/server/core.ts +70 -0
  136. package/src/server/dispatch.ts +70 -0
  137. package/src/server/form.ts +85 -0
  138. package/src/server/functions.ts +129 -0
  139. package/src/server/handler.ts +39 -0
  140. package/src/server/head.ts +96 -0
  141. package/src/server/media.ts +60 -0
  142. package/src/server/navigation-cache.ts +80 -0
  143. package/src/server/navigation.tsx +85 -0
  144. package/src/server/page-cold.ts +95 -0
  145. package/src/server/page-stream.tsx +181 -0
  146. package/src/server/page-types.ts +51 -0
  147. package/src/server/page-warm.ts +130 -0
  148. package/src/server/page.ts +69 -0
  149. package/src/server/payload.ts +89 -0
  150. package/src/server/prerender.ts +60 -0
  151. package/src/server/preview.tsx +67 -0
  152. package/src/server/render.ts +167 -0
  153. package/src/server/request.ts +98 -0
  154. package/src/server/resolver.ts +228 -0
  155. package/src/server/routes.ts +150 -0
  156. package/src/server/services.ts +69 -0
  157. package/src/server/text.ts +29 -0
  158. package/src/server/types.ts +160 -0
  159. package/src/server.tsx +82 -971
  160. package/src/shape.mjs +30 -6
  161. package/src/share/display.ts +50 -0
  162. package/src/share/network.ts +119 -0
  163. package/src/share/receive.ts +70 -0
  164. package/src/share/state.ts +30 -0
  165. package/src/share/tab-queue.ts +28 -0
  166. package/src/share-server.ts +128 -43
  167. package/src/share.ts +103 -209
  168. package/src/sheets.mjs +30 -18
  169. package/src/sidecar/build.mjs +170 -0
  170. package/src/sidecar/cargo.mjs +164 -0
  171. package/src/sidecar/common.mjs +17 -0
  172. package/src/sidecar/dev-build.mjs +91 -0
  173. package/src/sidecar/dev-watch.mjs +88 -0
  174. package/src/sidecar/manifest.mjs +19 -0
  175. package/src/sidecar/process.mjs +58 -0
  176. package/src/sidecar/refresh.mjs +19 -0
  177. package/src/sidecar.mjs +75 -416
  178. package/src/socket/check.ts +50 -0
  179. package/src/socket/deliver.ts +16 -0
  180. package/src/socket/publish.ts +56 -0
  181. package/src/socket/rooms.ts +41 -0
  182. package/src/socket/route.ts +79 -0
  183. package/src/socket/shared.ts +26 -0
  184. package/src/socket/types.ts +48 -0
  185. package/src/socket.ts +205 -194
  186. package/src/stream.ts +10 -1
  187. package/src/verify/app.ts +39 -0
  188. package/src/verify/cleanup.ts +27 -0
  189. package/src/verify/database.ts +11 -0
  190. package/src/verify/live.ts +63 -0
  191. package/src/verify/notify.ts +35 -0
  192. package/src/verify/skip.ts +19 -0
  193. package/src/verify/support.ts +149 -0
  194. package/src/verify/timing.ts +15 -0
  195. package/src/verify/trigger.ts +14 -0
  196. package/src/verify.ts +77 -176
  197. package/src/view.tsx +96 -11
  198. package/src/vite/asset-bytes.mjs +95 -0
  199. package/src/vite/assets.mjs +117 -0
  200. package/src/vite/config-hooks.mjs +166 -0
  201. package/src/vite/dev-hooks.mjs +103 -0
  202. package/src/vite/dev.mjs +102 -0
  203. package/src/vite/entries.mjs +145 -0
  204. package/src/vite/module-hooks.mjs +288 -0
  205. package/src/vite/route-modules.mjs +289 -0
  206. package/src/vite/server-functions.mjs +222 -0
  207. package/src/vite/shape-entry-css.mjs +187 -0
  208. package/src/vite/shape-override.mjs +226 -0
  209. package/src/vite/shared.mjs +51 -0
  210. package/src/vite/sheets.mjs +92 -0
  211. package/src/vite/state.mjs +89 -0
  212. package/src/vite/syntax.mjs +139 -0
  213. package/src/vite.mjs +152 -853
  214. package/src/capture.d.mts +0 -1
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 { table: string; id?: string }
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 { name: string; pk: string[] }
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 => (typeof t === 'string' ? t : (t as Record<symbol, string> | null)?.[NAME] ?? (() => { throw new StartError('R017', `\`dbChanges\` got something that isn't a table: ${String(t)}`, 'pass Drizzle tables, or their names') })())
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 { name: string; primary?: boolean }
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(db: { $weft?: DbHandle }, tables: unknown[], options: ChangeOptions = {}): Source {
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) throw new StartError('R017', '`dbChanges` needs a client from `@weft-js/start/db/sqlite`, `/postgres` or `/mysql`', 'for MongoDB use `mongoChanges` from `@weft-js/db/mongo`; for another client, write a `Source`')
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: () => Promise.all(tables.map(async t => ({ name: nameOf(t), pk: typeof t === 'string' ? [] : await pkOf(t, handle.dialect) }))),
110
- probe: () => load().then(s => s.probe()),
111
- start: (sink, probe) => load().then(s => s.start(sink, probe)),
112
- settle: tags => load().then(s => s.settle?.(tags)),
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(env: Record<string, string | undefined> = (globalThis as unknown as { process?: { env: Record<string, string | undefined> } }).process?.env ?? {}): string | undefined {
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 { t: string[]; /** Tag prefixes: `todos:` for every row of a table-level change. */ p?: string[]; /** Tags a barrier covered: a subscription with one of them is poked already. */ k?: string[] }
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,>(t: T) => ((t as { unref?(): void } | null | undefined)?.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(source: Source, out: FeedOut, { hold = source.hold ?? 25, throttle = 250, barrier = 1000 } = {}): Feed {
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>(), prefixes = new Set<string>(), covered = 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], p = [...prefixes], k = [...covered]
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(setTimeout(flush, Math.max(Math.min(hold, first + throttle - now), last + throttle - now)))
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
- get running() { return running },
223
- covers(tag) {
224
- const at = tag.indexOf(':')
225
- return known.has(at < 0 ? tag : tag.slice(0, at))
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
- async settle(tags) {
246
- const ours = tags.filter(t => this.covers(t))
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
+ }