@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/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 {
|
|
7
|
-
import {
|
|
8
|
-
|
|
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 =
|
|
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>(
|
|
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<
|
|
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?: {
|
|
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: {
|
|
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>(
|
|
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 {
|
|
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(
|
|
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]): {
|
|
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<
|
|
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)
|
|
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',
|
|
162
|
+
kind: 'channel',
|
|
163
|
+
$id: id,
|
|
164
|
+
[FN]: fn,
|
|
103
165
|
connect: (params?: unknown, options?: ConnectOptions) => connect(fn, params, options),
|
|
104
|
-
in: (params?: unknown) => ({
|
|
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: {
|
|
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> & {
|
|
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>(
|
|
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))
|
|
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',
|
|
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(
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
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)
|