@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,6 +1,8 @@
|
|
|
1
1
|
import ts from 'typescript'
|
|
2
2
|
import { ABIDE_PACKAGE_NAME } from '../../shared/ABIDE_PACKAGE_NAME.ts'
|
|
3
3
|
import { attrLiftPosition } from './attrLiftPosition.ts'
|
|
4
|
+
import { hasTopLevelAwait } from './hasTopLevelAwait.ts'
|
|
5
|
+
import { isFunctionScopeBoundary } from './isFunctionScopeBoundary.ts'
|
|
4
6
|
import { isWhitespaceText } from './isWhitespaceText.ts'
|
|
5
7
|
import { type LiftPosition, liftAsyncSubExpressions } from './liftAsyncSubExpressions.ts'
|
|
6
8
|
import { parseTemplate } from './parseTemplate.ts'
|
|
@@ -102,6 +104,15 @@ export function compileShadow(
|
|
|
102
104
|
builder.raw(
|
|
103
105
|
'declare function $$cellValue<R>(seed: () => R): R extends AsyncIterable<infer F> ? F : Awaited<R>\n',
|
|
104
106
|
)
|
|
107
|
+
/* The STREAMING sibling (ADR-0042): a no-`await` async `computed`/`linked` cell reads
|
|
108
|
+
`undefined`-while-pending, so its shadow type is the resolved value UNIONED with `undefined` —
|
|
109
|
+
the author must guard it (`?.`/`??`), exactly the runtime. A BLOCKING (`await`) cell uses
|
|
110
|
+
`$$cellValue` above (resolved `T`, never pending — its render region suspends). Selected in
|
|
111
|
+
`scopeLineFor` by the syntactic blocking bit. Unconditional (a named cell can occur with no
|
|
112
|
+
classifier), matching `$$cellValue`. */
|
|
113
|
+
builder.raw(
|
|
114
|
+
'declare function $$cellValuePending<R>(seed: () => R): (R extends AsyncIterable<infer F> ? F : Awaited<R>) | undefined\n',
|
|
115
|
+
)
|
|
105
116
|
/* The peek helpers the async-interpolation wrap targets (ADR-0032): `$$peek` unwraps a promise
|
|
106
117
|
sub-expression to its resolved value (`undefined` while pending) and `$$peekStream` reads an
|
|
107
118
|
async iterable's latest frame, so `getFoo()?.name`/`{#if getFoo()}` type-check against the
|
|
@@ -712,7 +723,13 @@ function scopeLineFor(
|
|
|
712
723
|
const fnIsThunk = ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)
|
|
713
724
|
const wrapPrefix = fnIsThunk ? '' : seedIsAsync(fn) ? 'async () => (' : '() => ('
|
|
714
725
|
const wrapSuffix = fnIsThunk ? '' : ')'
|
|
715
|
-
|
|
726
|
+
/* ADR-0042: a STREAMING promise cell (async seed, no top-level `await`) reads
|
|
727
|
+
`undefined`-while-pending → `$$cellValuePending` (resolved `T | undefined`). A BLOCKING
|
|
728
|
+
(`await`) cell suspends its region so it is always resolved, and a sync cell is never pending
|
|
729
|
+
→ `$$cellValue` (resolved `T`). (A bare stream seed keeps `$$cellValue`'s frame type — the
|
|
730
|
+
pre-existing type-directed case, unchanged here.) */
|
|
731
|
+
const helper = seedIsAsync(fn) && !seedIsBlocking(fn) ? '$$cellValuePending' : '$$cellValue'
|
|
732
|
+
const prefix = `${keyword} ${name}${annotation} = ${helper}(${wrapPrefix}`
|
|
716
733
|
return withCalleeRef({
|
|
717
734
|
text: `${prefix}${verbatim(fn)}${wrapSuffix});`,
|
|
718
735
|
segments: [span(declaration.name, keywordOffset), span(fn, prefix.length)],
|
|
@@ -732,40 +749,20 @@ function seedIsAsync(seed: ts.Expression): boolean {
|
|
|
732
749
|
?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) ?? false
|
|
733
750
|
)
|
|
734
751
|
}
|
|
735
|
-
return
|
|
752
|
+
return hasTopLevelAwait(seed)
|
|
736
753
|
}
|
|
737
754
|
|
|
738
|
-
/* True
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
if (ts.isAwaitExpression(child)) {
|
|
748
|
-
found = true
|
|
749
|
-
return
|
|
750
|
-
}
|
|
751
|
-
ts.forEachChild(child, visit)
|
|
755
|
+
/* True for a BLOCKING async cell (ADR-0042 D6): an async thunk whose BODY has a top-level `await`
|
|
756
|
+
(`computed(async () => await X)`), or a bare seed carrying a top-level `await` (`computed(await
|
|
757
|
+
X)` — wrapSeed makes it an async thunk over that await). An async thunk with NO await
|
|
758
|
+
(`computed(async () => getFoo())`) is STREAMING, not blocking. Shares `hasTopLevelAwait` with
|
|
759
|
+
desugarSignals' `isBlockingSeed` so the shadow type (`$$cellValue` vs `$$cellValuePending`) agrees
|
|
760
|
+
with the runtime's blocking flag — the "one predicate" ADR-0042 D5 requires. */
|
|
761
|
+
function seedIsBlocking(seed: ts.Expression): boolean {
|
|
762
|
+
if (ts.isArrowFunction(seed) || ts.isFunctionExpression(seed)) {
|
|
763
|
+
return seedIsAsync(seed) && hasTopLevelAwait(seed.body)
|
|
752
764
|
}
|
|
753
|
-
|
|
754
|
-
return found
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
/* A node that opens its own (possibly async) function scope — awaits inside it are the author's
|
|
758
|
-
concern, so both the top-level-await walk and the seed classifier stop descending here. */
|
|
759
|
-
function isFunctionScopeBoundary(node: ts.Node): boolean {
|
|
760
|
-
return (
|
|
761
|
-
ts.isFunctionDeclaration(node) ||
|
|
762
|
-
ts.isFunctionExpression(node) ||
|
|
763
|
-
ts.isArrowFunction(node) ||
|
|
764
|
-
ts.isMethodDeclaration(node) ||
|
|
765
|
-
ts.isGetAccessorDeclaration(node) ||
|
|
766
|
-
ts.isSetAccessorDeclaration(node) ||
|
|
767
|
-
ts.isConstructorDeclaration(node)
|
|
768
|
-
)
|
|
765
|
+
return hasTopLevelAwait(seed)
|
|
769
766
|
}
|
|
770
767
|
|
|
771
768
|
/* Whether any ELEMENT in the tree carries an `attach` — gates emitting the DOM-typed
|
|
@@ -1047,7 +1044,11 @@ function emitNode(node: TemplateNode, builder: Builder): void {
|
|
|
1047
1044
|
} else if (branch.branch === 'catch' && branch.as !== undefined) {
|
|
1048
1045
|
builder.raw('const ')
|
|
1049
1046
|
builder.mapped(branch.as, branch.asLoc)
|
|
1050
|
-
|
|
1047
|
+
/* `unknown`, not `any` — a `catch` error is statically unknowable, and this
|
|
1048
|
+
matches what strict-mode plain TS gives a real `catch (err)`
|
|
1049
|
+
(`useUnknownInCatchVariables`), so the author must narrow before dereferencing
|
|
1050
|
+
(`err instanceof Error ? … : …`) exactly as outside a template (ADR-0042 fidelity). */
|
|
1051
|
+
builder.raw(' = undefined as unknown;\n')
|
|
1051
1052
|
}
|
|
1052
1053
|
emitNodes(branch.children, builder)
|
|
1053
1054
|
builder.raw('}\n')
|
|
@@ -1093,7 +1094,9 @@ function emitNode(node: TemplateNode, builder: Builder): void {
|
|
|
1093
1094
|
handlers consume their own branches inline and never route through this case. */
|
|
1094
1095
|
builder.raw('{\n')
|
|
1095
1096
|
if (node.as !== undefined) {
|
|
1096
|
-
|
|
1097
|
+
/* `unknown`, not `any` — see the await-`catch` binding above (matches strict TS's
|
|
1098
|
+
`catch (err)`, forcing the author to narrow). */
|
|
1099
|
+
builder.raw(`const ${node.as} = undefined as unknown;\n`)
|
|
1097
1100
|
}
|
|
1098
1101
|
emitNodes(node.children, builder)
|
|
1099
1102
|
builder.raw('}\n')
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import ts from 'typescript'
|
|
2
2
|
import { assignmentTargetNames } from './assignmentTargetNames.ts'
|
|
3
|
+
import { hasTopLevelAwait } from './hasTopLevelAwait.ts'
|
|
3
4
|
import { type ReactiveImportBindings, reactiveImportBindings } from './resolveReactiveExport.ts'
|
|
4
5
|
import { signalCallee } from './signalCallee.ts'
|
|
5
6
|
import type { InterpolationKind } from './types/InterpolationKind.ts'
|
|
@@ -13,34 +14,6 @@ type EagerStreamPredicate = (declaration: ts.VariableDeclaration) => boolean
|
|
|
13
14
|
|
|
14
15
|
const factory = ts.factory
|
|
15
16
|
|
|
16
|
-
/* True when `node` holds an `await` that runs at the seed's own top level — one NOT nested
|
|
17
|
-
inside a further function. The walk stops at every nested function boundary, so an
|
|
18
|
-
`await` in an inner callback (`items.map(async (x) => await f(x))`) does not count: only
|
|
19
|
-
a top-level `await` marks the seed itself as an async thunk the async-cell path unwraps. */
|
|
20
|
-
function hasTopLevelAwait(node: ts.Node): boolean {
|
|
21
|
-
let found = false
|
|
22
|
-
const visit = (child: ts.Node): void => {
|
|
23
|
-
if (found) {
|
|
24
|
-
return
|
|
25
|
-
}
|
|
26
|
-
/* A nested function is its own await scope — don't descend into it. */
|
|
27
|
-
if (
|
|
28
|
-
ts.isFunctionDeclaration(child) ||
|
|
29
|
-
ts.isFunctionExpression(child) ||
|
|
30
|
-
ts.isArrowFunction(child)
|
|
31
|
-
) {
|
|
32
|
-
return
|
|
33
|
-
}
|
|
34
|
-
if (ts.isAwaitExpression(child)) {
|
|
35
|
-
found = true
|
|
36
|
-
return
|
|
37
|
-
}
|
|
38
|
-
ts.forEachChild(child, visit)
|
|
39
|
-
}
|
|
40
|
-
visit(node)
|
|
41
|
-
return found
|
|
42
|
-
}
|
|
43
|
-
|
|
44
17
|
/* Normalises a `computed`/`linked` argument into a seed THUNK: a literal `() => …` /
|
|
45
18
|
`function` argument passes through unchanged (the author already wrote the thunk), any
|
|
46
19
|
other expression is wrapped as `() => arg`. The wrapper arrow is made ASYNC when the
|
|
@@ -63,6 +36,24 @@ function wrapSeed(argument: ts.Expression): ts.Expression {
|
|
|
63
36
|
)
|
|
64
37
|
}
|
|
65
38
|
|
|
39
|
+
/* Wraps a seed expression as an async unwrapping thunk `async () => await (arg)` — for a
|
|
40
|
+
type-directed PROMISE seed (ADR-0023/0043) the author wrote WITHOUT `await`
|
|
41
|
+
(`state.computed(getFoo())`). The `async`+`await` makes createAsyncCell unwrap the resolved
|
|
42
|
+
value; a plain `() => getFoo()` thunk would fall to trackedComputed's lazy opaque path (its
|
|
43
|
+
probe self-identifies only a stream, never a promise). The parens keep a comma/ternary seed
|
|
44
|
+
a single await operand. Distinct from `wrapSeed`, which only makes the arrow async when the
|
|
45
|
+
arg already carries a top-level `await`. */
|
|
46
|
+
function wrapAwaitSeed(argument: ts.Expression): ts.Expression {
|
|
47
|
+
return factory.createArrowFunction(
|
|
48
|
+
[factory.createModifier(ts.SyntaxKind.AsyncKeyword)],
|
|
49
|
+
undefined,
|
|
50
|
+
[],
|
|
51
|
+
undefined,
|
|
52
|
+
factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
|
|
53
|
+
factory.createAwaitExpression(factory.createParenthesizedExpression(argument)),
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
66
57
|
/* True for an async seed thunk — an arrow/function carrying the `async` modifier, whether
|
|
67
58
|
from `wrapSeed`'s `await` lowering or a passthrough `async () => …` literal the author
|
|
68
59
|
wrote. The routing signal: an async seed becomes an async cell (read via `$$readCell`),
|
|
@@ -106,6 +97,23 @@ function isBareCallComputed(declaration: ts.VariableDeclaration): boolean {
|
|
|
106
97
|
return ts.isCallExpression(argument) || ts.isIdentifier(argument)
|
|
107
98
|
}
|
|
108
99
|
|
|
100
|
+
/* True when a `computed`/`linked` seed is a BLOCKING async cell (ADR-0042 D6): its wrapped seed
|
|
101
|
+
is an async thunk whose BODY has a top-level `await` — `computed(await X)` (wrapSeed makes it
|
|
102
|
+
`async () => await X`) or `computed(async () => await X)`. An async thunk with NO await
|
|
103
|
+
(`computed(async () => getFoo())`) is STREAMING — `await` is the sole blocking marker. A
|
|
104
|
+
sync/stream seed is not blocking. The read of a blocking cell suspends its render region until
|
|
105
|
+
the value resolves; a streaming cell reads `undefined`-while-pending. Reuses `hasTopLevelAwait`
|
|
106
|
+
applied to the thunk BODY (not the arrow), so the walk descends exactly one function level. */
|
|
107
|
+
function isBlockingSeed(argument: ts.Expression): boolean {
|
|
108
|
+
const wrapped = wrapSeed(argument)
|
|
109
|
+
if (!isAsyncSeed(wrapped)) {
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
const body =
|
|
113
|
+
ts.isArrowFunction(wrapped) || ts.isFunctionExpression(wrapped) ? wrapped.body : wrapped
|
|
114
|
+
return hasTopLevelAwait(body)
|
|
115
|
+
}
|
|
116
|
+
|
|
109
117
|
/* Emits a compile WARNING (best-effort, never fatal) when a signal read appears AFTER the
|
|
110
118
|
first top-level `await` in an async seed — a value read there is no longer tracked, so the
|
|
111
119
|
cell won't reseed when it changes. Detection only: reads before the await (or with no
|
|
@@ -223,6 +231,10 @@ export function desugarSignals(
|
|
|
223
231
|
derivedNames: Set<string>
|
|
224
232
|
computedNames: Set<string>
|
|
225
233
|
cellReadNames: Set<string>
|
|
234
|
+
/* The full BLOCKING cell set (ADR-0042): the template-injected `await` cells passed in, unioned
|
|
235
|
+
with the script-level `await` computeds/linked collected here. The client template lowering
|
|
236
|
+
reads these via `$$readCellBlocking` (suspend-on-pending). */
|
|
237
|
+
blockingCellNames: Set<string>
|
|
226
238
|
} {
|
|
227
239
|
assertNoRemovedReaders(source)
|
|
228
240
|
/* The full set of names written anywhere (this script + the template). A prop in this set
|
|
@@ -241,6 +253,10 @@ export function desugarSignals(
|
|
|
241
253
|
One read shape covers both — `$$readCell` peeks an async cell and reads `.value` off a
|
|
242
254
|
sync one — so `linked(getStream())` auto-tracks with no read-site branching. */
|
|
243
255
|
const cellReadNames = new Set<string>()
|
|
256
|
+
/* Script-level BLOCKING cell names (ADR-0042 D6): a `computed`/`linked` whose seed carries a
|
|
257
|
+
top-level `await`. Unioned with the template-injected `blockingCellNames` and returned so the
|
|
258
|
+
CLIENT template lowering reads them via `$$readCellBlocking` (suspend-on-pending). */
|
|
259
|
+
const scriptBlockingNames = new Set<string>()
|
|
244
260
|
/* A `props()` destructure must be lowered even when it declares no reactive binding
|
|
245
261
|
(a rest-only `const { ...rest } = props()`), so track its presence on its own. */
|
|
246
262
|
let hasPropsDestructure = false
|
|
@@ -277,6 +293,21 @@ export function desugarSignals(
|
|
|
277
293
|
}
|
|
278
294
|
return isBareCallComputed(declaration)
|
|
279
295
|
}
|
|
296
|
+
/* A type-directed PROMISE seed (ADR-0023/0043): a no-`await` `state.computed(getFoo())` whose
|
|
297
|
+
seed's checker type resolves to a promise. Routed to an eager STREAMING async cell that
|
|
298
|
+
unwraps the resolved value and reactively re-resolves when its tracked deps change — the
|
|
299
|
+
script-level twin of a bare async interpolation (ADR-0032). Only fires with a warm classifier
|
|
300
|
+
('promise'); fail-open (no program) leaves it to `isEagerStreamComputed`'s bare-call probe,
|
|
301
|
+
and the `await`-marker seed is excluded (its wrapped thunk is already async). Kept separate
|
|
302
|
+
from `isEagerStreamComputed` so `computedStatements` can wrap it as `async () => await (seed)`
|
|
303
|
+
(a stream seed stays a bare probe thunk). */
|
|
304
|
+
const isPromiseComputed: EagerStreamPredicate = (declaration) => {
|
|
305
|
+
const argument = seedArgument(declaration)
|
|
306
|
+
if (argument === undefined || isAsyncSeed(wrapSeed(argument))) {
|
|
307
|
+
return false
|
|
308
|
+
}
|
|
309
|
+
return seedKind(argument) === 'promise'
|
|
310
|
+
}
|
|
280
311
|
for (const statement of source.statements) {
|
|
281
312
|
if (!ts.isVariableStatement(statement)) {
|
|
282
313
|
continue
|
|
@@ -323,8 +354,19 @@ export function desugarSignals(
|
|
|
323
354
|
`name()` (a sync/promise seed), or an eager cell read via `$$readCell(name)`
|
|
324
355
|
when the wrapped seed is async (an `await`-lowered / passthrough-`async` thunk)
|
|
325
356
|
or its seed type resolves to a stream (`isEagerStreamComputed`, ADR-0023). */
|
|
326
|
-
if (
|
|
357
|
+
if (
|
|
358
|
+
isAsyncComputed(declaration) ||
|
|
359
|
+
isEagerStreamComputed(declaration) ||
|
|
360
|
+
isPromiseComputed(declaration)
|
|
361
|
+
) {
|
|
327
362
|
cellReadNames.add(declaration.name.text)
|
|
363
|
+
/* A blocking async computed (author `await`) reads suspend-on-pending; a bare
|
|
364
|
+
promise (isPromiseComputed) carries no `await` → streaming, so isBlockingSeed
|
|
365
|
+
is false and it never joins this set. */
|
|
366
|
+
const argument = seedArgument(declaration)
|
|
367
|
+
if (argument !== undefined && isBlockingSeed(argument)) {
|
|
368
|
+
scriptBlockingNames.add(declaration.name.text)
|
|
369
|
+
}
|
|
328
370
|
} else {
|
|
329
371
|
computedNames.add(declaration.name.text)
|
|
330
372
|
}
|
|
@@ -333,6 +375,11 @@ export function desugarSignals(
|
|
|
333
375
|
is synchronous, an `AsyncState` when it tracks a promise/stream — one read
|
|
334
376
|
shape auto-tracks whichever source the runtime primitive resolved to. */
|
|
335
377
|
cellReadNames.add(declaration.name.text)
|
|
378
|
+
/* A blocking async linked (author `await`) reads suspend-on-pending, like computed. */
|
|
379
|
+
const argument = seedArgument(declaration)
|
|
380
|
+
if (argument !== undefined && isBlockingSeed(argument)) {
|
|
381
|
+
scriptBlockingNames.add(declaration.name.text)
|
|
382
|
+
}
|
|
336
383
|
} else if (callee === 'state') {
|
|
337
384
|
/* `state(initial, transform)` → a `.value` cell (its write-coercion transform
|
|
338
385
|
forces a local store); referenced as `name.value`, unchanged. */
|
|
@@ -381,13 +428,21 @@ export function desugarSignals(
|
|
|
381
428
|
blockingCellNames,
|
|
382
429
|
writtenNames,
|
|
383
430
|
isEagerStreamComputed,
|
|
431
|
+
isPromiseComputed,
|
|
384
432
|
),
|
|
385
433
|
)
|
|
386
434
|
}
|
|
387
435
|
return factory.updateSourceFile(root, statements)
|
|
388
436
|
}
|
|
389
437
|
|
|
390
|
-
return {
|
|
438
|
+
return {
|
|
439
|
+
transformer,
|
|
440
|
+
stateNames,
|
|
441
|
+
derivedNames,
|
|
442
|
+
computedNames,
|
|
443
|
+
cellReadNames,
|
|
444
|
+
blockingCellNames: new Set([...blockingCellNames, ...scriptBlockingNames]),
|
|
445
|
+
}
|
|
391
446
|
}
|
|
392
447
|
|
|
393
448
|
/* The lowered form of a top-level statement: state slots → `model.x = init`
|
|
@@ -402,12 +457,19 @@ function loweredStatement(
|
|
|
402
457
|
blockingCellNames: ReadonlySet<string>,
|
|
403
458
|
writtenNames: ReadonlySet<string>,
|
|
404
459
|
isEagerStreamComputed: EagerStreamPredicate,
|
|
460
|
+
isPromiseComputed: EagerStreamPredicate,
|
|
405
461
|
): ts.Statement[] {
|
|
406
462
|
rejectMixedDeclaration(statement, bindings)
|
|
407
463
|
return (
|
|
408
464
|
injectedComputedStatements(statement, injectedCellNames, blockingCellNames) ??
|
|
409
465
|
stateAssignmentStatements(statement, bindings) ??
|
|
410
|
-
computedStatements(
|
|
466
|
+
computedStatements(
|
|
467
|
+
statement,
|
|
468
|
+
bindings,
|
|
469
|
+
signalNames,
|
|
470
|
+
isEagerStreamComputed,
|
|
471
|
+
isPromiseComputed,
|
|
472
|
+
) ??
|
|
411
473
|
propsStatements(statement, bindings, writtenNames) ??
|
|
412
474
|
cellStatements(statement, bindings, signalNames) ?? [statement]
|
|
413
475
|
)
|
|
@@ -598,6 +660,7 @@ function computedStatements(
|
|
|
598
660
|
bindings: ReactiveImportBindings,
|
|
599
661
|
signalNames: ReadonlySet<string>,
|
|
600
662
|
isEagerStreamComputed: EagerStreamPredicate,
|
|
663
|
+
isPromiseComputed: EagerStreamPredicate,
|
|
601
664
|
): ts.Statement[] | undefined {
|
|
602
665
|
if (!ts.isVariableStatement(statement)) {
|
|
603
666
|
return undefined
|
|
@@ -627,10 +690,40 @@ function computedStatements(
|
|
|
627
690
|
)
|
|
628
691
|
: wrapSeed(argument)
|
|
629
692
|
if (isAsyncSeed(wrapped)) {
|
|
630
|
-
/* Async seed → the eager
|
|
631
|
-
|
|
693
|
+
/* Async seed → the eager async cell (`AsyncComputed`, read via `$$readCell`), the
|
|
694
|
+
runtime unwraps the promise. `await` in the thunk body → BLOCKING (joins the SSR
|
|
695
|
+
barrier, resolves inline, client suspends its render region); async modifier with NO
|
|
696
|
+
await → STREAMING (ships pending, resolves on the client) — ADR-0042 D6. Routed
|
|
697
|
+
through `trackedComputed(thunk, streaming)` (same createAsyncCell for an async thunk)
|
|
698
|
+
to carry the flag, matching the template-injected path. */
|
|
632
699
|
warnPostAwaitReads(wrapped, signalNames)
|
|
633
|
-
|
|
700
|
+
const streaming = argument === undefined || !isBlockingSeed(argument)
|
|
701
|
+
statements.push(
|
|
702
|
+
constDeclaration(
|
|
703
|
+
name,
|
|
704
|
+
scopeMethodCall('trackedComputed', [
|
|
705
|
+
wrapped,
|
|
706
|
+
streaming ? factory.createTrue() : factory.createFalse(),
|
|
707
|
+
]),
|
|
708
|
+
),
|
|
709
|
+
)
|
|
710
|
+
} else if (argument !== undefined && isPromiseComputed(declaration)) {
|
|
711
|
+
/* Type-directed PROMISE seed (ADR-0023/0043): a bare `state.computed(getFoo())` whose
|
|
712
|
+
seed resolves to a promise. Wrap it as `async () => await (seed)` so createAsyncCell
|
|
713
|
+
unwraps the resolved value — a plain `() => getFoo()` thunk would fall to
|
|
714
|
+
trackedComputed's lazy opaque path (its probe self-identifies only a stream). Passed
|
|
715
|
+
`streaming: true` (the `true` arg): no author `await` → it does NOT join the SSR
|
|
716
|
+
blocking barrier — the shell ships pending and the client resolves + reactively
|
|
717
|
+
re-resolves, the ADR-0032 no-await tier. `await getFoo()` stays the BLOCKING form. */
|
|
718
|
+
statements.push(
|
|
719
|
+
constDeclaration(
|
|
720
|
+
name,
|
|
721
|
+
scopeMethodCall('trackedComputed', [
|
|
722
|
+
wrapAwaitSeed(argument),
|
|
723
|
+
factory.createTrue(),
|
|
724
|
+
]),
|
|
725
|
+
),
|
|
726
|
+
)
|
|
634
727
|
} else if (isEagerStreamComputed(declaration)) {
|
|
635
728
|
/* Stream seed → the eager `trackedComputed`, which probes the seed and auto-tracks a
|
|
636
729
|
stream (`AsyncComputed`) or falls back to a lazy computed; read via `$$readCell`.
|
|
@@ -77,6 +77,10 @@ export function generateBuild(
|
|
|
77
77
|
isLayout = false,
|
|
78
78
|
/* `linked` / async `computed` names, lowered to `$$readCell(name)` in template exprs. */
|
|
79
79
|
cellReadNames: ReadonlySet<string> = new Set(),
|
|
80
|
+
/* The subset that are BLOCKING `await` cells (ADR-0042), lowered to `$$readCellBlocking(name)`
|
|
81
|
+
on the CLIENT so a pending read suspends its render region. Server keeps `$$readCell` (the
|
|
82
|
+
SSR barrier resolves blocking cells before the template, so the read never suspends). */
|
|
83
|
+
blockingCellNames: ReadonlySet<string> = new Set(),
|
|
80
84
|
): string {
|
|
81
85
|
const nextVar = makeVarNamer()
|
|
82
86
|
/* A per-body source-order ordinal for each `<Child/>` mount site, so two same-type siblings
|
|
@@ -121,7 +125,7 @@ export function generateBuild(
|
|
|
121
125
|
withShadow,
|
|
122
126
|
bindRead,
|
|
123
127
|
bindWrite,
|
|
124
|
-
} = lowerContext(stateNames, derivedNames, computedNames, cellReadNames)
|
|
128
|
+
} = lowerContext(stateNames, derivedNames, computedNames, cellReadNames, blockingCellNames)
|
|
125
129
|
|
|
126
130
|
/* Maps a plan `Binding`'s classification to the client `ShadowKind`: a `reactive` value
|
|
127
131
|
derefs as a `.value` cell (`derived`), a `plain` value as the bare local (`plain`). The
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
import { isFunctionScopeBoundary } from './isFunctionScopeBoundary.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
True when `node` contains an `await` at its OWN top level — not nested inside any function scope
|
|
6
|
+
(`isFunctionScopeBoundary`: function/arrow/method/accessor/constructor). The SINGLE seed classifier
|
|
7
|
+
both the build lowering (`desugarSignals`: `await` present → a BLOCKING cell that suspends its render
|
|
8
|
+
region) and the type shadow (`compileShadow`: `$$cellValue` `T` vs `$$cellValuePending` `T | undefined`)
|
|
9
|
+
share, so the runtime blocking flag and the shadow type can never disagree on whether a seed is
|
|
10
|
+
`await`-marked (ADR-0042 D5's "one predicate"). An `await` in an inner callback / method
|
|
11
|
+
(`items.map(async (x) => await f(x))`, `{ async m() { await x } }`) does NOT count — only a
|
|
12
|
+
top-level `await` marks the seed itself.
|
|
13
|
+
*/
|
|
14
|
+
export function hasTopLevelAwait(node: ts.Node): boolean {
|
|
15
|
+
let found = false
|
|
16
|
+
const visit = (child: ts.Node): void => {
|
|
17
|
+
if (found || isFunctionScopeBoundary(child)) {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
if (ts.isAwaitExpression(child)) {
|
|
21
|
+
found = true
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
ts.forEachChild(child, visit)
|
|
25
|
+
}
|
|
26
|
+
visit(node)
|
|
27
|
+
return found
|
|
28
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { cachedShadowProgram } from './cachedShadowProgram.ts'
|
|
2
|
+
import type { ShadowProgram } from './createShadowProgram.ts'
|
|
2
3
|
import { shadowInterpolationClassifier } from './shadowInterpolationClassifier.ts'
|
|
3
4
|
import type { InterpolationClassifier } from './types/InterpolationClassifier.ts'
|
|
4
5
|
|
|
@@ -12,24 +13,18 @@ The incremental `createShadowLanguageService` does not expose the checker, shado
|
|
|
12
13
|
`ts.SourceFile`, or per-file mappings publicly, so v1 uses the one-shot
|
|
13
14
|
`createShadowProgram` (reused per root) rather than the LS overlay path.
|
|
14
15
|
|
|
15
|
-
FAIL-OPEN throughout: if the program can't be built
|
|
16
|
-
classifier is absent (→ today's
|
|
17
|
-
|
|
18
|
-
`'sync'`. A type-resolution
|
|
16
|
+
FAIL-OPEN throughout: if the program can't be built (warned once per root by
|
|
17
|
+
`cachedShadowProgram`), or the file has no shadow, the classifier is absent (→ today's
|
|
18
|
+
plain-value binding); and the returned closure wraps its whole body so ANY throw
|
|
19
|
+
(resolution failure, missing node, checker hiccup) returns `'sync'`. A type-resolution
|
|
20
|
+
problem degrades to today's behavior, never breaks the build.
|
|
19
21
|
*/
|
|
20
22
|
export function interpolationClassifierForRoot(
|
|
21
23
|
cache: Map<string, ShadowProgram | undefined>,
|
|
22
24
|
root: string,
|
|
23
25
|
abidePath: string,
|
|
24
26
|
): InterpolationClassifier | undefined {
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
cache.set(root, createShadowProgram(root))
|
|
28
|
-
} catch {
|
|
29
|
-
cache.set(root, undefined)
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
const shadowProgram = cache.get(root)
|
|
27
|
+
const shadowProgram = cachedShadowProgram(cache, root)
|
|
33
28
|
if (shadowProgram === undefined) {
|
|
34
29
|
return undefined
|
|
35
30
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
A node that opens its own (possibly async) function scope — awaits inside it are that scope's
|
|
5
|
+
concern, not the enclosing seed's. Both the top-level-await walk (`hasTopLevelAwait`) and the
|
|
6
|
+
top-level-await diagnostics stop descending here, so a seed classifies the same way a value read
|
|
7
|
+
inside such a scope would behave. One predicate shared by the build lowering and the type shadow so
|
|
8
|
+
they can never disagree on a boundary (ADR-0042 D5's "one predicate").
|
|
9
|
+
*/
|
|
10
|
+
export function isFunctionScopeBoundary(node: ts.Node): boolean {
|
|
11
|
+
return (
|
|
12
|
+
ts.isFunctionDeclaration(node) ||
|
|
13
|
+
ts.isFunctionExpression(node) ||
|
|
14
|
+
ts.isArrowFunction(node) ||
|
|
15
|
+
ts.isMethodDeclaration(node) ||
|
|
16
|
+
ts.isGetAccessorDeclaration(node) ||
|
|
17
|
+
ts.isSetAccessorDeclaration(node) ||
|
|
18
|
+
ts.isConstructorDeclaration(node)
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -101,8 +101,8 @@ export function liftAsyncSubExpressions(
|
|
|
101
101
|
/* A nested function is its own evaluation scope — an async (sub)expression inside a callback
|
|
102
102
|
(`items.map(x => fetchName(x))`, `items.map(async x => await load(x))`) must NOT be hoisted
|
|
103
103
|
to a top-level cell: its parameters/closure vars would become free identifiers, and it
|
|
104
|
-
would run once instead of per row. Stop at the boundary, mirroring
|
|
105
|
-
`
|
|
104
|
+
would run once instead of per row. Stop at the boundary, mirroring the shared
|
|
105
|
+
`hasTopLevelAwait` walk. */
|
|
106
106
|
if (
|
|
107
107
|
ts.isArrowFunction(node) ||
|
|
108
108
|
ts.isFunctionExpression(node) ||
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
|
|
3
|
+
/* Arithmetic compound assignments → their plain binary counterpart, lowered to a
|
|
4
|
+
read-combine-write (`x += y` → `write(read + y)`). */
|
|
5
|
+
const ARITHMETIC_COMPOUND_OPERATORS = new Map<ts.SyntaxKind, ts.BinaryOperator>([
|
|
6
|
+
[ts.SyntaxKind.PlusEqualsToken, ts.SyntaxKind.PlusToken],
|
|
7
|
+
[ts.SyntaxKind.MinusEqualsToken, ts.SyntaxKind.MinusToken],
|
|
8
|
+
[ts.SyntaxKind.AsteriskEqualsToken, ts.SyntaxKind.AsteriskToken],
|
|
9
|
+
[ts.SyntaxKind.SlashEqualsToken, ts.SyntaxKind.SlashToken],
|
|
10
|
+
])
|
|
11
|
+
|
|
12
|
+
/* Logical compound assignments SHORT-CIRCUIT: the write fires only when the guard
|
|
13
|
+
passes (`x ??= v` is `x ?? (x = v)`), so they lower to `read <op> write(right)`, NOT
|
|
14
|
+
an unconditional write — matching JS, so a non-nullish/truthy/falsy guard does no
|
|
15
|
+
needless patch or cell reseed. */
|
|
16
|
+
const LOGICAL_ASSIGNMENT_OPERATORS = new Map<ts.SyntaxKind, ts.BinaryOperator>([
|
|
17
|
+
[ts.SyntaxKind.QuestionQuestionEqualsToken, ts.SyntaxKind.QuestionQuestionToken],
|
|
18
|
+
[ts.SyntaxKind.BarBarEqualsToken, ts.SyntaxKind.BarBarToken],
|
|
19
|
+
[ts.SyntaxKind.AmpersandAmpersandEqualsToken, ts.SyntaxKind.AmpersandAmpersandToken],
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
Lowers an assignment (`=`, arithmetic `+=`, or logical `??=`/`||=`/`&&=`) to a reactive
|
|
24
|
+
write, shared by the `$$model` doc path (lowerDocAccess) and the `$$writeCell` linked-cell
|
|
25
|
+
path (renameSignalRefs) so the two lowerings can't drift. `makeRead` builds the target's
|
|
26
|
+
read expression (only called for compound operators); `makeWrite` wraps a value in the
|
|
27
|
+
write call. Returns undefined for an operator this doesn't handle so the caller can leave
|
|
28
|
+
the node untouched. The write must evaluate to the written value (see `Cell.set` /
|
|
29
|
+
`Doc.replace`) for the logical short-circuit to yield the right result.
|
|
30
|
+
*/
|
|
31
|
+
export function lowerCompoundAssignment(
|
|
32
|
+
operator: ts.SyntaxKind,
|
|
33
|
+
makeRead: () => ts.Expression,
|
|
34
|
+
right: ts.Expression,
|
|
35
|
+
makeWrite: (value: ts.Expression) => ts.Expression,
|
|
36
|
+
): ts.Expression | undefined {
|
|
37
|
+
if (operator === ts.SyntaxKind.EqualsToken) {
|
|
38
|
+
return makeWrite(right)
|
|
39
|
+
}
|
|
40
|
+
const arithmetic = ARITHMETIC_COMPOUND_OPERATORS.get(operator)
|
|
41
|
+
if (arithmetic !== undefined) {
|
|
42
|
+
return makeWrite(ts.factory.createBinaryExpression(makeRead(), arithmetic, right))
|
|
43
|
+
}
|
|
44
|
+
const logical = LOGICAL_ASSIGNMENT_OPERATORS.get(operator)
|
|
45
|
+
if (logical !== undefined) {
|
|
46
|
+
return ts.factory.createBinaryExpression(makeRead(), logical, makeWrite(right))
|
|
47
|
+
}
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
@@ -27,6 +27,9 @@ export function lowerContext(
|
|
|
27
27
|
computedNames: ReadonlySet<string> = new Set(),
|
|
28
28
|
/* `linked` / async `computed` names, read through `$$readCell(name)`. */
|
|
29
29
|
cellReadNames: ReadonlySet<string> = new Set(),
|
|
30
|
+
/* The subset that are BLOCKING `await` cells (ADR-0042), read through `$$readCellBlocking(name)`
|
|
31
|
+
(suspend-on-pending). Passed only by the CLIENT back-end; SSR leaves it empty. */
|
|
32
|
+
blockingCellNames: ReadonlySet<string> = new Set(),
|
|
30
33
|
) {
|
|
31
34
|
/* The typed branch-local shadow stack: one auto-popping value owning both kinds.
|
|
32
35
|
`derived` names deref to `.value` like a `computed` (block value params the client
|
|
@@ -51,6 +54,7 @@ export function lowerContext(
|
|
|
51
54
|
new Set(scope.names('derived')),
|
|
52
55
|
new Set(scope.names('plain')),
|
|
53
56
|
cellReadNames,
|
|
57
|
+
blockingCellNames,
|
|
54
58
|
),
|
|
55
59
|
docAccessTransformer('$$model'),
|
|
56
60
|
])
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import ts from 'typescript'
|
|
2
2
|
import { escapeKey } from '../runtime/escapeKey.ts'
|
|
3
|
-
import {
|
|
3
|
+
import { lowerCompoundAssignment } from './lowerCompoundAssignment.ts'
|
|
4
|
+
import { lowerUpdateExpression } from './lowerUpdateExpression.ts'
|
|
4
5
|
|
|
5
6
|
/*
|
|
6
7
|
The linchpin compiler pass. Rewrites idiomatic data access on a reactive document
|
|
@@ -91,33 +92,24 @@ export function docAccessTransformer(docName: string): ts.TransformerFactory<ts.
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
function visit(node: ts.Node): ts.Node {
|
|
94
|
-
/* Assignment (plain or
|
|
95
|
+
/* Assignment (plain, compound, or logical) to a doc path → replace patch. */
|
|
95
96
|
if (ts.isBinaryExpression(node)) {
|
|
96
97
|
const segments = pathSegments(node.left as ts.Expression)
|
|
97
98
|
if (segments) {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (binary) {
|
|
107
|
-
const next = ts.factory.createBinaryExpression(
|
|
108
|
-
docCall(docName, 'read', [buildPath(segments)]),
|
|
109
|
-
binary,
|
|
110
|
-
ts.visitNode(node.right, visit) as ts.Expression,
|
|
111
|
-
)
|
|
112
|
-
return docCall(docName, 'replace', [buildPath(segments), next])
|
|
99
|
+
const lowered = lowerCompoundAssignment(
|
|
100
|
+
node.operatorToken.kind,
|
|
101
|
+
() => docCall(docName, 'read', [buildPath(segments)]),
|
|
102
|
+
ts.visitNode(node.right, visit) as ts.Expression,
|
|
103
|
+
(value) => docCall(docName, 'replace', [buildPath(segments), value]),
|
|
104
|
+
)
|
|
105
|
+
if (lowered !== undefined) {
|
|
106
|
+
return lowered
|
|
113
107
|
}
|
|
114
108
|
}
|
|
115
109
|
}
|
|
116
|
-
/* `path++` / `++path` / `path--` on a doc path → a replace patch
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
an lvalue. abide handlers are statements, so the postfix/prefix value is
|
|
120
|
-
discarded and both lower identically. Only `++`/`--` are handled here; other
|
|
110
|
+
/* `path++` / `++path` / `path--` on a doc path → a replace patch. A bare `++`
|
|
111
|
+
would otherwise survive onto the lowered read (`model.read("n")++`) — invalid,
|
|
112
|
+
since a call result is not an lvalue. Only `++`/`--` are handled here; other
|
|
121
113
|
unary operators (`!path`, `-path`) fall through so their operand lowers as a
|
|
122
114
|
normal read. */
|
|
123
115
|
if (
|
|
@@ -127,16 +119,12 @@ export function docAccessTransformer(docName: string): ts.TransformerFactory<ts.
|
|
|
127
119
|
) {
|
|
128
120
|
const segments = pathSegments(node.operand)
|
|
129
121
|
if (segments) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
docCall(docName, 'read', [buildPath(segments)]),
|
|
136
|
-
step,
|
|
137
|
-
ts.factory.createNumericLiteral(1),
|
|
122
|
+
return lowerUpdateExpression(
|
|
123
|
+
ts.isPostfixUnaryExpression(node),
|
|
124
|
+
node.operator === ts.SyntaxKind.PlusPlusToken,
|
|
125
|
+
() => docCall(docName, 'read', [buildPath(segments)]),
|
|
126
|
+
(value) => docCall(docName, 'replace', [buildPath(segments), value]),
|
|
138
127
|
)
|
|
139
|
-
return docCall(docName, 'replace', [buildPath(segments), next])
|
|
140
128
|
}
|
|
141
129
|
}
|
|
142
130
|
/* doc array `.push(a, b, …)` → one `add` patch per argument at the array's `-`
|