@abide/abide 0.110.4 → 0.111.0
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/CHANGELOG.md +13 -0
- package/docs/REGISTRY.md +22 -21
- package/docs/RULEBOOK.md +85 -19
- package/docs/abide.md +38 -24
- package/package.json +1 -1
- package/src/compiler/compileApp.ts +2 -1
- package/src/server/BRIDGE_VERSION.ts +1 -1
- package/src/server/HEARTBEAT_MILLISECONDS.ts +2 -0
- package/src/server/IDLE_TIMEOUT_SECONDS.ts +6 -0
- package/src/server/answerRequest.ts +44 -3
- package/src/server/answerRpc.ts +14 -41
- package/src/server/answerSocket.ts +2 -2
- package/src/server/answerValue.ts +15 -3
- package/src/server/answerWorker.ts +195 -0
- package/src/server/appManifest.ts +48 -0
- package/src/server/assistant.ts +1 -1
- package/src/server/bridgeRelay.ts +4 -50
- package/src/server/claudeApi.ts +1 -1
- package/src/server/claudeCode.ts +3 -3
- package/src/server/clientBundle.ts +7 -0
- package/src/server/config.ts +14 -0
- package/src/server/createApp.ts +28 -4
- package/src/server/devWorker.ts +4 -1
- package/src/server/requestScope.ts +31 -11
- package/src/server/responseRelay.ts +7 -7
- package/src/server/serveBuild.ts +2 -0
- package/src/server/serveDev.ts +4 -1
- package/src/server/streamEvent.ts +60 -1
- package/src/shared/ADDRESSES.ts +11 -0
- package/src/shared/LONGEST_DELAY.ts +2 -0
- package/src/shared/OFFLINE_CACHES.ts +12 -0
- package/src/shared/OFFLINE_META.ts +7 -0
- package/src/shared/answerOf.ts +29 -4
- package/src/shared/attributeText.ts +1 -1
- package/src/shared/reactive/Expiry.ts +13 -1
- package/src/shared/reactive/Reactive.ts +13 -3
- package/src/shared/safeUrl.ts +8 -1
- package/src/ui/Router.ts +25 -0
- package/src/ui/registerWorker.ts +34 -0
- package/src/ui/routing.ts +1 -0
- package/src/ui/socketWire.ts +33 -2
package/src/server/serveBuild.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
PUBLIC_DIRECTORY,
|
|
25
25
|
} from './CLIENT_BUILD.ts'
|
|
26
26
|
import { bundleServer, type ServedFile } from './clientBundle.ts'
|
|
27
|
+
import { settingOn } from './config.ts'
|
|
27
28
|
import type { Manifest } from './manifestOf.ts'
|
|
28
29
|
import { namesUnder } from './namesUnder.ts'
|
|
29
30
|
import { ENCODINGS } from './negotiateEncoding.ts'
|
|
@@ -137,6 +138,7 @@ export function builtSources(parts: BuildParts): Promise<AppSources> {
|
|
|
137
138
|
files,
|
|
138
139
|
'public, max-age=31536000, immutable',
|
|
139
140
|
),
|
|
141
|
+
offline: settingOn('ABIDE_OFFLINE', true),
|
|
140
142
|
},
|
|
141
143
|
})
|
|
142
144
|
}
|
package/src/server/serveDev.ts
CHANGED
|
@@ -31,6 +31,7 @@ import { DEV_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
|
31
31
|
import type { Compilation } from './abideModules.ts'
|
|
32
32
|
import { DEFAULT_PORT } from './DEFAULT_PORT.ts'
|
|
33
33
|
import type { FromWorker, ToWorker } from './devWorker.ts'
|
|
34
|
+
import { IDLE_TIMEOUT_SECONDS } from './IDLE_TIMEOUT_SECONDS.ts'
|
|
34
35
|
import { muxSocketOptions, sendOrClose, upgraded } from './muxUpgrade.ts'
|
|
35
36
|
import { placeOf } from './placement.ts'
|
|
36
37
|
import { plainAnswer } from './plainAnswer.ts'
|
|
@@ -77,7 +78,7 @@ export async function serveDev(
|
|
|
77
78
|
options: DevOptions,
|
|
78
79
|
): Promise<Bun.Server<undefined>> {
|
|
79
80
|
// 38.24, 38.35 — every request, asked of the worker there is and streamed back a chunk per read.
|
|
80
|
-
// 16.
|
|
81
|
+
// 16.23 — a request's idle timeout set on the request bun handed this listener.
|
|
81
82
|
const relay = askOver((request, seconds) =>
|
|
82
83
|
served?.timeout(request, seconds),
|
|
83
84
|
)
|
|
@@ -302,6 +303,8 @@ export async function serveDev(
|
|
|
302
303
|
...(port === undefined ? {} : { port }),
|
|
303
304
|
// 16.54, as `App.listen` lifts it: the worker behind this is where the ceiling is read.
|
|
304
305
|
maxRequestBodySize: Number.MAX_SAFE_INTEGER,
|
|
306
|
+
// 16.56 — the bound a request received whole lifts and its answer puts back (16.23).
|
|
307
|
+
idleTimeout: IDLE_TIMEOUT_SECONDS,
|
|
305
308
|
async fetch(request, listening) {
|
|
306
309
|
const asked = new URL(request.url).pathname
|
|
307
310
|
// THE MUX STANDS UNDER THE MOUNT, which is the app's configuration and so the worker's to
|
|
@@ -5,9 +5,68 @@
|
|
|
5
5
|
import type { Problem } from '#shared/Failed.ts'
|
|
6
6
|
import type { Framed } from '#shared/framing.ts'
|
|
7
7
|
import { REFUSAL_KEY } from '#shared/REFUSAL_KEY.ts'
|
|
8
|
+
import { HEARTBEAT_MILLISECONDS } from './HEARTBEAT_MILLISECONDS.ts'
|
|
8
9
|
|
|
9
10
|
/** AN EVENT STREAM OPENS ON A COMMENT, so a proxy has bytes to pass before the first event. */
|
|
10
|
-
export const EVENT_STREAM_OPENER = ':\n\n'
|
|
11
|
+
export const EVENT_STREAM_OPENER = new TextEncoder().encode(':\n\n')
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* `body`, read one chunk a pull; `release` runs once it closes, errors or its reader hangs up.
|
|
15
|
+
*
|
|
16
|
+
* 16.57 — `beats`: a comment wherever a read has waited a heartbeat for its next chunk. A COMMENT
|
|
17
|
+
* RATHER THAN AN EVENT: every reader skips one (18.27), and it gives a proxy in front bytes to
|
|
18
|
+
* pass, so neither it nor the reader takes a quiet stream for a dead one. A bridge's reader goes by
|
|
19
|
+
* it too (44.27), where a frame would sit in the pairing's inbox through a drop.
|
|
20
|
+
*
|
|
21
|
+
* BEATS ONLY WHILE A READ WAITS, at a high-water mark of 0: a reader that stalled or never started
|
|
22
|
+
* pulls nothing, so no comment queues behind it and no timer outlives it, and `body` is pulled no
|
|
23
|
+
* further ahead than it was. A read the beat won is kept for the next pull.
|
|
24
|
+
*/
|
|
25
|
+
export function heldBody(
|
|
26
|
+
body: ReadableStream<Uint8Array>,
|
|
27
|
+
release: () => void,
|
|
28
|
+
beats: boolean,
|
|
29
|
+
): ReadableStream<Uint8Array> {
|
|
30
|
+
const reader = body.getReader()
|
|
31
|
+
let pending: ReturnType<typeof reader.read> | undefined
|
|
32
|
+
return new ReadableStream<Uint8Array>(
|
|
33
|
+
{
|
|
34
|
+
async pull(controller) {
|
|
35
|
+
pending ??= reader.read()
|
|
36
|
+
let beat: ReturnType<typeof setTimeout> | undefined
|
|
37
|
+
try {
|
|
38
|
+
const next = beats
|
|
39
|
+
? await Promise.race([
|
|
40
|
+
pending,
|
|
41
|
+
new Promise<undefined>((quiet) => {
|
|
42
|
+
beat = setTimeout(
|
|
43
|
+
() => quiet(undefined),
|
|
44
|
+
HEARTBEAT_MILLISECONDS,
|
|
45
|
+
)
|
|
46
|
+
}),
|
|
47
|
+
])
|
|
48
|
+
: await pending
|
|
49
|
+
if (next === undefined)
|
|
50
|
+
return controller.enqueue(EVENT_STREAM_OPENER)
|
|
51
|
+
pending = undefined
|
|
52
|
+
if (!next.done) return controller.enqueue(next.value)
|
|
53
|
+
controller.close()
|
|
54
|
+
release()
|
|
55
|
+
} catch (thrown) {
|
|
56
|
+
controller.error(thrown)
|
|
57
|
+
release()
|
|
58
|
+
} finally {
|
|
59
|
+
clearTimeout(beat)
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
cancel(reason) {
|
|
63
|
+
release()
|
|
64
|
+
return reader.cancel(reason)
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{ highWaterMark: 0 },
|
|
68
|
+
)
|
|
69
|
+
}
|
|
11
70
|
|
|
12
71
|
/**
|
|
13
72
|
* One event: its `event` name and `id` where it has them, and `data` — one line, which JSON is as
|
package/src/shared/ADDRESSES.ts
CHANGED
|
@@ -59,3 +59,14 @@ export const SOCKET_ADDRESS = `${ABIDE_ADDRESS}/socket`
|
|
|
59
59
|
* 26.2.
|
|
60
60
|
*/
|
|
61
61
|
export const PRINCIPAL_ADDRESS = `${ABIDE_ADDRESS}/principal`
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Where an app with `ABIDE_OFFLINE` on serves its service worker, and where every other app serves
|
|
65
|
+
* the one that removes it.
|
|
66
|
+
*
|
|
67
|
+
* 45.1, 45.9.
|
|
68
|
+
*/
|
|
69
|
+
export const WORKER_ADDRESS = `${ABIDE_ADDRESS}/worker.js`
|
|
70
|
+
|
|
71
|
+
/** The web app manifest an app answers where it keeps its pages offline and has an icon (45.15). */
|
|
72
|
+
export const APP_MANIFEST_ADDRESS = `${ABIDE_ADDRESS}/manifest.webmanifest`
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// 45.3, 45.5 — THE CACHES THE SERVICE WORKER KEEPS, named once for the worker the server writes and
|
|
2
|
+
// the router that asks whether a page was saved (45.12). A leaf, for the reason every constant
|
|
3
|
+
// crossing a seam is one.
|
|
4
|
+
|
|
5
|
+
/** What every cache the worker keeps is named under, so a cache an earlier worker named is found. */
|
|
6
|
+
export const OFFLINE_CACHE_PREFIX = 'abide:'
|
|
7
|
+
|
|
8
|
+
/** The pages, the handlers' answers and the public files the worker saved, by address. */
|
|
9
|
+
export const OFFLINE_SAVED = `${OFFLINE_CACHE_PREFIX}saved`
|
|
10
|
+
|
|
11
|
+
/** The build's bundle, saved as the worker installs. */
|
|
12
|
+
export const OFFLINE_BUNDLE = `${OFFLINE_CACHE_PREFIX}bundle`
|
package/src/shared/answerOf.ts
CHANGED
|
@@ -81,15 +81,40 @@ async function fetched(
|
|
|
81
81
|
streams: boolean,
|
|
82
82
|
signal: AbortSignal | undefined,
|
|
83
83
|
): Promise<Answer> {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
let response: Response
|
|
85
|
+
try {
|
|
86
|
+
response = await fetch(
|
|
87
|
+
request.url,
|
|
88
|
+
signal === undefined ? request.init : { ...request.init, signal },
|
|
89
|
+
)
|
|
90
|
+
} catch (error) {
|
|
91
|
+
// An abort is the load's own ending (16.49), which the value has already answered.
|
|
92
|
+
if (signal?.aborted === true) throw error
|
|
93
|
+
// 16.79 — NO ANSWER AT ALL, as a status the app can narrow on rather than the browser's
|
|
94
|
+
// own `TypeError`, whose message differs in each.
|
|
95
|
+
return unanswered()
|
|
96
|
+
}
|
|
88
97
|
// THE HANDLER SIGNED THIS CALLER IN OR OUT, and the cookie is already the browser's.
|
|
89
98
|
principalRotated(response)
|
|
90
99
|
return answerFrom(response, undefined, streams)
|
|
91
100
|
}
|
|
92
101
|
|
|
102
|
+
/** The answer a call that reached no server stands in for: an empty 503, which `answered` refuses. */
|
|
103
|
+
function unanswered(): Answer {
|
|
104
|
+
return {
|
|
105
|
+
status: 503,
|
|
106
|
+
statusText: '',
|
|
107
|
+
ok: false,
|
|
108
|
+
framing: null,
|
|
109
|
+
followable: false,
|
|
110
|
+
version: undefined,
|
|
111
|
+
life: undefined,
|
|
112
|
+
text: '',
|
|
113
|
+
bytes: undefined,
|
|
114
|
+
body: undefined,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
93
118
|
/**
|
|
94
119
|
* `response`, classified: a body `read` to its end already (an early call the server painted)
|
|
95
120
|
* decoded as what it carried, and one unread read as bytes where it is a binary (16.34), as text
|
|
@@ -31,7 +31,7 @@ export function attributeText(name: string, value: unknown): string | null {
|
|
|
31
31
|
const safe = safeUrl(text)
|
|
32
32
|
if (safe !== text)
|
|
33
33
|
RENDER_LOG.warning(
|
|
34
|
-
`abide: \`${name}\` was handed a URL whose scheme is not http:, https:, mailto: or
|
|
34
|
+
`abide: \`${name}\` was handed a URL whose scheme is not http:, https:, mailto:, tel:, blob: or data:, and is written empty. RULEBOOK 32.20.`,
|
|
35
35
|
)
|
|
36
36
|
return safe
|
|
37
37
|
}
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
// nothing is dropped early, and the read-driven backstop in `Reactive.ts` catches up the
|
|
39
39
|
// moment the clock does.
|
|
40
40
|
|
|
41
|
+
import { LONGEST_DELAY } from '../LONGEST_DELAY.ts'
|
|
42
|
+
|
|
41
43
|
export type Expiry = {
|
|
42
44
|
/** The life of one production, in milliseconds. */
|
|
43
45
|
ms: number
|
|
@@ -90,13 +92,23 @@ export function expiryFor(
|
|
|
90
92
|
* number and has no process to hold open, so the clause is satisfied there by there being nothing
|
|
91
93
|
* to satisfy. CLAUDE.md's uniformity bias is exactly this shape — one mechanism, inert on the side
|
|
92
94
|
* that cannot need it.
|
|
95
|
+
*
|
|
96
|
+
* PAST WHAT `setTimeout` HOLDS — 2^31 - 1 ms, near 25 days — a delay is run after 1 ms, and a
|
|
97
|
+
* deadline that fires early finds nothing expired and re-arms: a timer a millisecond for the life of
|
|
98
|
+
* the value. So `Infinity` arms nothing, and a longer finite delay arms at the cap — early, which a
|
|
99
|
+
* deadline re-checks and re-arms from, and which a debounce or throttle window that long takes as
|
|
100
|
+
* its end.
|
|
93
101
|
*/
|
|
94
102
|
export function armTimer(
|
|
95
103
|
expiry: { timer: ReturnType<typeof setTimeout> | null },
|
|
96
104
|
delay: number,
|
|
97
105
|
fire: () => void,
|
|
98
106
|
): void {
|
|
99
|
-
|
|
107
|
+
if (delay === Number.POSITIVE_INFINITY) return
|
|
108
|
+
const timer = setTimeout(
|
|
109
|
+
fire,
|
|
110
|
+
delay > 0 ? Math.min(delay, LONGEST_DELAY) : 0,
|
|
111
|
+
)
|
|
100
112
|
const unref = (timer as { unref?: () => void }).unref
|
|
101
113
|
if (typeof unref === 'function') unref.call(timer)
|
|
102
114
|
expiry.timer = timer
|
|
@@ -1998,7 +1998,16 @@ function refused(cell: Cell, failure: unknown): unknown {
|
|
|
1998
1998
|
// had set it true, and `error` went from nothing to this. A guard that asked about
|
|
1999
1999
|
// either is woken here or never.
|
|
2000
2000
|
probesMoved(cell)
|
|
2001
|
-
if (
|
|
2001
|
+
if (cell.servable) return failure
|
|
2002
|
+
// 7.19 — A SERVER THAT COULD NOT BE REACHED (16.79) says nothing about the value held, so the
|
|
2003
|
+
// invalidated value is served again rather than lost to a read that throws, as 4.12 serves one.
|
|
2004
|
+
if (
|
|
2005
|
+
cell.landed &&
|
|
2006
|
+
isFailedNamed(failure, 'HttpError') &&
|
|
2007
|
+
(failure as Failed<string, unknown>).status === 503
|
|
2008
|
+
)
|
|
2009
|
+
serveAgain(cell)
|
|
2010
|
+
else cascade(cell)
|
|
2002
2011
|
return failure
|
|
2003
2012
|
}
|
|
2004
2013
|
|
|
@@ -3253,7 +3262,9 @@ function settled(cell: Cell, untilStreaming = false): Promise<unknown> {
|
|
|
3253
3262
|
if (untilStreaming && cell.stream !== null)
|
|
3254
3263
|
return Promise.resolve(STREAMING)
|
|
3255
3264
|
if (cell.servable && !busyNow(cell)) return Promise.resolve(heldValue(cell))
|
|
3256
|
-
|
|
3265
|
+
// 2.8 — UNSERVED, NOT UNLANDED: an invalidated value has landed (7.8), and a reload that failed
|
|
3266
|
+
// over it leaves the failure as the answer, or the await waits on a load that has ended.
|
|
3267
|
+
if (!busyNow(cell)) {
|
|
3257
3268
|
const failure = failureOf(cell)
|
|
3258
3269
|
if (failure !== undefined) return Promise.reject(failure)
|
|
3259
3270
|
}
|
|
@@ -3280,7 +3291,6 @@ function settled(cell: Cell, untilStreaming = false): Promise<unknown> {
|
|
|
3280
3291
|
settle(heldValue(cell))
|
|
3281
3292
|
return
|
|
3282
3293
|
}
|
|
3283
|
-
if (cell.landed) return
|
|
3284
3294
|
const failure = failureOf(cell)
|
|
3285
3295
|
if (failure !== undefined) {
|
|
3286
3296
|
waiter.alive = false
|
package/src/shared/safeUrl.ts
CHANGED
|
@@ -8,7 +8,14 @@
|
|
|
8
8
|
// attribute's name.
|
|
9
9
|
import { textOf } from './Raw.ts'
|
|
10
10
|
|
|
11
|
-
const SAFE_SCHEMES = new Set([
|
|
11
|
+
const SAFE_SCHEMES = new Set([
|
|
12
|
+
'http:',
|
|
13
|
+
'https:',
|
|
14
|
+
'mailto:',
|
|
15
|
+
'tel:',
|
|
16
|
+
'blob:',
|
|
17
|
+
'data:',
|
|
18
|
+
])
|
|
12
19
|
|
|
13
20
|
/** A URL attribute's text, or `''` where its scheme is outside the safe set. */
|
|
14
21
|
export function safeUrl(value: unknown): string {
|
package/src/ui/Router.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { ERROR_META } from '#shared/ERROR_META.ts'
|
|
|
4
4
|
import { firstMatch, type Params, partsOf } from '#shared/matchRoute.ts'
|
|
5
5
|
import { unmounted } from '#shared/mount.ts'
|
|
6
6
|
import { NAVIGATION_HEADER } from '#shared/NAVIGATION_HEADER.ts'
|
|
7
|
+
import { OFFLINE_SAVED } from '#shared/OFFLINE_CACHES.ts'
|
|
7
8
|
import type { ShareScope } from '#shared/reactive/share.ts'
|
|
8
9
|
import { arrive, route } from '#shared/route.ts'
|
|
9
10
|
import { rankRoutes, type Segment } from '#shared/routePattern.ts'
|
|
@@ -255,6 +256,20 @@ export class Router {
|
|
|
255
256
|
// whole reason the ticket is checked here rather than after the mount — and
|
|
256
257
|
// `navigating` stays the newer navigation's to clear.
|
|
257
258
|
if (ticket.signal.aborted) return
|
|
259
|
+
// 30.32 — OFFLINE, THE DOCUMENT LOAD BELOW CAN ONLY PAINT THE BROWSER'S ERROR PAGE over a
|
|
260
|
+
// page that works. A history step has already moved the bar, so it is left to the
|
|
261
|
+
// document.
|
|
262
|
+
if (
|
|
263
|
+
answer === undefined &&
|
|
264
|
+
!navigator.onLine &&
|
|
265
|
+
step.direction === 'push'
|
|
266
|
+
) {
|
|
267
|
+
// 45.12 — UNLESS THE SERVICE WORKER SAVED THE PAGE, which the document load is
|
|
268
|
+
// then answered with.
|
|
269
|
+
if ((await savedOffline(target)) && !ticket.signal.aborted)
|
|
270
|
+
location.href = target.href
|
|
271
|
+
return
|
|
272
|
+
}
|
|
258
273
|
// 30.18 — A REFUSAL, A RESPONSE MIDDLEWARE ANSWERED ITSELF, or no answer at all: the
|
|
259
274
|
// document asks again and paints whatever the server says. A fetch that failed —
|
|
260
275
|
// offline, or a redirect to another origin that CORS will not let it follow — goes to
|
|
@@ -696,6 +711,16 @@ function loadOrMiss(route: Compiled): Promise<PageModule[] | undefined> {
|
|
|
696
711
|
return load(route).catch(() => undefined)
|
|
697
712
|
}
|
|
698
713
|
|
|
714
|
+
/** Whether a service worker controls the page and saved `target`'s page (45.12). */
|
|
715
|
+
async function savedOffline(target: URL): Promise<boolean> {
|
|
716
|
+
if (navigator.serviceWorker?.controller == null) return false
|
|
717
|
+
const saved = await caches.match(target.href, {
|
|
718
|
+
cacheName: OFFLINE_SAVED,
|
|
719
|
+
ignoreVary: true,
|
|
720
|
+
})
|
|
721
|
+
return saved !== undefined
|
|
722
|
+
}
|
|
723
|
+
|
|
699
724
|
/**
|
|
700
725
|
* Asks the server's `middleware` about an address; `undefined` where the fetch failed.
|
|
701
726
|
*
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// 45.1 — THE SERVICE WORKER, REGISTERED where the document carries the tag `ABIDE_OFFLINE` has the
|
|
2
|
+
// server write. The route table calls this in every app, so what the server says decides.
|
|
3
|
+
|
|
4
|
+
import { WORKER_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
5
|
+
import { MOUNT, mounted } from '#shared/mount.ts'
|
|
6
|
+
import { OFFLINE_META } from '#shared/OFFLINE_META.ts'
|
|
7
|
+
import { onPrincipalRotated } from '#shared/principalRotated.ts'
|
|
8
|
+
import { HYDRATE_LOG } from './Mismatch.ts'
|
|
9
|
+
|
|
10
|
+
/** Registers the app's service worker once the page has loaded, over the mount's whole scope. */
|
|
11
|
+
export function registerWorker(): void {
|
|
12
|
+
if (document.querySelector(`meta[name="${OFFLINE_META}"]`) === null) return
|
|
13
|
+
if (!('serviceWorker' in navigator)) return
|
|
14
|
+
const workers = navigator.serviceWorker
|
|
15
|
+
const register = (): void => {
|
|
16
|
+
workers
|
|
17
|
+
.register(mounted(WORKER_ADDRESS), {
|
|
18
|
+
scope: `${MOUNT.current()}/`,
|
|
19
|
+
})
|
|
20
|
+
// 45.10
|
|
21
|
+
.catch((error: unknown) =>
|
|
22
|
+
HYDRATE_LOG.warning(
|
|
23
|
+
'the service worker did not register',
|
|
24
|
+
error,
|
|
25
|
+
),
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
// AFTER THE LOAD, so installing it — every file of the bundle fetched — does not compete with
|
|
29
|
+
// the page it is installing for.
|
|
30
|
+
if (document.readyState === 'complete') register()
|
|
31
|
+
else addEventListener('load', register, { once: true })
|
|
32
|
+
// 45.7 — a sign-in or sign-out in any tab: the pages it saved were another caller's.
|
|
33
|
+
onPrincipalRotated(() => workers.controller?.postMessage('rotated'), true)
|
|
34
|
+
}
|
package/src/ui/routing.ts
CHANGED
|
@@ -15,4 +15,5 @@
|
|
|
15
15
|
// `ROUTING_SPECIFIER`. An app with no page never resolves this entry.
|
|
16
16
|
|
|
17
17
|
export { listenForRoutes } from './listenForRoutes.ts'
|
|
18
|
+
export { registerWorker } from './registerWorker.ts'
|
|
18
19
|
export { type PageMount, type Route, Router, startRouter } from './Router.ts'
|
package/src/ui/socketWire.ts
CHANGED
|
@@ -133,6 +133,7 @@ class Mux {
|
|
|
133
133
|
subscriptions: Map<number, Following>
|
|
134
134
|
nextId: number
|
|
135
135
|
retry: number
|
|
136
|
+
retrying: ReturnType<typeof setTimeout> | undefined
|
|
136
137
|
|
|
137
138
|
constructor(url: string) {
|
|
138
139
|
this.url = url
|
|
@@ -140,7 +141,23 @@ class Mux {
|
|
|
140
141
|
this.subscriptions = new Map()
|
|
141
142
|
this.nextId = 1
|
|
142
143
|
this.retry = RETRY_FLOOR_MS
|
|
144
|
+
this.retrying = undefined
|
|
143
145
|
onPrincipalRotated(() => this.reopen(), true)
|
|
146
|
+
// 18.33 — THE BACKOFF IS FOR A SERVER THAT IS DOWN, not for a network that has just come
|
|
147
|
+
// back or a tab whose timers the browser was throttling while it was hidden.
|
|
148
|
+
addEventListener('online', () => this.redial())
|
|
149
|
+
document.addEventListener('visibilitychange', () => {
|
|
150
|
+
if (document.visibilityState === 'visible') this.redial()
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Opens a closed socket now, in place of the retry waiting out its backoff. */
|
|
155
|
+
redial(): void {
|
|
156
|
+
if (this.socket !== null || this.subscriptions.size === 0) return
|
|
157
|
+
clearTimeout(this.retrying)
|
|
158
|
+
this.retrying = undefined
|
|
159
|
+
this.retry = RETRY_FLOOR_MS
|
|
160
|
+
this.connect()
|
|
144
161
|
}
|
|
145
162
|
|
|
146
163
|
/**
|
|
@@ -188,11 +205,25 @@ class Mux {
|
|
|
188
205
|
socket.onclose = () => {
|
|
189
206
|
this.socket = null
|
|
190
207
|
if (this.subscriptions.size === 0) return
|
|
191
|
-
|
|
192
|
-
this.retry = Math.min(this.retry * 2, RETRY_CEILING_MS)
|
|
208
|
+
this.retryLater()
|
|
193
209
|
}
|
|
194
210
|
}
|
|
195
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Arms the next attempt at the backoff standing, and doubles it.
|
|
214
|
+
*
|
|
215
|
+
* 18.34 — OFFLINE, AN ATTEMPT CANNOT SUCCEED, so none is armed: the `online` event's `redial`
|
|
216
|
+
* makes the next one.
|
|
217
|
+
*/
|
|
218
|
+
retryLater(): void {
|
|
219
|
+
if (!navigator.onLine) return
|
|
220
|
+
this.retrying = setTimeout(() => {
|
|
221
|
+
this.retrying = undefined
|
|
222
|
+
this.connect()
|
|
223
|
+
}, this.retry)
|
|
224
|
+
this.retry = Math.min(this.retry * 2, RETRY_CEILING_MS)
|
|
225
|
+
}
|
|
226
|
+
|
|
196
227
|
receive(frame: ServerFrame): void {
|
|
197
228
|
this.subscriptions.get(frame.id)?.receive(frame)
|
|
198
229
|
}
|