@abide/abide 0.53.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.
Files changed (56) hide show
  1. package/AGENTS.md +9 -5
  2. package/CHANGELOG.md +57 -0
  3. package/README.md +1 -1
  4. package/package.json +2 -2
  5. package/src/checkAbide.ts +1 -1
  6. package/src/lib/server/runtime/amendBroadcaster.ts +53 -0
  7. package/src/lib/server/runtime/createUiPageRenderer.ts +26 -0
  8. package/src/lib/server/runtime/runWithRequestScope.ts +1 -0
  9. package/src/lib/server/runtime/types/RequestStore.ts +9 -0
  10. package/src/lib/server/sockets/amendFamilyEntry.ts +39 -0
  11. package/src/lib/server/sockets/createSocketDispatcher.ts +9 -0
  12. package/src/lib/server/sockets/defineSocket.ts +11 -1
  13. package/src/lib/shared/AMEND_TOPIC_PREFIX.ts +9 -0
  14. package/src/lib/shared/SOCKET_SEED.ts +13 -0
  15. package/src/lib/shared/amend.ts +48 -0
  16. package/src/lib/shared/amendBroadcastSlot.ts +19 -0
  17. package/src/lib/shared/applyAmendLocally.ts +20 -0
  18. package/src/lib/shared/attachRpcSelectorMethods.ts +5 -5
  19. package/src/lib/shared/buildSocketOverChannel.ts +9 -2
  20. package/src/lib/shared/cache.ts +35 -10
  21. package/src/lib/shared/cacheReaderSocketSlot.ts +11 -0
  22. package/src/lib/shared/createCacheStore.ts +8 -0
  23. package/src/lib/shared/peek.ts +1 -1
  24. package/src/lib/shared/socketTailsSlot.ts +13 -0
  25. package/src/lib/shared/types/AmendApply.ts +15 -0
  26. package/src/lib/shared/types/CacheReaderHook.ts +11 -0
  27. package/src/lib/shared/types/RemoteFunction.ts +30 -19
  28. package/src/lib/shared/types/SocketTails.ts +13 -0
  29. package/src/lib/shared/types/SsrPayload.ts +6 -0
  30. package/src/lib/test/createTestApp.ts +6 -0
  31. package/src/lib/ui/amendReaderHook.ts +125 -0
  32. package/src/lib/ui/compile/cachedShadowProgram.ts +38 -0
  33. package/src/lib/ui/compile/compileComponent.ts +9 -2
  34. package/src/lib/ui/compile/desugarSignals.ts +68 -3
  35. package/src/lib/ui/compile/interpolationClassifierForRoot.ts +8 -13
  36. package/src/lib/ui/compile/lowerCompoundAssignment.ts +49 -0
  37. package/src/lib/ui/compile/lowerDocAccess.ts +19 -31
  38. package/src/lib/ui/compile/lowerUpdateExpression.ts +32 -0
  39. package/src/lib/ui/compile/renameSignalRefs.ts +18 -24
  40. package/src/lib/ui/compile/seedTypeClassifierForRoot.ts +9 -14
  41. package/src/lib/ui/compile/wrapReactionCellSources.ts +10 -6
  42. package/src/lib/ui/dom/awaitBlock.ts +1 -1
  43. package/src/lib/ui/dom/tryBlock.ts +1 -1
  44. package/src/lib/ui/router.ts +8 -0
  45. package/src/lib/ui/runtime/createDoc.ts +5 -1
  46. package/src/lib/ui/runtime/restoreWarmSeeds.ts +20 -0
  47. package/src/lib/ui/runtime/types/Cell.ts +4 -2
  48. package/src/lib/ui/runtime/types/Doc.ts +3 -1
  49. package/src/lib/ui/runtime/warmSeedBackup.ts +10 -0
  50. package/src/lib/ui/socketProxy.ts +17 -1
  51. package/src/lib/ui/startClient.ts +30 -0
  52. package/src/lib/ui/types/Scope.ts +1 -1
  53. package/src/lib/ui/watch.ts +18 -5
  54. package/src/serverEntry.ts +15 -0
  55. package/src/lib/shared/patch.ts +0 -41
  56. package/src/lib/ui/compile/COMPOUND_ASSIGNMENT_OPERATORS.ts +0 -19
@@ -8,6 +8,7 @@ import { effect } from './effect.ts'
8
8
  import { generationGuard } from './runtime/generationGuard.ts'
9
9
  import type { EffectResult } from './runtime/types/EffectResult.ts'
10
10
  import type { State } from './runtime/types/State.ts'
11
+ import { untrack } from './runtime/untrack.ts'
11
12
 
12
13
  /*
13
14
  The single reaction primitive: `watch(source, handler)` names its trigger and runs
@@ -25,6 +26,14 @@ into a server bundle; server-side it is an inert no-op. Unlike bare `watch(…)`
25
26
  the SSR back-end strips — a `.watch(…)` member call survives to the server and relies on
26
27
  that inert stub.
27
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
+
28
37
  Sources (discriminated at runtime, monomorphic per branch):
29
38
  watch(thunk) // compiler binding form — auto-tracked, == effect(thunk)
30
39
  watch(count, n => …) // a state cell → handler(newValue)
@@ -93,18 +102,22 @@ export function watch(
93
102
  ) {
94
103
  return reactToRpc(source, undefined, handler)
95
104
  }
96
- /* 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). */
97
108
  if (Array.isArray(source)) {
98
109
  const cells = source as ReadonlyArray<State<unknown>>
99
110
  return effect(() => {
100
111
  const values = cells.map((cell) => cell.value)
101
- handler(values)
112
+ untrack(() => handler(values))
102
113
  })
103
114
  }
104
- /* 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). */
105
117
  const cell = source as State<unknown>
106
118
  return effect(() => {
107
- handler(cell.value)
119
+ const value = cell.value
120
+ untrack(() => handler(value))
108
121
  })
109
122
  }
110
123
 
@@ -120,7 +133,7 @@ function isSubscribable(source: unknown): boolean {
120
133
  /*
121
134
  Reacts to an rpc's cached value: an effect that runs the smart read (subscribing
122
135
  its key synchronously, so the effect re-runs when the value changes — a refresh,
123
- a patch, an invalidate) and pipes the resolved value to the handler. Triggers the
136
+ an amend, an invalidate) and pipes the resolved value to the handler. Triggers the
124
137
  read, unlike peek: `watch` observes a live query, so it keeps it flowing.
125
138
  */
126
139
  function reactToRpc(fn: unknown, args: unknown, handler: (value: unknown) => void): () => void {
@@ -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
 
@@ -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
- ])