@abide/abide 0.52.0 → 0.54.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/AGENTS.md +25 -12
- package/CHANGELOG.md +82 -0
- package/README.md +8 -6
- package/package.json +3 -2
- package/src/checkAbide.ts +1 -1
- package/src/lib/server/runtime/amendBroadcaster.ts +53 -0
- package/src/lib/server/runtime/createUiPageRenderer.ts +26 -0
- package/src/lib/server/runtime/runWithRequestScope.ts +1 -0
- package/src/lib/server/runtime/types/RequestStore.ts +9 -0
- package/src/lib/server/sockets/amendFamilyEntry.ts +39 -0
- package/src/lib/server/sockets/createSocketDispatcher.ts +9 -0
- package/src/lib/server/sockets/defineSocket.ts +11 -1
- package/src/lib/shared/AMEND_TOPIC_PREFIX.ts +9 -0
- package/src/lib/shared/SOCKET_SEED.ts +13 -0
- package/src/lib/shared/amend.ts +48 -0
- package/src/lib/shared/amendBroadcastSlot.ts +19 -0
- package/src/lib/shared/applyAmendLocally.ts +20 -0
- package/src/lib/shared/attachRpcSelectorMethods.ts +5 -5
- package/src/lib/shared/buildSocketOverChannel.ts +9 -2
- package/src/lib/shared/cache.ts +35 -10
- package/src/lib/shared/cacheReaderSocketSlot.ts +11 -0
- package/src/lib/shared/createCacheStore.ts +8 -0
- package/src/lib/shared/peek.ts +1 -1
- package/src/lib/shared/socketTailsSlot.ts +13 -0
- package/src/lib/shared/types/AmendApply.ts +15 -0
- package/src/lib/shared/types/CacheReaderHook.ts +11 -0
- package/src/lib/shared/types/RemoteFunction.ts +30 -19
- package/src/lib/shared/types/SocketTails.ts +13 -0
- package/src/lib/shared/types/SsrPayload.ts +6 -0
- package/src/lib/test/createTestApp.ts +6 -0
- package/src/lib/ui/amendReaderHook.ts +125 -0
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +1 -0
- package/src/lib/ui/compile/analyzeComponent.ts +2 -0
- package/src/lib/ui/compile/cachedShadowProgram.ts +38 -0
- package/src/lib/ui/compile/compileComponent.ts +10 -1
- package/src/lib/ui/compile/compileSSR.ts +12 -0
- package/src/lib/ui/compile/compileShadow.ts +37 -34
- package/src/lib/ui/compile/desugarSignals.ts +127 -34
- package/src/lib/ui/compile/generateBuild.ts +5 -1
- package/src/lib/ui/compile/hasTopLevelAwait.ts +28 -0
- package/src/lib/ui/compile/interpolationClassifierForRoot.ts +8 -13
- package/src/lib/ui/compile/isFunctionScopeBoundary.ts +20 -0
- package/src/lib/ui/compile/liftAsyncSubExpressions.ts +2 -2
- package/src/lib/ui/compile/lowerCompoundAssignment.ts +49 -0
- package/src/lib/ui/compile/lowerContext.ts +4 -0
- package/src/lib/ui/compile/lowerDocAccess.ts +19 -31
- package/src/lib/ui/compile/lowerScript.ts +11 -1
- package/src/lib/ui/compile/lowerUpdateExpression.ts +32 -0
- package/src/lib/ui/compile/renameSignalRefs.ts +36 -24
- package/src/lib/ui/compile/seedTypeClassifierForRoot.ts +9 -14
- package/src/lib/ui/compile/types/AnalyzedComponent.ts +4 -0
- package/src/lib/ui/compile/wrapReactionCellSources.ts +10 -6
- package/src/lib/ui/dom/appendText.ts +29 -8
- package/src/lib/ui/dom/appendTextAt.ts +21 -4
- package/src/lib/ui/dom/attr.ts +16 -1
- package/src/lib/ui/dom/awaitBlock.ts +34 -13
- package/src/lib/ui/dom/each.ts +10 -2
- package/src/lib/ui/dom/eachAsync.ts +9 -1
- package/src/lib/ui/dom/readCellBlocking.ts +29 -0
- package/src/lib/ui/dom/readTextOrSuspend.ts +21 -0
- package/src/lib/ui/dom/spreadAttrs.ts +47 -1
- package/src/lib/ui/dom/switchBlock.ts +19 -2
- package/src/lib/ui/dom/tryBlock.ts +10 -0
- package/src/lib/ui/dom/when.ts +19 -1
- package/src/lib/ui/dom/withSuspense.ts +22 -0
- package/src/lib/ui/router.ts +8 -0
- package/src/lib/ui/runtime/SuspenseSignal.ts +24 -0
- package/src/lib/ui/runtime/createDoc.ts +5 -1
- package/src/lib/ui/runtime/flushEffects.ts +18 -10
- package/src/lib/ui/runtime/restoreWarmSeeds.ts +20 -0
- package/src/lib/ui/runtime/types/Cell.ts +4 -2
- package/src/lib/ui/runtime/types/Doc.ts +3 -1
- package/src/lib/ui/runtime/warmSeedBackup.ts +10 -0
- package/src/lib/ui/socketProxy.ts +17 -1
- package/src/lib/ui/startClient.ts +30 -0
- package/src/lib/ui/types/Scope.ts +1 -1
- package/src/lib/ui/watch.ts +25 -7
- package/src/serverEntry.ts +15 -0
- package/src/lib/shared/patch.ts +0 -41
- package/src/lib/ui/compile/COMPOUND_ASSIGNMENT_OPERATORS.ts +0 -19
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { effect } from '../effect.ts'
|
|
2
|
+
import { SuspenseSignal } from '../runtime/SuspenseSignal.ts'
|
|
1
3
|
import { attr } from './attr.ts'
|
|
2
4
|
import { on } from './on.ts'
|
|
3
5
|
|
|
@@ -19,7 +21,51 @@ export function spreadAttrs(
|
|
|
19
21
|
exclude: string[] = [],
|
|
20
22
|
): void {
|
|
21
23
|
const skip = new Set(exclude)
|
|
22
|
-
|
|
24
|
+
let object: Record<string, unknown>
|
|
25
|
+
try {
|
|
26
|
+
object = source()
|
|
27
|
+
} catch (signal) {
|
|
28
|
+
/* A spread over a PENDING blocking `await` read throws a `SuspenseSignal` here (ADR-0042):
|
|
29
|
+
its keys can't be enumerated yet. Binding the empty object now would create no `attr`/`on`
|
|
30
|
+
effects, so the attributes would never appear after the cell resolves — instead defer the
|
|
31
|
+
enumeration into a one-shot effect that re-runs on settle (the read tracked its cell). The
|
|
32
|
+
effect is pinned to this build's scope, so the `attr` binds it creates own their lifetime
|
|
33
|
+
correctly; a `spread` flag makes the enumeration fire exactly once (later value changes
|
|
34
|
+
re-read per key via `attr`, never re-enumerate). */
|
|
35
|
+
if (!(signal instanceof SuspenseSignal)) {
|
|
36
|
+
throw signal
|
|
37
|
+
}
|
|
38
|
+
let spread = false
|
|
39
|
+
effect(() => {
|
|
40
|
+
let resolved: Record<string, unknown>
|
|
41
|
+
try {
|
|
42
|
+
resolved = source()
|
|
43
|
+
} catch (retry) {
|
|
44
|
+
if (!(retry instanceof SuspenseSignal)) {
|
|
45
|
+
throw retry
|
|
46
|
+
}
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
if (spread) {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
spread = true
|
|
53
|
+
bindKeys(element, source, resolved, skip)
|
|
54
|
+
})
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
bindKeys(element, source, object, skip)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Binds each own key of `object` onto `element`: an `on<event>` function attaches as a listener,
|
|
61
|
+
any other non-function value binds as a reactive attribute re-reading `source()[key]` per change.
|
|
62
|
+
A key in `skip` is left to its explicit binding. */
|
|
63
|
+
function bindKeys(
|
|
64
|
+
element: Element,
|
|
65
|
+
source: () => Record<string, unknown>,
|
|
66
|
+
object: Record<string, unknown>,
|
|
67
|
+
skip: Set<string>,
|
|
68
|
+
): void {
|
|
23
69
|
for (const key in object) {
|
|
24
70
|
if (skip.has(key)) {
|
|
25
71
|
continue
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SuspenseSignal } from '../runtime/SuspenseSignal.ts'
|
|
1
2
|
import { mountSwappableRange } from './mountSwappableRange.ts'
|
|
2
3
|
import type { SwitchCase } from './types/SwitchCase.ts'
|
|
3
4
|
|
|
@@ -47,8 +48,24 @@ export function switchBlock(
|
|
|
47
48
|
parent,
|
|
48
49
|
/* A pending bare async subject (compiler-supplied `isPending`) selects no case — not
|
|
49
50
|
even the default — so the block renders nothing until the cell settles, rather than
|
|
50
|
-
conflating "still loading" with a subject that matched the default.
|
|
51
|
-
|
|
51
|
+
conflating "still loading" with a subject that matched the default. A blocking `await`
|
|
52
|
+
cell embedded in the subject or a `match` (a member access / compound the `isPending`
|
|
53
|
+
gate doesn't cover) throws a `SuspenseSignal` while pending (ADR-0042); select no case
|
|
54
|
+
until it settles, exactly as a bare async subject holds — the read tracked its cell, so
|
|
55
|
+
the swap effect re-runs on resolve. */
|
|
56
|
+
() => {
|
|
57
|
+
if (isPending?.() === true) {
|
|
58
|
+
return -1
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
return select(subject())
|
|
62
|
+
} catch (signal) {
|
|
63
|
+
if (!(signal instanceof SuspenseSignal)) {
|
|
64
|
+
throw signal
|
|
65
|
+
}
|
|
66
|
+
return -1
|
|
67
|
+
}
|
|
68
|
+
},
|
|
52
69
|
(index) => {
|
|
53
70
|
const chosen = index === -1 ? undefined : cases[index]
|
|
54
71
|
return chosen && ((p) => chosen.render(p))
|
|
@@ -4,6 +4,7 @@ import { CURRENT_BOUNDARY } from '../runtime/CURRENT_BOUNDARY.ts'
|
|
|
4
4
|
import { claimExpected } from '../runtime/claimExpected.ts'
|
|
5
5
|
import { OWNER } from '../runtime/OWNER.ts'
|
|
6
6
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
7
|
+
import { SuspenseSignal } from '../runtime/SuspenseSignal.ts'
|
|
7
8
|
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
8
9
|
import type { Boundary } from '../runtime/types/Boundary.ts'
|
|
9
10
|
import type { State } from '../runtime/types/State.ts'
|
|
@@ -104,6 +105,15 @@ export function tryBlock(
|
|
|
104
105
|
activeKind = 'try'
|
|
105
106
|
errorCell = undefined
|
|
106
107
|
} catch (error) {
|
|
108
|
+
/* A `SuspenseSignal` is "value pending", not an error, and must NEVER render `{:catch}`
|
|
109
|
+
(ADR-0042 D3, mirroring `flushEffects`, which refuses to route it to a boundary) — that
|
|
110
|
+
would flash the author's catch during loading and, since `rewatch` only arms on an
|
|
111
|
+
`AsyncCellError`, stick there forever. Every reading region withholds its own suspend
|
|
112
|
+
locally (post-ADR-0042 the `{#await}`/`{#each await}` subjects included), so this is
|
|
113
|
+
unreachable in a compiled template; rethrow defensively rather than mislabel it. */
|
|
114
|
+
if (error instanceof SuspenseSignal) {
|
|
115
|
+
throw error
|
|
116
|
+
}
|
|
107
117
|
if (!hasCatch) {
|
|
108
118
|
throw error
|
|
109
119
|
}
|
package/src/lib/ui/dom/when.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SuspenseSignal } from '../runtime/SuspenseSignal.ts'
|
|
1
2
|
import { mountSwappableRange } from './mountSwappableRange.ts'
|
|
2
3
|
|
|
3
4
|
/*
|
|
@@ -26,7 +27,24 @@ export function when(
|
|
|
26
27
|
): void {
|
|
27
28
|
mountSwappableRange(
|
|
28
29
|
parent,
|
|
29
|
-
() =>
|
|
30
|
+
() => {
|
|
31
|
+
if (isPending?.() === true) {
|
|
32
|
+
return 'pending'
|
|
33
|
+
}
|
|
34
|
+
/* A blocking `await` cell embedded in the condition — a member access or compound the
|
|
35
|
+
whole-subject `isPending` gate doesn't cover (`{#if !sources.length}`, `{#if user &&
|
|
36
|
+
await load()}`) — throws a `SuspenseSignal` while pending (ADR-0042). Withhold the
|
|
37
|
+
block (render neither branch) until it settles, exactly as a bare async subject holds;
|
|
38
|
+
the read tracked its cell, so the swap effect re-runs on resolve. */
|
|
39
|
+
try {
|
|
40
|
+
return condition() ? 'then' : 'else'
|
|
41
|
+
} catch (signal) {
|
|
42
|
+
if (!(signal instanceof SuspenseSignal)) {
|
|
43
|
+
throw signal
|
|
44
|
+
}
|
|
45
|
+
return 'pending'
|
|
46
|
+
}
|
|
47
|
+
},
|
|
30
48
|
(branch) => (branch === 'then' ? render : branch === 'else' ? renderElse : undefined),
|
|
31
49
|
before,
|
|
32
50
|
/* An async subject (compiler supplies `isPending`) can render a different branch on the
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { SuspenseSignal } from '../runtime/SuspenseSignal.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Runs `read`, returning `fallback` when it SUSPENDS — a pending blocking `await` read throws a
|
|
5
|
+
`SuspenseSignal` (ADR-0042). The reading region shows `fallback` until the value resolves; the
|
|
6
|
+
throwing read subscribed the enclosing effect to the cell, so the effect re-runs and fills in on
|
|
7
|
+
settle. Any non-suspense error propagates unchanged (a real rejection still reaches `{#try}`). The
|
|
8
|
+
single place the `instanceof SuspenseSignal` swallow lives for a value-returning bind, so the read
|
|
9
|
+
sites (`each`, `watch`, …) share one definition rather than copying the try/catch. Text binds use
|
|
10
|
+
the sibling `readTextOrSuspend` (fallback `''` with String coercion); an attribute leaves itself
|
|
11
|
+
unset inline (a side effect, not a fallback value).
|
|
12
|
+
*/
|
|
13
|
+
export function withSuspense<T>(read: () => T, fallback: T): T {
|
|
14
|
+
try {
|
|
15
|
+
return read()
|
|
16
|
+
} catch (signal) {
|
|
17
|
+
if (!(signal instanceof SuspenseSignal)) {
|
|
18
|
+
throw signal
|
|
19
|
+
}
|
|
20
|
+
return fallback
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/lib/ui/router.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { exitRenderPass } from './runtime/exitRenderPass.ts'
|
|
|
12
12
|
import { historyEntries } from './runtime/historyEntries.ts'
|
|
13
13
|
import { PENDING_OUTLET } from './runtime/PENDING_OUTLET.ts'
|
|
14
14
|
import { RENDER } from './runtime/RENDER.ts'
|
|
15
|
+
import { restoreWarmSeeds } from './runtime/restoreWarmSeeds.ts'
|
|
15
16
|
import { runtimePath } from './runtime/runtimePath.ts'
|
|
16
17
|
import type { AbideHistoryState } from './runtime/types/AbideHistoryState.ts'
|
|
17
18
|
import type { NavVerdict } from './runtime/types/NavVerdict.ts'
|
|
@@ -566,6 +567,13 @@ export function router(
|
|
|
566
567
|
disposeFrom(0)
|
|
567
568
|
rootBoundary = undefined
|
|
568
569
|
mountedPageKey = undefined
|
|
570
|
+
/* Re-seed the cell/doc warm partitions the failed hydration pass consumed,
|
|
571
|
+
so the cold rebuild re-adopts the SSR-resolved values instead of
|
|
572
|
+
refetching — a cold refetch leaves blocking `await` cells pending, and a
|
|
573
|
+
top-level `$$readCellBlocking` sits in no suspense region, so its
|
|
574
|
+
SuspenseSignal would escape this rebuild (it runs inside the catch, past
|
|
575
|
+
the try) and kill the mount. Restored, the rebuild reads settled. */
|
|
576
|
+
restoreWarmSeeds()
|
|
569
577
|
buildFrom(0, chainKeys, layoutViews, pageView, key, params, false)
|
|
570
578
|
}
|
|
571
579
|
/* Reapply the destination entry's scroll once its DOM exists — a
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AsyncComputed } from '../../shared/types/AsyncComputed.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
The sentinel a compiled blocking read (`$$readCellBlocking`, an `await`-marked cell) raises
|
|
5
|
+
when the cell has no value *yet* — "not resolved," not an error (ADR-0042 D3). It is a sibling
|
|
6
|
+
of `AsyncCellError` but a DISTINCT class so the two are told apart by `instanceof`: a suspend is
|
|
7
|
+
caught LOCALLY by the reading region — each DOM primitive (`appendText`, `attr`, `each`,
|
|
8
|
+
`spreadAttrs`, `watch`, and the `when`/`switchBlock` condition) swallows it and withholds to an
|
|
9
|
+
empty fallback, re-running on settle because the throwing read already subscribed the region's
|
|
10
|
+
effect to the cell. It must never be handled as an error: `flushEffects` refuses to route a
|
|
11
|
+
`SuspenseSignal` to a reactive `{#try}` (that would flash the author's `{:catch}` during loading),
|
|
12
|
+
while an `AsyncCellError` still passes through. It carries the originating cell for diagnostics.
|
|
13
|
+
Extends `Error` only to satisfy throw-an-Error lint and match `AsyncCellError`; it carries no
|
|
14
|
+
`cause` — there is nothing wrong, the value is simply pending.
|
|
15
|
+
*/
|
|
16
|
+
export class SuspenseSignal extends Error {
|
|
17
|
+
readonly cell: AsyncComputed<unknown>
|
|
18
|
+
|
|
19
|
+
constructor(cell: AsyncComputed<unknown>) {
|
|
20
|
+
super('abide: suspense (value pending)')
|
|
21
|
+
this.name = 'SuspenseSignal'
|
|
22
|
+
this.cell = cell
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -312,6 +312,7 @@ export function createDoc(initial: unknown): Doc {
|
|
|
312
312
|
parent ??= resolveParent()
|
|
313
313
|
parent[leafKey] = value
|
|
314
314
|
writeNode(node, value)
|
|
315
|
+
return value
|
|
315
316
|
},
|
|
316
317
|
}
|
|
317
318
|
}
|
|
@@ -321,7 +322,10 @@ export function createDoc(initial: unknown): Doc {
|
|
|
321
322
|
cell,
|
|
322
323
|
derive,
|
|
323
324
|
apply,
|
|
324
|
-
replace: (path, value) =>
|
|
325
|
+
replace: (path, value) => {
|
|
326
|
+
apply({ op: 'replace', path, value })
|
|
327
|
+
return value
|
|
328
|
+
},
|
|
325
329
|
add: (path, value) => apply({ op: 'add', path, value }),
|
|
326
330
|
remove: (path) => apply({ op: 'remove', path }),
|
|
327
331
|
snapshot: () => tree,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { boundaryFor } from './boundaryFor.ts'
|
|
2
2
|
import { NODE_STATE } from './NODE_STATE.ts'
|
|
3
3
|
import { REACTIVE_CONTEXT } from './REACTIVE_CONTEXT.ts'
|
|
4
|
+
import { SuspenseSignal } from './SuspenseSignal.ts'
|
|
4
5
|
import type { ReactiveNode } from './types/ReactiveNode.ts'
|
|
5
6
|
import { updateIfNecessary } from './updateIfNecessary.ts'
|
|
6
7
|
|
|
@@ -50,16 +51,23 @@ function drain(): void {
|
|
|
50
51
|
try {
|
|
51
52
|
updateIfNecessary(node)
|
|
52
53
|
} catch (error) {
|
|
53
|
-
/* A
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
it
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
/* A `SuspenseSignal` is "no value yet", not an error, and must NEVER route to a
|
|
55
|
+
reactive `{#try}` (that would flash the author's `{:catch}` during loading,
|
|
56
|
+
ADR-0042 D3). Every reading region catches its own suspend locally and re-runs on
|
|
57
|
+
settle (the throwing read subscribed its cell), so a `SuspenseSignal` reaches here
|
|
58
|
+
only if it escaped an unguarded read — surface it via the error collector below
|
|
59
|
+
rather than leaking it to a boundary. A normal error routes to the node's nearest
|
|
60
|
+
`{#try}` boundary: swap the guarded content to the catch branch instead of the
|
|
61
|
+
rethrow fallback — the "later re-run throw" the sync boundary could not catch.
|
|
62
|
+
Reset it CLEAN either way so a later write to its deps can re-queue it (otherwise
|
|
63
|
+
`mark`'s CLEAN→dirty gate leaves it permanently inert). */
|
|
64
|
+
if (!(error instanceof SuspenseSignal)) {
|
|
65
|
+
const boundary = boundaryFor.get(node)
|
|
66
|
+
if (boundary !== undefined) {
|
|
67
|
+
boundary.handle(error)
|
|
68
|
+
node.status = NODE_STATE.CLEAN
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
63
71
|
}
|
|
64
72
|
/* No boundary — one effect throwing must not strand the effects queued behind
|
|
65
73
|
it (they live in this same `batch`, unreachable once we swap
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { CELL_SEED } from './CELL_SEED.ts'
|
|
2
|
+
import { DOC_SEED } from './DOC_SEED.ts'
|
|
3
|
+
import { warmSeedBackup } from './warmSeedBackup.ts'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Re-populate the async-cell and doc-state warm-seed manifests from the pristine SSR copies stashed at
|
|
7
|
+
boot, undoing the consume-once deletes the failed hydration pass made. The router's hydration-desync
|
|
8
|
+
recovery calls this before it rebuilds the page COLD: with the seeds restored, each rebuilt cell
|
|
9
|
+
re-adopts its SSR-resolved value (reads settled, not pending) instead of refetching — so a blocking
|
|
10
|
+
`await` cell can't throw an uncaught SuspenseSignal at mount, and the cold render shows real data with
|
|
11
|
+
no loading flash. A no-op when boot never seeded (a fresh client-only mount).
|
|
12
|
+
*/
|
|
13
|
+
export function restoreWarmSeeds(): void {
|
|
14
|
+
if (warmSeedBackup.cells !== undefined) {
|
|
15
|
+
Object.assign(CELL_SEED, warmSeedBackup.cells)
|
|
16
|
+
}
|
|
17
|
+
if (warmSeedBackup.docs !== undefined) {
|
|
18
|
+
Object.assign(DOC_SEED, warmSeedBackup.docs)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/* A stable reactive accessor bound to one document path: `get` subscribes the
|
|
2
2
|
running observer, `set` mutates that path and wakes its readers — all path
|
|
3
3
|
resolution done once at bind time, so the access itself is string-free. This
|
|
4
|
-
is the shape the template compiler emits for a `{path}` read / write.
|
|
5
|
-
|
|
4
|
+
is the shape the template compiler emits for a `{path}` read / write. `set`
|
|
5
|
+
returns the written value so a lowered assignment evaluates to that value in
|
|
6
|
+
expression position (chained `a = b = x`, the postfix `x++` correction). */
|
|
7
|
+
export type Cell<T> = { get: () => T; set: (value: T) => T }
|
|
@@ -15,7 +15,9 @@ export type Doc = {
|
|
|
15
15
|
stored/serialized/journalled) and returns a string-free reader bound to it. */
|
|
16
16
|
derive: <T>(path: string, compute: () => T) => () => T
|
|
17
17
|
apply: (patch: Patch) => void
|
|
18
|
-
|
|
18
|
+
/* Returns the written value so a lowered assignment/`++` evaluates to it in
|
|
19
|
+
expression position (mirrored by `Scope.replace` and `Cell.set`). */
|
|
20
|
+
replace: (path: string, value: unknown) => unknown
|
|
19
21
|
add: (path: string, value: unknown) => void
|
|
20
22
|
remove: (path: string) => void
|
|
21
23
|
snapshot: () => unknown
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SsrPayload } from '../../shared/types/SsrPayload.ts'
|
|
2
|
+
|
|
3
|
+
/* The pristine SSR warm-seed manifests (`__SSR__.cells` / `__SSR__.docs`), stashed by `startClient`
|
|
4
|
+
before the hydration pass drains their client copies — each adopted seed is DELETED from CELL_SEED /
|
|
5
|
+
consumed from DOC_SEED as it hydrates. `Object.assign`-ing into those seeds leaves these source
|
|
6
|
+
records untouched, so they stay a clean re-seed source. The router's discard→cold-rebuild recovery
|
|
7
|
+
reads them via `restoreWarmSeeds` so the rebuilt cells re-adopt the SSR-resolved values instead of
|
|
8
|
+
refetching cold — a cold refetch would leave a blocking `await` cell pending and throw an uncaught
|
|
9
|
+
SuspenseSignal at mount, killing the page. Empty on a fresh client-only mount (nothing seeded). */
|
|
10
|
+
export const warmSeedBackup: { cells?: SsrPayload['cells']; docs?: SsrPayload['docs'] } = {}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { buildSocketOverChannel } from '../shared/buildSocketOverChannel.ts'
|
|
2
|
+
import { decodeRefJson } from '../shared/decodeRefJson.ts'
|
|
3
|
+
import { SOCKET_SEED } from '../shared/SOCKET_SEED.ts'
|
|
2
4
|
import type { Socket } from '../shared/types/Socket.ts'
|
|
3
5
|
import { getSocketChannel } from './socketChannel.ts'
|
|
4
6
|
import { watch } from './watch.ts'
|
|
@@ -22,7 +24,21 @@ socketProxy API, not the wire layer.
|
|
|
22
24
|
*/
|
|
23
25
|
// @documentation plumbing
|
|
24
26
|
export function socketProxy<T>(name: string): Socket<T> {
|
|
25
|
-
|
|
27
|
+
/* Warm-seed from the server's retained frame (shipped in `__SSR__.sockets`, drained into
|
|
28
|
+
SOCKET_SEED by startClient before mount) so `peek(socket)` returns the SAME value the SSR
|
|
29
|
+
render committed to instead of undefined on this not-yet-connected client — otherwise the two
|
|
30
|
+
disagree and hydration discards the server markup. A frame that failed to serialize server-side
|
|
31
|
+
simply isn't present, and the socket falls back to a cold peek. */
|
|
32
|
+
const seeded = SOCKET_SEED[name]
|
|
33
|
+
let initialFrame: T | undefined
|
|
34
|
+
if (seeded !== undefined) {
|
|
35
|
+
try {
|
|
36
|
+
initialFrame = decodeRefJson(seeded) as T
|
|
37
|
+
} catch {
|
|
38
|
+
initialFrame = undefined
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const socket = buildSocketOverChannel<T>(name, getSocketChannel, initialFrame)
|
|
26
42
|
/* Overwrite the shared builder's inert `.watch` with the real reaction sugar
|
|
27
43
|
(`socket.watch(handler)` ≡ `watch(socket, handler)`). Attached here so the ui-only
|
|
28
44
|
`watch` primitive never rides into a server bundle. */
|
|
@@ -1,17 +1,23 @@
|
|
|
1
|
+
import { amendBroadcastSlot } from '../shared/amendBroadcastSlot.ts'
|
|
2
|
+
import { applyAmendLocally } from '../shared/applyAmendLocally.ts'
|
|
1
3
|
import { applyCacheStalenessLocally } from '../shared/applyCacheStalenessLocally.ts'
|
|
4
|
+
import { cacheReaderSocketSlot } from '../shared/cacheReaderSocketSlot.ts'
|
|
2
5
|
import { cacheStalenessSlot } from '../shared/cacheStalenessSlot.ts'
|
|
3
6
|
import { cacheStoreSlot } from '../shared/cacheStoreSlot.ts'
|
|
4
7
|
import { createCacheStore } from '../shared/createCacheStore.ts'
|
|
5
8
|
import { pageSlot } from '../shared/pageSlot.ts'
|
|
9
|
+
import { SOCKET_SEED } from '../shared/SOCKET_SEED.ts'
|
|
6
10
|
import { sharedCacheStoreSlot } from '../shared/sharedCacheStoreSlot.ts'
|
|
7
11
|
import type { SsrPayload } from '../shared/types/SsrPayload.ts'
|
|
8
12
|
import type { StreamedResolution } from '../shared/types/StreamedResolution.ts'
|
|
13
|
+
import { createAmendReaderHook } from './amendReaderHook.ts'
|
|
9
14
|
import { probeNavigation } from './probeNavigation.ts'
|
|
10
15
|
import { router } from './router.ts'
|
|
11
16
|
import { CELL_SEED } from './runtime/CELL_SEED.ts'
|
|
12
17
|
import { clientPage } from './runtime/clientPage.ts'
|
|
13
18
|
import { DOC_SEED } from './runtime/DOC_SEED.ts'
|
|
14
19
|
import type { RouteLoader } from './runtime/types/RouteLoader.ts'
|
|
20
|
+
import { warmSeedBackup } from './runtime/warmSeedBackup.ts'
|
|
15
21
|
import { seedBootState } from './seedBootState.ts'
|
|
16
22
|
import { seedStreamedResolution } from './seedStreamedResolution.ts'
|
|
17
23
|
import { subscribeCacheStaleness } from './subscribeCacheStaleness.ts'
|
|
@@ -57,6 +63,13 @@ export function startClient(
|
|
|
57
63
|
broadcaster instead — the ADR-0041 side-swap). Same function as the slot fallback so
|
|
58
64
|
the local-apply path can't diverge. */
|
|
59
65
|
cacheStalenessSlot.resolver = () => applyCacheStalenessLocally
|
|
66
|
+
/* amend(args, value) applies to THIS tab's cache too — the server entry broadcasts instead
|
|
67
|
+
(the ADR-0043 side-swap). Same function as the slot fallback for the same reason. */
|
|
68
|
+
amendBroadcastSlot.resolver = () => applyAmendLocally
|
|
69
|
+
/* Open/close a per-call amend value subscription as reactive readers of a key come and go
|
|
70
|
+
(ADR-0043), so a server amend(args, value) push lands on keys this tab is reading. */
|
|
71
|
+
const amendReader = createAmendReaderHook()
|
|
72
|
+
cacheReaderSocketSlot.resolver = () => amendReader.hook
|
|
60
73
|
/* Seed both SSR cache partitions through the one streamed-resolution sink: `ssr.cache`
|
|
61
74
|
(inline — reads settled at render-return, in __SSR__) and `__abideResumeCache` (pending
|
|
62
75
|
{#await} reads whose `__abideResolve(...)` chunks the stream pushed during parse, before
|
|
@@ -80,6 +93,18 @@ export function startClient(
|
|
|
80
93
|
if (ssr.docs !== undefined) {
|
|
81
94
|
Object.assign(DOC_SEED, ssr.docs)
|
|
82
95
|
}
|
|
96
|
+
/* Seed the socket warm partition into SOCKET_SEED before mount: a hydrating `socketProxy` reads
|
|
97
|
+
its name key to seed `lastFrame`, so `peek(socket)` returns the server's retained frame instead
|
|
98
|
+
of undefined on the not-yet-connected client — congruent with the SSR HTML. */
|
|
99
|
+
if (ssr.sockets !== undefined) {
|
|
100
|
+
Object.assign(SOCKET_SEED, ssr.sockets)
|
|
101
|
+
}
|
|
102
|
+
/* Stash the pristine cell/doc manifests for the router's hydration-desync recovery: the hydration
|
|
103
|
+
pass consumes CELL_SEED/DOC_SEED (each adopted key is deleted), so a discard→cold-rebuild would
|
|
104
|
+
otherwise refetch every blocking cell — leaving it pending and throwing an uncaught
|
|
105
|
+
SuspenseSignal at mount. `restoreWarmSeeds` re-seeds from these before the cold rebuild. */
|
|
106
|
+
warmSeedBackup.cells = ssr.cells
|
|
107
|
+
warmSeedBackup.docs = ssr.docs
|
|
83
108
|
/* Keep the cache channel live past boot: replace the head's buffering collector with
|
|
84
109
|
the store-connected sink so a post-boot resolution — the inline doc-stream cache
|
|
85
110
|
script — seeds the store through `seedStreamedResolution` directly instead of pushing
|
|
@@ -94,6 +119,11 @@ export function startClient(
|
|
|
94
119
|
const disposeRouter = router(target, routes, layoutRoutes, probeNavigation)
|
|
95
120
|
return () => {
|
|
96
121
|
disposeStaleness()
|
|
122
|
+
amendReader.dispose()
|
|
123
|
+
/* Clear the reader hook so a post-teardown cache read doesn't re-open amend subscriptions
|
|
124
|
+
— unlike the staleness/store slots (whose leaked resolver stays benign), a live hook on
|
|
125
|
+
the read path must not outlive the client it belongs to. */
|
|
126
|
+
cacheReaderSocketSlot.resolver = undefined
|
|
97
127
|
disposeRouter()
|
|
98
128
|
}
|
|
99
129
|
}
|
|
@@ -25,7 +25,7 @@ export type Scope = {
|
|
|
25
25
|
readonly parent: Scope | undefined
|
|
26
26
|
/* data — mirrors Doc */
|
|
27
27
|
read: <T>(path: string) => T
|
|
28
|
-
replace: (path: string, value: unknown) =>
|
|
28
|
+
replace: (path: string, value: unknown) => unknown
|
|
29
29
|
add: (path: string, value: unknown) => void
|
|
30
30
|
remove: (path: string) => void
|
|
31
31
|
apply: (patch: Patch) => void
|
package/src/lib/ui/watch.ts
CHANGED
|
@@ -3,10 +3,12 @@ import { REMOTE_FUNCTION } from '../shared/REMOTE_FUNCTION.ts'
|
|
|
3
3
|
import type { CacheOnContext } from '../shared/types/CacheOnContext.ts'
|
|
4
4
|
import type { NamedAsyncIterable } from '../shared/types/NamedAsyncIterable.ts'
|
|
5
5
|
import type { RemoteFunction } from '../shared/types/RemoteFunction.ts'
|
|
6
|
+
import { withSuspense } from './dom/withSuspense.ts'
|
|
6
7
|
import { effect } from './effect.ts'
|
|
7
8
|
import { generationGuard } from './runtime/generationGuard.ts'
|
|
8
9
|
import type { EffectResult } from './runtime/types/EffectResult.ts'
|
|
9
10
|
import type { State } from './runtime/types/State.ts'
|
|
11
|
+
import { untrack } from './runtime/untrack.ts'
|
|
10
12
|
|
|
11
13
|
/*
|
|
12
14
|
The single reaction primitive: `watch(source, handler)` names its trigger and runs
|
|
@@ -24,6 +26,14 @@ into a server bundle; server-side it is an inert no-op. Unlike bare `watch(…)`
|
|
|
24
26
|
the SSR back-end strips — a `.watch(…)` member call survives to the server and relies on
|
|
25
27
|
that inert stub.
|
|
26
28
|
|
|
29
|
+
Tracking contract (ADR-0044): the named source(s) are the sole triggers — the handler
|
|
30
|
+
is a sink, run untracked, so a reactive read in its body never becomes an accidental
|
|
31
|
+
extra trigger. Only the bare `watch(thunk)` form auto-tracks everything it reads (that
|
|
32
|
+
IS its job — it is the compiler binding). This makes all explicit-source forms uniform:
|
|
33
|
+
the subscribable (cache.on, per-frame) and rpc (handler in a .then microtask) branches
|
|
34
|
+
already ran their handlers out of the tracking window; the two synchronous cell branches
|
|
35
|
+
now match by wrapping the handler in `untrack`.
|
|
36
|
+
|
|
27
37
|
Sources (discriminated at runtime, monomorphic per branch):
|
|
28
38
|
watch(thunk) // compiler binding form — auto-tracked, == effect(thunk)
|
|
29
39
|
watch(count, n => …) // a state cell → handler(newValue)
|
|
@@ -61,9 +71,13 @@ export function watch(
|
|
|
61
71
|
also silence the compiler's generated bindings, which build on the client where the test
|
|
62
72
|
DOM sets no global `window`). SSR-safety comes from the compiler stripping author
|
|
63
73
|
`watch(...)` calls, and the subscribable branch's own server guard (cache.on). */
|
|
64
|
-
/* Compiler binding form watch(thunk): a bare auto-tracked effect (== today's $$effect).
|
|
74
|
+
/* Compiler binding form watch(thunk): a bare auto-tracked effect (== today's $$effect). A
|
|
75
|
+
pending blocking `await` read throws a `SuspenseSignal` (ADR-0042) — withhold this reaction
|
|
76
|
+
(e.g. leave a `class:`/`style:` binding untouched) until the value resolves; the read tracked
|
|
77
|
+
its cell, so the effect re-runs on settle. Rethrows any real error. */
|
|
65
78
|
if (argsOrHandler === undefined) {
|
|
66
|
-
|
|
79
|
+
const thunk = source as () => EffectResult
|
|
80
|
+
return effect(() => withSuspense<EffectResult>(thunk, undefined))
|
|
67
81
|
}
|
|
68
82
|
/* watch(fn, args, handler): an rpc selector with explicit args. */
|
|
69
83
|
if (maybeHandler !== undefined) {
|
|
@@ -88,18 +102,22 @@ export function watch(
|
|
|
88
102
|
) {
|
|
89
103
|
return reactToRpc(source, undefined, handler)
|
|
90
104
|
}
|
|
91
|
-
/* Multiple cells → fire on any change; hand the handler the current values.
|
|
105
|
+
/* Multiple cells → fire on any change; hand the handler the current values. Read every
|
|
106
|
+
cell (the tracked triggers), then run the handler untracked so a reactive read in its
|
|
107
|
+
body is not captured as an extra trigger (ADR-0044). */
|
|
92
108
|
if (Array.isArray(source)) {
|
|
93
109
|
const cells = source as ReadonlyArray<State<unknown>>
|
|
94
110
|
return effect(() => {
|
|
95
111
|
const values = cells.map((cell) => cell.value)
|
|
96
|
-
handler(values)
|
|
112
|
+
untrack(() => handler(values))
|
|
97
113
|
})
|
|
98
114
|
}
|
|
99
|
-
/* A single state cell → handler(newValue) on change.
|
|
115
|
+
/* A single state cell → handler(newValue) on change. The cell read is the tracked
|
|
116
|
+
trigger; the handler is a sink, run untracked (ADR-0044). */
|
|
100
117
|
const cell = source as State<unknown>
|
|
101
118
|
return effect(() => {
|
|
102
|
-
|
|
119
|
+
const value = cell.value
|
|
120
|
+
untrack(() => handler(value))
|
|
103
121
|
})
|
|
104
122
|
}
|
|
105
123
|
|
|
@@ -115,7 +133,7 @@ function isSubscribable(source: unknown): boolean {
|
|
|
115
133
|
/*
|
|
116
134
|
Reacts to an rpc's cached value: an effect that runs the smart read (subscribing
|
|
117
135
|
its key synchronously, so the effect re-runs when the value changes — a refresh,
|
|
118
|
-
|
|
136
|
+
an amend, an invalidate) and pipes the resolved value to the handler. Triggers the
|
|
119
137
|
read, unlike peek: `watch` observes a live query, so it keeps it flowing.
|
|
120
138
|
*/
|
|
121
139
|
function reactToRpc(fn: unknown, args: unknown, handler: (value: unknown) => void): () => void {
|
package/src/serverEntry.ts
CHANGED
|
@@ -26,10 +26,12 @@ import { shell } from './_virtual/shell.ts'
|
|
|
26
26
|
import { sockets } from './_virtual/sockets.ts'
|
|
27
27
|
import { exitWithParent } from './lib/bundle/exitWithParent.ts'
|
|
28
28
|
import { loadEnvFromBinaryDir } from './lib/cli/loadEnvFromBinaryDir.ts'
|
|
29
|
+
import { broadcastAmend } from './lib/server/runtime/amendBroadcaster.ts'
|
|
29
30
|
import { broadcastCacheStaleness } from './lib/server/runtime/cacheStalenessBroadcaster.ts'
|
|
30
31
|
import { createServer } from './lib/server/runtime/createServer.ts'
|
|
31
32
|
import { requestContext } from './lib/server/runtime/requestContext.ts'
|
|
32
33
|
import { resolvePageSnapshot } from './lib/server/runtime/resolvePageSnapshot.ts'
|
|
34
|
+
import { amendBroadcastSlot } from './lib/shared/amendBroadcastSlot.ts'
|
|
33
35
|
import { cacheStalenessSlot } from './lib/shared/cacheStalenessSlot.ts'
|
|
34
36
|
import { cacheStoreSlot } from './lib/shared/cacheStoreSlot.ts'
|
|
35
37
|
import { createCacheStore } from './lib/shared/createCacheStore.ts'
|
|
@@ -40,6 +42,7 @@ import { pendingAsyncCellsSlot } from './lib/shared/pendingAsyncCellsSlot.ts'
|
|
|
40
42
|
import { resolvedCellsSlot } from './lib/shared/resolvedCellsSlot.ts'
|
|
41
43
|
import { runningAsStandaloneBinary } from './lib/shared/runningAsStandaloneBinary.ts'
|
|
42
44
|
import { sharedCacheStoreSlot } from './lib/shared/sharedCacheStoreSlot.ts'
|
|
45
|
+
import { socketTailsSlot } from './lib/shared/socketTailsSlot.ts'
|
|
43
46
|
import { streamedCellsSlot } from './lib/shared/streamedCellsSlot.ts'
|
|
44
47
|
|
|
45
48
|
/*
|
|
@@ -89,6 +92,14 @@ so its server socket code never enters the client reachability graph.
|
|
|
89
92
|
*/
|
|
90
93
|
cacheStalenessSlot.resolver = () => broadcastCacheStaleness
|
|
91
94
|
|
|
95
|
+
/*
|
|
96
|
+
Server-side amend(args, value) broadcasts the keyed value to every authorized reader
|
|
97
|
+
instead of mutating a throwaway request store (ADR-0043). Imported ONLY here so the
|
|
98
|
+
broadcaster's socket code never enters the client reachability graph — the same import
|
|
99
|
+
discipline as the staleness broadcaster above.
|
|
100
|
+
*/
|
|
101
|
+
amendBroadcastSlot.resolver = () => broadcastAmend
|
|
102
|
+
|
|
92
103
|
/* One process-wide fallback list for reads with no request in flight (boot/cron/socket) —
|
|
93
104
|
real requests each carry their own `pendingAsyncCells`, so the SSR barrier drains a
|
|
94
105
|
per-request list and concurrent renders never cross-contaminate. */
|
|
@@ -99,6 +110,10 @@ const sharedResolvedCells = { entries: [] }
|
|
|
99
110
|
resolvedCellsSlot.resolver = () => requestContext.getStore()?.resolvedCells ?? sharedResolvedCells
|
|
100
111
|
const sharedStreamedCells = { entries: [] }
|
|
101
112
|
streamedCellsSlot.resolver = () => requestContext.getStore()?.streamedCells ?? sharedStreamedCells
|
|
113
|
+
/* No shared fallback: `defineSocket.peek` records into this on every retained-frame read, including
|
|
114
|
+
reads outside any request (ws handlers, boot) — a fallback list would accumulate entries no render
|
|
115
|
+
ever drains. Off-request this resolves undefined and the record is skipped (see socketTailsSlot). */
|
|
116
|
+
socketTailsSlot.resolver = () => requestContext.getStore()?.socketTails
|
|
102
117
|
const sharedDocSnapshots = { entries: [] }
|
|
103
118
|
docSnapshotsSlot.resolver = () => requestContext.getStore()?.docSnapshots ?? sharedDocSnapshots
|
|
104
119
|
|
package/src/lib/shared/patch.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { cache } from './cache.ts'
|
|
2
|
-
import type { CacheOptions } from './types/CacheOptions.ts'
|
|
3
|
-
import type { CacheSelector } from './types/CacheSelector.ts'
|
|
4
|
-
import type { RemoteFunction } from './types/RemoteFunction.ts'
|
|
5
|
-
|
|
6
|
-
/*
|
|
7
|
-
Mutate the retained value of the matching cached read(s) in place — reactive
|
|
8
|
-
(readers re-render), no network. The optimistic-update / real-time primitive:
|
|
9
|
-
patch a cached list from a socket frame (`on(chat, m => getList.patch(l => [...l,
|
|
10
|
-
m]))`) or apply an optimistic edit before a write lands. The updater receives the
|
|
11
|
-
current decoded value and returns the next.
|
|
12
|
-
|
|
13
|
-
patch(getFoo, args, updater) → that exact call
|
|
14
|
-
patch(getFoo, updater) → every args-variant of that rpc
|
|
15
|
-
patch({ tags }, updater) → every entry sharing a tag
|
|
16
|
-
|
|
17
|
-
Instance sugar `getFoo.patch(args?, updater)` ≡ `patch(getFoo, args, updater)`.
|
|
18
|
-
The updater is always the last argument; a not-yet-read key has nothing to patch.
|
|
19
|
-
*/
|
|
20
|
-
// @documentation cache
|
|
21
|
-
export function patch<Args, Return>(
|
|
22
|
-
fn: RemoteFunction<Args, Return> | ((args?: Args) => Promise<Return>),
|
|
23
|
-
args: Args | undefined,
|
|
24
|
-
updater: (current: Return) => Return,
|
|
25
|
-
): void
|
|
26
|
-
export function patch<Args, Return>(
|
|
27
|
-
fn: RemoteFunction<Args, Return> | ((args?: Args) => Promise<Return>),
|
|
28
|
-
updater: (current: Return) => Return,
|
|
29
|
-
): void
|
|
30
|
-
export function patch<Return>(
|
|
31
|
-
selector: Pick<CacheOptions, 'tags'>,
|
|
32
|
-
updater: (current: Return) => Return,
|
|
33
|
-
): void
|
|
34
|
-
export function patch(selector: unknown, argsOrUpdater: unknown, maybeUpdater?: unknown): void {
|
|
35
|
-
/* Updater is always last: 3 args → (selector, args, updater); 2 args → (selector, updater). */
|
|
36
|
-
const updater = (maybeUpdater !== undefined ? maybeUpdater : argsOrUpdater) as (
|
|
37
|
-
current: unknown,
|
|
38
|
-
) => unknown
|
|
39
|
-
const args = maybeUpdater !== undefined ? argsOrUpdater : undefined
|
|
40
|
-
cache.patch(selector as CacheSelector<unknown, unknown>, args, updater)
|
|
41
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import ts from 'typescript'
|
|
2
|
-
|
|
3
|
-
/*
|
|
4
|
-
Maps a compound-assignment operator to its plain binary counterpart, for lowering
|
|
5
|
-
`x += y` into an unconditional read-combine-write. Logical assignments
|
|
6
|
-
(`||=`/`&&=`/`??=`) lower the same way — the patch/cell write always fires,
|
|
7
|
-
consistent with how `+=` lowers. Shared by lowerDocAccess (the `$$model` write path)
|
|
8
|
-
and renameSignalRefs (the `$$writeCell` linked-cell path) so the two lowerings can't
|
|
9
|
-
drift.
|
|
10
|
-
*/
|
|
11
|
-
export const COMPOUND_ASSIGNMENT_OPERATORS = new Map<ts.SyntaxKind, ts.BinaryOperator>([
|
|
12
|
-
[ts.SyntaxKind.PlusEqualsToken, ts.SyntaxKind.PlusToken],
|
|
13
|
-
[ts.SyntaxKind.MinusEqualsToken, ts.SyntaxKind.MinusToken],
|
|
14
|
-
[ts.SyntaxKind.AsteriskEqualsToken, ts.SyntaxKind.AsteriskToken],
|
|
15
|
-
[ts.SyntaxKind.SlashEqualsToken, ts.SyntaxKind.SlashToken],
|
|
16
|
-
[ts.SyntaxKind.BarBarEqualsToken, ts.SyntaxKind.BarBarToken],
|
|
17
|
-
[ts.SyntaxKind.AmpersandAmpersandEqualsToken, ts.SyntaxKind.AmpersandAmpersandToken],
|
|
18
|
-
[ts.SyntaxKind.QuestionQuestionEqualsToken, ts.SyntaxKind.QuestionQuestionToken],
|
|
19
|
-
])
|