@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/channel.ts CHANGED
@@ -3,9 +3,22 @@
3
3
  // finds its messages. The socket server is socket.ts, and `room.in(params).publish` reaches it through
4
4
  // `runtime.channels`, so nothing of the server ships here.
5
5
 
6
- import { Box, DEV, batch, collect as boxes, createRoot, derived, effect, getOwner, onCleanup, state, untrack, type Derived, type State } from '@weft-js/core'
7
- import { HANDOVER, SOCKETS, openLine, socketDown } from './link'
8
- import { currentScope, fns, FnError, keyOf, net, rt, runtime, skip, StartError, track, type Context, type Fn, type Skip, type StandardSchemaV1 } from './data'
6
+ import { type Derived, type State } from '@weft-js/core'
7
+ import {
8
+ fns,
9
+ runtime,
10
+ StartError,
11
+ track,
12
+ type Context,
13
+ type Fn,
14
+ type Skip,
15
+ type StandardSchemaV1,
16
+ } from './data'
17
+ import { connect, shared, connectBuiltIn } from './channel/connections'
18
+ import { keepOpen, wakeSocket, setFake, type Frame, type SocketLink } from './channel/socket'
19
+
20
+ export { connectBuiltIn, keepOpen, wakeSocket }
21
+ export type { Frame, SocketLink }
9
22
 
10
23
  // ---- definitions ----------------------------------------------------------------------------------------------------
11
24
 
@@ -19,7 +32,13 @@ type Outputs<T extends Schemas> = { [K in keyof T]: Out<T[K]> }
19
32
  * Where a connection stands; `unauthorized` waits for `rejoin`. `unavailable`: this deploy can't hold a socket (a
20
33
  * function host, R024), so nothing connects and nothing is retried.
21
34
  */
22
- export type ChannelStatus = 'connecting' | 'open' | 'reconnecting' | 'closed' | 'unauthorized' | 'unavailable'
35
+ export type ChannelStatus =
36
+ | 'connecting'
37
+ | 'open'
38
+ | 'reconnecting'
39
+ | 'closed'
40
+ | 'unauthorized'
41
+ | 'unavailable'
23
42
 
24
43
  /** `room.publish`: who a message goes to, and whether it can be replayed. */
25
44
  export interface PublishOptions {
@@ -35,11 +54,19 @@ export interface PublishOptions {
35
54
  export interface Room<P, M> {
36
55
  params: P
37
56
  /** To everyone in the room, on every instance. */
38
- publish<K extends keyof M & string>(name: K, message: M[K], options?: PublishOptions): Promise<void>
57
+ publish<K extends keyof M & string>(
58
+ name: K,
59
+ message: M[K],
60
+ options?: PublishOptions,
61
+ ): Promise<void>
39
62
  }
40
63
 
41
64
  /** `channel()`: the params, the messages each side may send, and who may join. */
42
- export interface ChannelDef<P extends StandardSchemaV1 | undefined, C extends Schemas, S extends Schemas> {
65
+ export interface ChannelDef<
66
+ P extends StandardSchemaV1 | undefined,
67
+ C extends Schemas,
68
+ S extends Schemas,
69
+ > {
43
70
  params?: P
44
71
  /** Required (R001). Runs on every join and at each session re-check. `() => true` must be written out. */
45
72
  authorize: (args: { ctx: Context; params: Out<P> }) => boolean | Promise<boolean>
@@ -48,9 +75,22 @@ export interface ChannelDef<P extends StandardSchemaV1 | undefined, C extends Sc
48
75
  /** Server → client, validated in dev and tests. */
49
76
  server?: S
50
77
  /** `id` is the sender's connection, the same one `left` gets. */
51
- on?: { [K in keyof C]?: (args: { msg: Out<C[K]>; id: string; ctx: Context; params: Out<P>; room: Room<Out<P>, Inputs<S>> }) => unknown }
78
+ on?: {
79
+ [K in keyof C]?: (args: {
80
+ msg: Out<C[K]>
81
+ id: string
82
+ ctx: Context
83
+ params: Out<P>
84
+ room: Room<Out<P>, Inputs<S>>
85
+ }) => unknown
86
+ }
52
87
  /** A connection stopped being in the room: it left, closed, or lost `authorize`. Not called if its instance dies. */
53
- left?: (args: { id: string; ctx: Context; params: Out<P>; room: Room<Out<P>, Inputs<S>> }) => unknown
88
+ left?: (args: {
89
+ id: string
90
+ ctx: Context
91
+ params: Out<P>
92
+ room: Room<Out<P>, Inputs<S>>
93
+ }) => unknown
54
94
  limits?: { perSecond?: number }
55
95
  $id?: string
56
96
  }
@@ -65,7 +105,10 @@ export interface Connection<C, S> {
65
105
  /** Ticks when the server couldn't resume: messages may be missing, so refetch what the room's history is. */
66
106
  readonly resets: Derived<number>
67
107
  /** Keep a room's messages of one name, in the cache: a remount within `keep` finds them. `key` drops a repeat. */
68
- collect<K extends keyof S & string>(name: K, options?: { max?: number; key?: (message: S[K]) => unknown }): Derived<S[K][]>
108
+ collect<K extends keyof S & string>(
109
+ name: K,
110
+ options?: { max?: number; key?: (message: S[K]) => unknown },
111
+ ): Derived<S[K][]>
69
112
  /** Each message of one name, until the owner is disposed. */
70
113
  on<K extends keyof S & string>(name: K, fn: (message: S[K]) => void): () => void
71
114
  onReset(fn: () => void): () => void
@@ -77,14 +120,22 @@ export interface Connection<C, S> {
77
120
 
78
121
  type Arg<P> = P | (() => P | Skip)
79
122
  /** How long a join outlives the last component using it. */
80
- export interface ConnectOptions { keep?: string | number }
123
+ export interface ConnectOptions {
124
+ keep?: string | number
125
+ }
81
126
 
82
127
  /** What `channel()` returns: connect from a page, publish from the server. */
83
128
  export interface Channel<P, C, S> {
84
129
  /** In a component or scope. A function argument re-joins when what it reads changes; `skip` leaves. */
85
- connect(...args: [P] extends [void] ? [params?: undefined | (() => undefined | Skip), options?: ConnectOptions] : [params: Arg<P>, options?: ConnectOptions]): Connection<C, S>
130
+ connect(
131
+ ...args: [P] extends [void]
132
+ ? [params?: undefined | (() => undefined | Skip), options?: ConnectOptions]
133
+ : [params: Arg<P>, options?: ConnectOptions]
134
+ ): Connection<C, S>
86
135
  /** On the server, from a mutation or a job. */
87
- in(...args: [P] extends [void] ? [] : [params: P]): { publish<K extends keyof S & string>(name: K, message: S[K]): Promise<void> }
136
+ in(...args: [P] extends [void] ? [] : [params: P]): {
137
+ publish<K extends keyof S & string>(name: K, message: S[K]): Promise<void>
138
+ }
88
139
  readonly kind: 'channel'
89
140
  readonly $id: string
90
141
  }
@@ -93,15 +144,29 @@ const FN = Symbol.for('weft.fn')
93
144
  let auto = 0
94
145
 
95
146
  /** A named room of schema-checked messages over the shared socket. */
96
- export function channel<P extends StandardSchemaV1 | undefined = undefined, C extends Schemas = {}, S extends Schemas = {}>(def: ChannelDef<P, C, S>): Channel<In<P>, Inputs<C>, Outputs<S>> {
147
+ export function channel<
148
+ P extends StandardSchemaV1 | undefined = undefined,
149
+ C extends Schemas = {},
150
+ S extends Schemas = {},
151
+ >(def: ChannelDef<P, C, S>): Channel<In<P>, Inputs<C>, Outputs<S>> {
97
152
  const id = def.$id ?? `c${++auto}`
98
- if (!(def as { $stub?: boolean }).$stub && !def.authorize) throw new StartError('R001', `\`${id}\` has no \`authorize\`: anyone could join it`, 'add `authorize` (`() => true` must be written out)')
153
+ if (!(def as { $stub?: boolean }).$stub && !def.authorize)
154
+ throw new StartError(
155
+ 'R001',
156
+ `\`${id}\` has no \`authorize\`: anyone could join it`,
157
+ 'add `authorize` (`() => true` must be written out)',
158
+ )
99
159
  const fn: Fn = { kind: 'channel', $id: id, def: def as Fn['def'] }
100
160
  fns.set(id, fn)
101
161
  return {
102
- kind: 'channel', $id: id, [FN]: fn,
162
+ kind: 'channel',
163
+ $id: id,
164
+ [FN]: fn,
103
165
  connect: (params?: unknown, options?: ConnectOptions) => connect(fn, params, options),
104
- in: (params?: unknown) => ({ publish: (name: string, message: unknown) => runtime.channels!.publish(fn, params, name, message) }),
166
+ in: (params?: unknown) => ({
167
+ publish: (name: string, message: unknown) =>
168
+ runtime.channels!.publish(fn, params, name, message),
169
+ }),
105
170
  } as never
106
171
  }
107
172
 
@@ -112,12 +177,18 @@ export interface SharedDef<K extends StandardSchemaV1, V extends StandardSchemaV
112
177
  /** What a reader has until the server says otherwise. Kept in the client bundle, so plain data. */
113
178
  initial: Out<V>
114
179
  /** Both required (R001). */
115
- authorize: { read: (args: { ctx: Context; key: Out<K> }) => boolean | Promise<boolean>; write: (args: { ctx: Context; key: Out<K> }) => boolean | Promise<boolean> }
180
+ authorize: {
181
+ read: (args: { ctx: Context; key: Out<K> }) => boolean | Promise<boolean>
182
+ write: (args: { ctx: Context; key: Out<K> }) => boolean | Promise<boolean>
183
+ }
116
184
  $id?: string
117
185
  }
118
186
 
119
187
  /** A box in a scope: reads as a value, and a write is optimistic, sent, and last writer wins on the server. */
120
- export type SharedBox<T> = State<T> & { readonly status: Derived<ChannelStatus>; readonly error: unknown }
188
+ export type SharedBox<T> = State<T> & {
189
+ readonly status: Derived<ChannelStatus>
190
+ readonly error: unknown
191
+ }
121
192
 
122
193
  /** What `sharedState()` returns: read a value on a page, set it from the server. */
123
194
  export interface SharedState<K, V> {
@@ -129,494 +200,51 @@ export interface SharedState<K, V> {
129
200
  }
130
201
 
131
202
  /** One value per key that every client holding that key sees change. */
132
- export function sharedState<K extends StandardSchemaV1, V extends StandardSchemaV1>(name: string, def: SharedDef<K, V>): SharedState<In<K>, Out<V>> {
203
+ export function sharedState<K extends StandardSchemaV1, V extends StandardSchemaV1>(
204
+ name: string,
205
+ def: SharedDef<K, V>,
206
+ ): SharedState<In<K>, Out<V>> {
133
207
  const id = def.$id ?? `shared:${name}`
134
- if (!(def as { $stub?: boolean }).$stub && (!def.authorize?.read || !def.authorize?.write)) throw new StartError('R001', `\`${id}\` needs \`authorize: { read, write }\`: anyone could read or write it`, '`() => true` must be written out')
208
+ if (!(def as { $stub?: boolean }).$stub && (!def.authorize?.read || !def.authorize?.write))
209
+ throw new StartError(
210
+ 'R001',
211
+ `\`${id}\` needs \`authorize: { read, write }\`: anyone could read or write it`,
212
+ '`() => true` must be written out',
213
+ )
135
214
  const fn: Fn = { kind: 'shared', $id: id, def }
136
215
  fns.set(id, fn)
137
216
  return {
138
- kind: 'shared', $id: id, [FN]: fn,
217
+ kind: 'shared',
218
+ $id: id,
219
+ [FN]: fn,
139
220
  connect: (key: unknown, options?: ConnectOptions) => shared(fn, key, options),
140
221
  in: (key: unknown) => ({ set: (value: unknown) => runtime.channels!.set(fn, key, value) }),
141
222
  } as never
142
223
  }
143
224
 
144
- // ---- joins ----------------------------------------------------------------------------------------------------------
145
-
146
- interface ListSpec { name: string; max: number; key?: ((m: never) => unknown) | undefined }
147
- interface List extends ListSpec { box: State<unknown[]> }
148
- interface Handle { on: Map<string, Set<(m: never) => void>>; resets: Set<() => void>; value?(v: { v: unknown; at: number }): void }
149
- interface Join {
150
- n: number
151
- key: string
152
- fn: Fn
153
- params: unknown
154
- refs: number
155
- timer?: ReturnType<typeof setTimeout> | undefined
156
- cursor?: string | undefined
157
- joined: boolean
158
- refused: boolean
159
- status: State<ChannelStatus>
160
- error: State<unknown>
161
- resets: State<number>
162
- lists: Map<string, List>
163
- handles: Set<Handle>
164
- /** `send`s waiting for the join. */
165
- waiting: Set<() => void>
166
- }
167
-
168
- const joins = new Map<string, Join>()
169
- const byN = new Map<number, Join>()
170
- let nth = 0
171
-
172
- const KEEP = 30_000
173
- const ms = (keep: string | number = KEEP): number => {
174
- if (typeof keep === 'number') return keep
175
- const m = /^(\d+)(ms|s|m)$/.exec(keep)
176
- return m ? +m[1]! * { ms: 1, s: 1000, m: 60_000 }[m[2] as 'ms' | 's' | 'm'] : Infinity
177
- }
178
-
179
- function acquire(fn: Fn, params: unknown): Join {
180
- const key = keyOf(fn.$id, params)
181
- let j = joins.get(key)
182
- if (!j) {
183
- j = createRoot(() => ({ n: ++nth, key, fn, params, refs: 0, joined: false, refused: false, status: state<ChannelStatus>('connecting'), error: state<unknown>(undefined), resets: state(0), lists: new Map(), handles: new Set(), waiting: new Set() }), null)
184
- if (DEV && net() && !net()!.resets.includes(reset)) net()!.resets.push(reset)
185
- joins.set(key, j)
186
- byN.set(j.n, j)
187
- if (live) enter(j)
188
- else start()
189
- }
190
- clearTimeout(j.timer)
191
- j.timer = undefined
192
- j.refs++
193
- return j
194
- }
195
-
196
- function release(j: Join, keep?: string | number) {
197
- if (--j.refs) return
198
- const t = ms(keep)
199
- if (t === Infinity) return
200
- if (t) j.timer = setTimeout(() => drop(j), t)
201
- else drop(j)
202
- }
203
-
204
- function drop(j: Join) {
205
- joins.delete(j.key)
206
- byN.delete(j.n)
207
- j.status.value = 'closed'
208
- if (live) send(['l', j.n])
209
- for (const w of Array.from(j.waiting)) w()
210
- if (!joins.size) live?.close()
211
- }
212
-
213
- function hold(j: Join, keep?: string | number): () => void {
214
- const token = {}
215
- const stop = () => {
216
- net()?.open.delete(token)
217
- release(j, keep)
218
- }
219
- if (DEV) net()?.open.set(token, [`channel \`${j.key}\``, stop])
220
- return stop
221
- }
222
-
223
- function listOf(j: Join, spec: ListSpec): List {
224
- const id = `${spec.name}:${spec.max}:${spec.key ? String(spec.key) : ''}`
225
- let l = j.lists.get(id)
226
- if (!l) j.lists.set(id, (l = createRoot(() => ({ ...spec, box: state<unknown[]>([]) }), null)))
227
- return l
228
- }
229
-
230
- const NONE: never[] = []
231
- type View = Join | undefined
232
-
233
- /** The part `connect` and `shared` have in common: the current join, as the argument moves. */
234
- function follow(fn: Fn, arg: unknown, options: ConnectOptions | undefined, handle: Handle, joined?: (j: Join) => void): State<View> {
235
- const view = state<View>(undefined)
236
- effect(() => {
237
- const params = typeof arg === 'function' ? (arg as () => unknown)() : arg
238
- if (params === skip) return undefined
239
- return untrack(() => {
240
- const j = acquire(fn, params)
241
- j.handles.add(handle)
242
- joined?.(j)
243
- const stop = hold(j, options?.keep)
244
- onCleanup(() => {
245
- j.handles.delete(handle)
246
- stop()
247
- })
248
- return j
249
- })
250
- }, now => { view.value = now })
251
- return view
252
- }
253
-
254
- function owned(fn: Fn) {
255
- if (!getOwner()) throw new StartError('W051', DEV ? `\`${fn.$id}.connect()\` was called outside a component or scope: nothing would close it` : fn.$id, DEV ? 'call it in a component, a `defineState` setup or `createRoot(dispose => …)`' : undefined)
256
- }
257
-
258
- /** `@weft-js/start/share`'s way in: a join that nothing on the page declared, so a tab shares this socket and its lifecycle. */
259
- export const connectBuiltIn = ($id: string) => connect({ kind: 'channel', $id, def: {} }, undefined, { keep: 0 })
260
-
261
- function connect(fn: Fn, arg: unknown, options?: ConnectOptions): Connection<Record<string, never>, Record<string, never>> {
262
- owned(fn)
263
- if (currentScope()) {
264
- const closed = { value: 'connecting' as ChannelStatus }
265
- return { status: closed, room: { value: undefined }, error: undefined, resets: { value: 0 }, collect: () => ({ value: NONE }), on: () => () => {}, onReset: () => () => {}, send: () => Promise.reject(new FnError({ message: 'a channel has no socket on the server' }, 500)), retry() {} }
266
- }
267
- const handle: Handle = { on: new Map(), resets: new Set() }
268
- const wanted: ListSpec[] = []
269
- const view = follow(fn, arg, options, handle, j => { for (const w of wanted) listOf(j, w) })
270
- return {
271
- status: derived(() => view.value?.status.value ?? 'closed'),
272
- room: derived(() => view.value?.key),
273
- get error() { return view.value?.error.value },
274
- resets: derived(() => view.value?.resets.value ?? 0),
275
- collect(name, { max = 200, key } = {}) {
276
- const spec: ListSpec = { name, max, key }
277
- wanted.push(spec)
278
- const had = untrack(() => view.value)
279
- if (had) listOf(had, spec)
280
- return derived(() => {
281
- const j = view.value
282
- return (j ? listOf(j, spec).box.value : NONE) as never[]
283
- })
284
- },
285
- on(name, listener) {
286
- const set = handle.on.get(name) ?? new Set()
287
- handle.on.set(name, set)
288
- set.add(listener)
289
- const stop = () => void set.delete(listener)
290
- onCleanup(stop)
291
- return stop
292
- },
293
- onReset(listener) {
294
- handle.resets.add(listener)
295
- const stop = () => void handle.resets.delete(listener)
296
- onCleanup(stop)
297
- return stop
298
- },
299
- send(name, message) {
300
- const j = untrack(() => view.value)
301
- return j ? post(j, name, message) : Promise.reject(new FnError({ message: 'this channel is closed' }, 400))
302
- },
303
- retry() {
304
- const j = untrack(() => view.value)
305
- if (j?.refused) rejoin(j)
306
- },
307
- }
308
- }
309
-
310
- /** `Box.w` is the one way a write gets in, so a subclass sees every write, and `Instance` never touches it. */
311
- class Shared<T> extends Box<T> {
312
- declare remote?: boolean
313
- declare pending: number
314
- declare base: { v: T; at: number }
315
- declare view: State<View>
316
- declare statusOf: Derived<ChannelStatus>
317
- declare problem: State<unknown>
318
- get status() { return this.statusOf }
319
- get error() { return this.problem.value }
320
- /** A server value: applied unless a local write is still on its way. */
321
- apply(v: { v: T; at: number }) {
322
- if (v.at >= this.base.at) this.base = v
323
- if (!this.pending) this.quiet(this.base.v)
324
- }
325
- quiet(v: T) {
326
- this.remote = true
327
- this.w(v)
328
- this.remote = false
329
- }
330
- override w(v: T) {
331
- if (this.remote) return super.w(v)
332
- const j = untrack(() => this.view.value)
333
- if (!j) return super.w(v)
334
- super.w(v)
335
- this.pending++
336
- this.problem.value = undefined
337
- void track(post(j, '$set', v).then(() => { this.pending-- }, e => {
338
- this.pending--
339
- this.problem.value = e
340
- }).finally(() => { if (!this.pending) this.quiet(this.base.v) }))
341
- }
342
- }
343
-
344
- function shared(fn: Fn, arg: unknown, options?: ConnectOptions): SharedBox<unknown> {
345
- owned(fn)
346
- const initial = fn.def.initial
347
- const [b] = boxes(() => new Shared<unknown>(initial))
348
- b.pending = 0
349
- b.base = { v: initial, at: 0 }
350
- b.problem = state<unknown>(undefined)
351
- if (currentScope()) {
352
- b.statusOf = { value: 'connecting' }
353
- b.view = state<View>(undefined)
354
- return b
355
- }
356
- const handle: Handle = { on: new Map(), resets: new Set(), value: v => b.apply(v) }
357
- b.view = follow(fn, arg, options, handle, () => { b.base = { v: initial, at: 0 } })
358
- b.statusOf = derived(() => b.view.value?.status.value ?? 'closed')
359
- return b
360
- }
361
-
362
- // ---- sends ----------------------------------------------------------------------------------------------------------
363
-
364
- const ACK_WAIT = 10_000
365
- const acks = new Map<number, { ok: () => void; no: (e: unknown) => void; n: number }>()
366
- let ackId = 0
367
-
368
- async function post(j: Join, name: string, message: unknown): Promise<void> {
369
- if (!SOCKETS && !(DEV && net())) throw new FnError({ code: 'R024', message: "this deploy can't hold a socket: not sent" }, 503)
370
- if (!j.joined && !j.refused) {
371
- await new Promise<void>(ok => {
372
- const done = () => {
373
- clearTimeout(t)
374
- j.waiting.delete(done)
375
- ok()
376
- }
377
- const t = setTimeout(done, ACK_WAIT)
378
- j.waiting.add(done)
379
- })
380
- }
381
- if (!j.joined) throw new FnError({ ...(j.refused && { code: 'R011' }), message: j.refused ? 'not allowed in this channel' : 'no connection: not sent' }, j.refused ? 403 : 503)
382
- const ack = ++ackId
383
- const p = new Promise<void>((ok, no) => { acks.set(ack, { ok, no, n: j.n }) })
384
- send(['m', j.n, ack, name, message])
385
- return track(p)
386
- }
387
-
388
- // ---- the socket ------------------------------------------------------------------------------------------------------
389
-
390
- /** What the server sends a channel's client: the first element says which. */
391
- export type Frame =
392
- | ['h', { b?: string }]
393
- | ['j', number, string | undefined]
394
- | ['m', number, string | undefined, string, never]
395
- | ['r', number, string | undefined]
396
- | ['e', number, { code?: string; message?: string }]
397
- | ['a', number, number, { code?: string; message?: string } | undefined]
398
-
399
- /** One end of a socket, as a channel holds it. */
400
- export interface SocketLink {
401
- send(text: string): void
402
- close(): void
403
- }
404
-
405
- /** This tab's line on the browser's one socket (link.ts). `done` gets the close code. */
406
- function browser(onFrame: (d: Frame) => void, closed: (code: number) => void): SocketLink {
407
- return openLine('c', '', text => onFrame(JSON.parse(text)), closed)
408
- }
409
-
410
225
  /** @weft-js/core/test only: an opaque specifier, so a bundler keeps neither the fake nor the socket server (research 30 §10). */
411
226
  const FAKE = './channel-fake.ts' // with its extension: nothing resolves this one for a linked package
412
227
  function fake(onFrame: (d: Frame) => void, closed: (code: number) => void): SocketLink {
413
228
  let link: SocketLink | undefined
414
229
  let up!: () => void
415
- void track(new Promise<void>(ok => { up = ok }))
416
- const loading = import(/* @vite-ignore */ FAKE).then(m => m.open(onFrame, closed, up)).then(l => (link = l), e => (up(), closed(1006), Promise.reject(e))).catch(() => {})
417
- return { send: t => void track(loading.then(() => link?.send(t))), close: () => { void loading.then(() => link?.close()) } }
418
- }
419
-
420
- const BASE = 500, CAP = 30_000, PING = 20_000, DEAD = 45_000, HIDDEN = 300_000
421
-
422
- let live: SocketLink | undefined
423
- let open = false
424
- let running = false
425
- let attempt = 0
426
- let build: string | undefined
427
- let nudge: (() => void) | undefined
428
- let parked = false
429
- let hiddenAt: ReturnType<typeof setTimeout> | undefined
430
-
431
- /** A leader tab can be hidden while a sibling is looking: `@weft-js/start/share` says when this socket must stay. */
432
- export const keepOpen: { stay?: (() => boolean) | undefined } = {}
433
-
434
- const send = (f: unknown[]) => live?.send(JSON.stringify(f))
435
- const statusAll = (s: ChannelStatus) => { for (const j of joins.values()) if (!j.refused) j.status.value = s }
436
-
437
- function enter(j: Join) {
438
- if (!open || !live) return
439
- send(['j', j.n, j.fn.$id, j.params, j.cursor])
440
- }
441
-
442
- function rejoin(j: Join) {
443
- j.refused = false
444
- j.status.value = 'connecting'
445
- if (running) enter(j)
446
- else start()
447
- }
448
-
449
- function start() {
450
- if (running) return
451
- // a function host: no socket to dial, so say so once instead of retrying forever
452
- if (!SOCKETS && !(DEV && net())) return statusAll('unavailable')
453
- running = true
454
- queueMicrotask(() => void run())
455
- }
456
-
457
- function dispatch(j: Join, name: string, msg: never) {
458
- if (name === '$v') {
459
- for (const h of j.handles) h.value?.(msg)
460
- return
461
- }
462
- for (const l of j.lists.values()) {
463
- if (l.name !== name) continue
464
- const was = untrack(() => l.box.value)
465
- const key = l.key?.(msg)
466
- const at = l.key ? was.findIndex(m => l.key!(m as never) === key) : -1
467
- const next = at >= 0 ? was.map((m, i) => (i === at ? msg : m)) : [...was, msg]
468
- l.box.value = next.length > l.max ? next.slice(-l.max) : next
469
- }
470
- for (const h of j.handles) for (const f of h.on.get(name) ?? []) f(msg)
471
- }
472
-
473
- function frame(d: Frame) {
474
- batch(() => {
475
- const j = typeof d[1] === 'number' ? byN.get(d[1]) : undefined
476
- if (d[0] === 'h') {
477
- if (build && d[1].b && d[1].b !== build) rt.stale.value = true
478
- build = d[1].b
479
- open = true
480
- attempt = 0
481
- for (const x of joins.values()) if (!x.refused) enter(x)
482
- } else if (d[0] === 'j' && j) {
483
- j.cursor ??= d[2]
484
- j.joined = true
485
- j.error.value = undefined
486
- j.status.value = 'open'
487
- for (const w of Array.from(j.waiting)) w()
488
- } else if (d[0] === 'm' && j) {
489
- j.cursor = d[2]
490
- dispatch(j, d[3], d[4])
491
- } else if (d[0] === 'r' && j) {
492
- j.cursor = d[2]
493
- for (const l of j.lists.values()) l.box.value = []
494
- j.resets.value++
495
- for (const h of j.handles) for (const f of h.resets) f()
496
- } else if (d[0] === 'e' && j) {
497
- j.joined = false
498
- j.refused = true
499
- j.error.value = new FnError(d[2], d[2].code === 'R011' ? 403 : 400)
500
- j.status.value = d[2].code === 'R011' ? 'unauthorized' : 'closed'
501
- for (const w of Array.from(j.waiting)) w()
502
- } else if (d[0] === 'a') {
503
- const p = acks.get(d[2])
504
- acks.delete(d[2])
505
- if (d[3]) p?.no(new FnError(d[3], d[3].code === 'R011' ? 403 : 400))
506
- else p?.ok()
507
- }
508
- })
509
- }
510
-
511
- const sleep = (t: number) => new Promise<void>(ok => {
512
- const timer = setTimeout(ok, t)
513
- nudge = () => {
514
- clearTimeout(timer)
515
- nudge = undefined
516
- ok()
517
- }
518
- })
519
-
520
- async function run() {
521
- listen(true)
522
- const fakeNet = () => (DEV ? net() : undefined)
523
- while (joins.size) {
524
- if (parked || ! navigator.onLine) {
525
- statusAll('reconnecting')
526
- await sleep(CAP)
527
- continue
528
- }
529
- let code = 0
530
- let dead: ReturnType<typeof setTimeout> | undefined
531
- let ping: ReturnType<typeof setInterval> | undefined
532
- const alive = () => {
533
- clearTimeout(dead)
534
- if (!fakeNet()) dead = setTimeout(() => live?.close(), DEAD)
535
- }
536
- await new Promise<void>(done => {
537
- const take = (d: Frame) => {
538
- alive()
539
- frame(d)
540
- }
541
- live = (fakeNet() ? fake : browser)(take, c => {
542
- code = c
543
- done()
544
- })
545
- alive()
546
- if (!fakeNet()) ping = setInterval(() => send(['p']), PING)
547
- })
548
- clearTimeout(dead)
549
- clearInterval(ping)
550
- live = undefined
551
- open = false
552
- for (const j of joins.values()) j.joined = false
553
- for (const [id, p] of acks) {
554
- acks.delete(id)
555
- p.no(new FnError({ message: 'the connection dropped: this may or may not have been sent' }, 503))
556
- }
557
- if (!joins.size) break
558
- const n = fakeNet()
559
- if (n) n.reconnects++
560
- if (code === 4401) for (const j of joins.values()) j.status.value = 'unauthorized'
561
- else statusAll('reconnecting')
562
- if (!n) await sleep(code === HANDOVER ? 0 : backoff())
230
+ void track(
231
+ new Promise<void>((ok) => {
232
+ up = ok
233
+ }),
234
+ )
235
+ const loading = import(/* @vite-ignore */ FAKE)
236
+ .then((m) => m.open(onFrame, closed, up))
237
+ .then(
238
+ (l) => (link = l),
239
+ (e) => (up(), closed(1006), Promise.reject(e)),
240
+ )
241
+ .catch(() => {})
242
+ return {
243
+ send: (t) => void track(loading.then(() => link?.send(t))),
244
+ close: () => {
245
+ void loading.then(() => link?.close())
246
+ },
563
247
  }
564
- running = false
565
- attempt = 0
566
- listen(false)
567
248
  }
568
249
 
569
- /** Full jitter, 0.5 s doubling to 30 s (research 30 §6). */
570
- const backoff = () => Math.random() * Math.min(CAP, BASE * 2 ** attempt++)
571
-
572
- // ---- the page -------------------------------------------------------------------------------------------------------
573
-
574
- const wake = () => {
575
- parked = false
576
- nudge?.()
577
- }
578
- function park() {
579
- parked = true
580
- live?.close()
581
- }
582
- const parkHidden = () => {
583
- if (keepOpen.stay?.()) hiddenAt = setTimeout(parkHidden, HIDDEN)
584
- else park()
585
- }
586
- function onVisibility() {
587
- clearTimeout(hiddenAt)
588
- if (document.visibilityState === 'hidden') hiddenAt = setTimeout(parkHidden, HIDDEN)
589
- else wake()
590
- }
591
- /** A sibling came back while this tab's socket was parked. */
592
- export const wakeSocket = () => wake()
593
- const onOffline = () => {
594
- socketDown()
595
- live?.close()
596
- }
597
-
598
- function listen(on: boolean) {
599
- if (typeof window === 'undefined') return
600
- const m = on ? 'addEventListener' : 'removeEventListener'
601
- window[m]('online', wake)
602
- window[m]('offline', onOffline)
603
- window[m]('pagehide', park)
604
- window[m]('pageshow', wake)
605
- document[m]('visibilitychange', onVisibility)
606
- if (!on) clearTimeout(hiddenAt)
607
- }
608
-
609
- /** @weft-js/core/test, after each test. */
610
- function reset() {
611
- for (const j of joins.values()) clearTimeout(j.timer)
612
- joins.clear()
613
- byN.clear()
614
- acks.clear()
615
- live?.close()
616
- nudge?.()
617
- build = undefined
618
- parked = false
619
- attempt = 0
620
- net()!.reconnects = 0
621
- untrack(() => { rt.stale.value = false })
622
- }
250
+ setFake(fake)