@playfast/reform 0.0.10 → 0.0.11

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 (45) hide show
  1. package/package.json +1 -1
  2. package/src/boundary/boundary.ts +16 -7
  3. package/src/calc/asyncCalc.ts +84 -37
  4. package/src/calc/asyncData.ts +8 -4
  5. package/src/calc/calc.ts +7 -3
  6. package/src/calc/calcFamily.ts +24 -10
  7. package/src/calc/compose.ts +1 -1
  8. package/src/calc/queryState.ts +8 -8
  9. package/src/channel/channel.ts +63 -37
  10. package/src/compose/composition.ts +20 -1
  11. package/src/compose/provide.ts +5 -1
  12. package/src/compose/slot.ts +4 -2
  13. package/src/compose/structure.ts +43 -19
  14. package/src/compose/ui.ts +21 -2
  15. package/src/compose/ui.typecheck.ts +4 -4
  16. package/src/definition/definition.ts +20 -7
  17. package/src/feature/feature.test.ts +4 -4
  18. package/src/feature/feature.ts +30 -16
  19. package/src/feature/feature.typecheck.ts +2 -2
  20. package/src/internal/capture.ts +1 -0
  21. package/src/internal/errors.ts +9 -4
  22. package/src/internal/inspect.ts +4 -4
  23. package/src/internal/queryDriver.ts +102 -53
  24. package/src/internal/reuse.ts +67 -30
  25. package/src/internal/scheduler.ts +35 -23
  26. package/src/internal/sources.ts +14 -8
  27. package/src/internal/stateRegistry.ts +3 -1
  28. package/src/internal/store.ts +10 -8
  29. package/src/internal/track.ts +3 -1
  30. package/src/procedure/procedure.ts +2 -2
  31. package/src/reducer/reducer.ts +17 -9
  32. package/src/remote/remoteState.test.ts +188 -1
  33. package/src/remote/remoteState.ts +112 -51
  34. package/src/remote/remoteState.typecheck.ts +4 -1
  35. package/src/runtime/bus.ts +3 -1
  36. package/src/runtime/hardening.test.ts +1 -1
  37. package/src/runtime/loop.ts +67 -46
  38. package/src/runtime/queries.ts +3 -1
  39. package/src/scene/scene.ts +16 -8
  40. package/src/state/state.ts +11 -10
  41. package/src/state/stateFamily.ts +27 -11
  42. package/src/state/stateGroup.ts +22 -9
  43. package/src/synced/syncedStore.ts +15 -9
  44. package/src/wire/tree.ts +66 -30
  45. package/src/wire/triggers.ts +9 -8
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@playfast/reform",
3
3
  "playbook": "./playbook",
4
- "version": "0.0.10",
4
+ "version": "0.0.11",
5
5
  "type": "module",
6
6
  "description": "The renderer-neutral core of the reform framework — typed, headless state, events, reducers, derived values, async/remote data, and compositions built on Effect.",
7
7
  "keywords": [
@@ -102,6 +102,7 @@ export interface BoundaryOptions {
102
102
  * loading state. Without the latch the boundary re-pends on any covered
103
103
  * first load, which is what a per-screen boundary wants.
104
104
  */
105
+ // oxlint-disable-next-line reform-rules/no-optional-fields -- public options-bag input; omission is the documented default (no latch) and external callers pass a plain `{ once: true }` literal
105
106
  readonly once?: boolean
106
107
  }
107
108
 
@@ -153,19 +154,23 @@ export const live = <N extends string, Over extends ReadonlyArray<AnyLifecycleSo
153
154
  { readonly key: ReadonlyArray<unknown>; readonly value: BoundaryState } | undefined
154
155
  >(undefined)
155
156
  const recompute = (): BoundaryState => {
156
- if (options.once === true && MutableRef.get(latched)) return readyArm
157
+ if (options.once === true && MutableRef.get(latched)) {
158
+ return readyArm
159
+ }
157
160
  const arms = sources.keyOf(sources.snapshot())
158
161
  const prev = MutableRef.get(memo)
159
- if (prev !== undefined && sameKey(arms, prev.key)) return prev.value
162
+ if (prev !== undefined && sameKey(arms, prev.key)) {
163
+ return prev.value
164
+ }
160
165
  const errors = arms.flatMap(errorOf)
161
- const value =
166
+ const mergedArm =
162
167
  errors.length > 0
163
168
  ? erroredArm(errors)
164
169
  : arms.some((arm) => tagOf(arm) === 'Loading')
165
170
  ? pendingArm
166
171
  : readyArm
167
- MutableRef.set(memo, { key: arms, value })
168
- return value
172
+ MutableRef.set(memo, { key: arms, value: mergedArm })
173
+ return mergedArm
169
174
  }
170
175
 
171
176
  const derived = makeDerivedStore(recompute, sources.subscribe, scheduler)
@@ -181,9 +186,13 @@ export const live = <N extends string, Over extends ReadonlyArray<AnyLifecycleSo
181
186
  // Ready self-corrects within the flush for readers, but a latch taken
182
187
  // there would freeze it; a subscriber runs only after the fixpoint,
183
188
  // where the value is converged.
184
- if (recompute()._tag === 'Ready') MutableRef.set(latched, true)
189
+ if (recompute()._tag === 'Ready') {
190
+ MutableRef.set(latched, true)
191
+ }
185
192
  const offSelf = derived.store.subscribe(() => {
186
- if (derived.store.get()._tag === 'Ready') MutableRef.set(latched, true)
193
+ if (derived.store.get()._tag === 'Ready') {
194
+ MutableRef.set(latched, true)
195
+ }
187
196
  })
188
197
  yield* Effect.addFinalizer(() => Effect.sync(offSelf))
189
198
  }
@@ -1,4 +1,4 @@
1
- import { Context, Effect, Layer, Option, Schema } from 'effect'
1
+ import { Context, Effect, Layer, Match, Option, Schema } from 'effect'
2
2
  import { type Manifest, yieldableClass } from '../definition/definition'
3
3
  import { type AnyEvent } from '../event/event'
4
4
  import {
@@ -35,6 +35,7 @@ export interface AsyncCalcManifest<N extends string, A, E> extends Manifest {
35
35
  readonly kind: 'AsyncCalc'
36
36
  readonly name: N
37
37
  readonly output: Schema.Schema<A, any>
38
+ // oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
38
39
  readonly error?: Schema.Schema<E, any>
39
40
  /** Whether the query can be disabled (drives the `Idle` arm). */
40
41
  readonly gated: boolean
@@ -63,8 +64,10 @@ export interface AsyncCalcConfig<Inputs extends ReadonlyArray<AnySource>, A, E,
63
64
  /** Schema of the `Success` value. */
64
65
  readonly output: Schema.Schema<A, any>
65
66
  /** Schema of the failure. Omitted ⇒ the query is infallible and there is no `Error` arm. */
67
+ // oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
66
68
  readonly error?: Schema.Schema<E, any>
67
69
  /** `true` ⇒ the query is always on: no `Idle` arm and `disabled` is rejected on `.live`. */
70
+ // oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
68
71
  readonly alwaysOn?: AlwaysOn
69
72
  }
70
73
 
@@ -81,6 +84,7 @@ export type AsyncCalcLive<
81
84
  R,
82
85
  > = {
83
86
  readonly query: (inputs: InputsObject<Inputs>) => Effect.Effect<A, E, R>
87
+ // oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
84
88
  readonly invalidateBy?: InvalidateBy<Inputs>
85
89
  /**
86
90
  * Refetch conciliation. `'switch'` (default): a new key cancels the in-flight
@@ -89,6 +93,7 @@ export type AsyncCalcLive<
89
93
  * refetch runs after it settles — a burst of N invalidations during one
90
94
  * flight costs 2 fetches, not N cancel-restarts.
91
95
  */
96
+ // oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
92
97
  readonly coalesce?: 'switch' | 'trailing'
93
98
  /**
94
99
  * Structural sharing for `Success` values: reconcile each refetch result
@@ -97,19 +102,58 @@ export type AsyncCalcLive<
97
102
  * subtree identities, so downstream memo boundaries skip them. Same pass as
98
103
  * `Calc`'s `reuse`; opt-in (one O(result) walk per settle).
99
104
  */
105
+ // oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
100
106
  readonly reuse?: boolean
101
107
  /**
102
108
  * Persist the `Success` value through an optional host `QueryStore` (e.g.
103
109
  * `@playfast/reform-query-browser`'s localStorage layer): hydrate it on build
104
110
  * so the value shows instantly (marked stale, so it still refetches) and write
105
111
  * through on each settle. `true` keys it by the calc name; pass `{ key }` to
106
- * override. Values (de)serialize through the calc's `output` schema. No host
112
+ * override a static string for a singleton, or a function of the inputs to
113
+ * key a family per-entity (so reactive-input keyed reads never collide on one
114
+ * slot). Values (de)serialize through the calc's `output` schema. No host
107
115
  * `QueryStore` in context ⇒ inert (no hard requirement added).
108
116
  */
109
- readonly persist?: boolean | { readonly key?: string }
117
+ // oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
118
+ readonly persist?:
119
+ | boolean
120
+ // oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
121
+ | { readonly key?: string | ((inputs: InputsObject<Inputs>) => string) }
110
122
  } & (Gated extends true
111
- ? { readonly disabled?: (inputs: InputsObject<Inputs>) => boolean }
112
- : { readonly disabled?: never })
123
+ ? // oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
124
+ { readonly disabled?: (inputs: InputsObject<Inputs>) => boolean }
125
+ : // oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
126
+ { readonly disabled?: never })
127
+
128
+ /** `.live` config plus the optional `invalidateOn` event list (impl signature). */
129
+ export type AsyncCalcLiveWithInvalidateOn<
130
+ Inputs extends ReadonlyArray<AnySource>,
131
+ A,
132
+ E,
133
+ Gated extends boolean,
134
+ R,
135
+ > = AsyncCalcLive<Inputs, A, E, Gated, R> & {
136
+ // oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
137
+ readonly invalidateOn?: ReadonlyArray<AnyEvent>
138
+ }
139
+
140
+ /** Loose runtime view of `.live` config — `disabled` recovered past its type erasure. */
141
+ export interface AsyncCalcLiveView<Inputs extends ReadonlyArray<AnySource>, A, E, R> {
142
+ readonly query: (inputs: InputsObject<Inputs>) => Effect.Effect<A, E, R>
143
+ // oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
144
+ readonly invalidateBy?: InvalidateBy<Inputs>
145
+ // oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
146
+ readonly disabled?: (inputs: InputsObject<Inputs>) => boolean
147
+ // oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
148
+ readonly coalesce?: 'switch' | 'trailing'
149
+ // oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
150
+ readonly reuse?: boolean
151
+ // oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
152
+ readonly persist?:
153
+ | boolean
154
+ // oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
155
+ | { readonly key?: string | ((inputs: InputsObject<Inputs>) => string) }
156
+ }
113
157
 
114
158
  /**
115
159
  * Define an async derived value. `output`/`error` schemas and `alwaysOn` shape
@@ -195,9 +239,7 @@ export function live<
195
239
  R,
196
240
  >(
197
241
  calc: AsyncCalcClass<N, Inputs, A, E, Gated>,
198
- config: AsyncCalcLive<Inputs, A, E, Gated, R> & {
199
- readonly invalidateOn?: ReadonlyArray<AnyEvent>
200
- },
242
+ config: AsyncCalcLiveWithInvalidateOn<Inputs, A, E, Gated, R>,
201
243
  // The wide-R implementation signature behind precise overloads — the same
202
244
  // seam `Reducer.live` uses; both overload returns are assignable (RIn is
203
245
  // covariant), so no value is ever cast.
@@ -218,25 +260,22 @@ export function live<
218
260
  Effect.gen(function* () {
219
261
  // `disabled` is rejected at the type level for non-gated calcs (erased to
220
262
  // `never` there); read it through a loose view for the runtime.
221
- const cfg = config as {
222
- readonly query: (inputs: InputsObject<Inputs>) => Effect.Effect<A, E, R>
223
- readonly invalidateBy?: InvalidateBy<Inputs>
224
- readonly disabled?: (inputs: InputsObject<Inputs>) => boolean
225
- readonly coalesce?: 'switch' | 'trailing'
226
- readonly reuse?: boolean
227
- readonly persist?: boolean | { readonly key?: string }
228
- }
263
+ // oxlint-disable-next-line reform-rules/no-type-assertion -- loose runtime view: `disabled` is type-erased to `never` for non-gated calcs, no guard recovers it
264
+ const cfg = config as AsyncCalcLiveView<Inputs, A, E, R>
229
265
 
230
- // `persist: true` keys by the calc name; `{ key }` overrides. The driver
266
+ // `persist: true` keys by the calc name; `{ key }` overrides a static
267
+ // string for a singleton, or a function of the inputs for a keyed family
268
+ // (the driver resolves the function per-input at hydrate/write). The driver
231
269
  // (de)serializes through the calc's own `output` schema.
270
+ const persistOption = cfg.persist
271
+ const persistKey =
272
+ persistOption === true || persistOption === undefined || persistOption === false
273
+ ? calc.manifest.name
274
+ : persistOption.key ?? calc.manifest.name
232
275
  const persist =
233
- cfg.persist === undefined || cfg.persist === false
276
+ persistOption === undefined || persistOption === false
234
277
  ? undefined
235
- : {
236
- key:
237
- cfg.persist === true ? calc.manifest.name : cfg.persist.key ?? calc.manifest.name,
238
- schema: calc.manifest.output,
239
- }
278
+ : { key: persistKey, schema: calc.manifest.output }
240
279
 
241
280
  // The hidden revision store, read requirement-free (`serviceOption`): the
242
281
  // assembly below always provides it alongside this driver, and feeding it
@@ -244,10 +283,17 @@ export function live<
244
283
  // the user's `query` and `invalidateBy` receive exactly
245
284
  // `InputsObject<Inputs>`, with no hidden property to leak into
246
285
  // spread-into-RPC payloads.
247
- const revision =
248
- revisionState === undefined
249
- ? undefined
250
- : Option.getOrUndefined(yield* Effect.serviceOption(revisionState.store))
286
+ const revision = yield* Match.value(revisionState).pipe(
287
+ Match.when(undefined, () => Effect.succeed(undefined)),
288
+ Match.orElse((state) =>
289
+ Effect.serviceOption(state.store).pipe(Effect.map(Option.getOrUndefined)),
290
+ ),
291
+ )
292
+ const extraKeyFor = (rev: NonNullable<typeof revision>) => ({
293
+ read: () => rev.getSnapshot(),
294
+ subscribe: (listener: () => void) => rev.subscribe(listener),
295
+ })
296
+ const extraKey = revision === undefined ? undefined : extraKeyFor(revision)
251
297
  const driver = yield* makeQueryDriver({
252
298
  name: calc.manifest.name,
253
299
  label: 'AsyncCalc',
@@ -259,13 +305,7 @@ export function live<
259
305
  coalesce: cfg.coalesce,
260
306
  reuse: cfg.reuse,
261
307
  persist,
262
- extraKey:
263
- revision === undefined
264
- ? undefined
265
- : {
266
- read: () => revision.getSnapshot(),
267
- subscribe: (listener) => revision.subscribe(listener),
268
- },
308
+ extraKey,
269
309
  })
270
310
 
271
311
  // The internal store is the full union; narrow to the definition's arms.
@@ -273,7 +313,9 @@ export function live<
273
313
  }),
274
314
  )
275
315
 
276
- if (config.invalidateOn === undefined || revisionState === undefined) return driver
316
+ if (config.invalidateOn === undefined || revisionState === undefined) {
317
+ return driver
318
+ }
277
319
  // The hidden reducer: an ordinary `Reducer` folding every listed event to
278
320
  // n + 1 — the revision's sole writer, registered/unregistered with this
279
321
  // layer's scope like any user reducer. `Layer.provide` builds the hidden
@@ -296,7 +338,12 @@ export function live<
296
338
  * focus/online provider layer calls. A no-op if the calc isn't live; resolves the
297
339
  * `Queries` registry (part of `Engine`).
298
340
  */
299
- export const invalidate = (calc: { readonly name: string }): Effect.Effect<void, never, Queries> =>
341
+ /** The minimal calc shape `invalidate`/`refetch` need: just its registry name. */
342
+ export interface NamedCalc {
343
+ readonly name: string
344
+ }
345
+
346
+ export const invalidate = (calc: NamedCalc): Effect.Effect<void, never, Queries> =>
300
347
  Effect.flatMap(Queries, (queries) =>
301
348
  Effect.sync(() => queries.byName.get(calc.name)?.invalidate()),
302
349
  )
@@ -305,7 +352,7 @@ export const invalidate = (calc: { readonly name: string }): Effect.Effect<void,
305
352
  * Force an immediate refetch of the current key, bypassing the no-op-key guard
306
353
  * and independent of `isStale`. A no-op if the calc isn't live or is disabled.
307
354
  */
308
- export const refetch = (calc: { readonly name: string }): Effect.Effect<void, never, Queries> =>
355
+ export const refetch = (calc: NamedCalc): Effect.Effect<void, never, Queries> =>
309
356
  Effect.flatMap(Queries, (queries) =>
310
357
  Effect.sync(() => queries.byName.get(calc.name)?.refetch()),
311
358
  )
@@ -49,9 +49,9 @@ export type AnyAsyncData<A, E> = AsyncIdle | AsyncLoading | AsyncSuccess<A> | As
49
49
 
50
50
  const idle: AsyncIdle = { _tag: 'Idle' }
51
51
  const loading: AsyncLoading = { _tag: 'Loading' }
52
- const success = <A>(value: A, refetching = false): AsyncSuccess<A> => ({
52
+ const success = <A>(payload: A, refetching = false): AsyncSuccess<A> => ({
53
53
  _tag: 'Success',
54
- value,
54
+ value: payload,
55
55
  refetching,
56
56
  })
57
57
  const error = <E>(err: E, refetching = false): AsyncError<E> => ({
@@ -82,7 +82,9 @@ export const AsyncData: AsyncDataConstructors = { idle, loading, success, error
82
82
  */
83
83
  export const narrowStore = <A, E, Gated extends boolean>(
84
84
  store: Store<AnyAsyncData<A, E>>,
85
- ): Store<AsyncData<A, E, Gated>> => store as unknown as Store<AsyncData<A, E, Gated>>
85
+ ): Store<AsyncData<A, E, Gated>> =>
86
+ // oxlint-disable-next-line reform-rules/no-type-assertion -- type-level narrow of the SAME runtime store to the definition's permitted arms; the single documented home for this seam
87
+ store as unknown as Store<AsyncData<A, E, Gated>>
86
88
 
87
89
  /**
88
90
  * The read-only inverse of `narrowStore`: widen a definition-narrowed store back
@@ -93,4 +95,6 @@ export const narrowStore = <A, E, Gated extends boolean>(
93
95
  */
94
96
  export const widenStore = <A, E, Gated extends boolean>(
95
97
  store: Store<AsyncData<A, E, Gated>>,
96
- ): Store<AnyAsyncData<A, E>> => store as unknown as Store<AnyAsyncData<A, E>>
98
+ ): Store<AnyAsyncData<A, E>> =>
99
+ // oxlint-disable-next-line reform-rules/no-type-assertion -- read-only widen of the SAME runtime store back to the full union; sound for reads, documented inverse of narrowStore
100
+ store as unknown as Store<AnyAsyncData<A, E>>
package/src/calc/calc.ts CHANGED
@@ -17,7 +17,9 @@ import { type AnySource } from '../state/token'
17
17
  // even though the reactive plumbing now lives in `internal/sources`.
18
18
  export { type InputsObject, type InputStores, type InvalidateBy } from '../internal/sources'
19
19
 
20
- export interface CalcOptions<Inputs extends ReadonlyArray<AnySource>> {
20
+ // Boundary config object: optional fields are the framework's public, JSON-like
21
+ // definition surface (`ExternalApi` postfix exempts them from `no-optional-fields`).
22
+ export interface CalcOptionsExternalApi<Inputs extends ReadonlyArray<AnySource>> {
21
23
  /**
22
24
  * Custom invalidation: recompute only when the projected key changes by value
23
25
  * equality (React-Query's `queryKey`). Omitted ⇒ recompute when any input
@@ -83,7 +85,7 @@ export const make = <const N extends string, const Inputs extends ReadonlyArray<
83
85
  export const live = <N extends string, Inputs extends ReadonlyArray<AnySource>, Out>(
84
86
  calc: CalcClass<N, Inputs, Out>,
85
87
  compute: (inputs: InputsObject<Inputs>) => Out,
86
- options: CalcOptions<Inputs> = {},
88
+ options: CalcOptionsExternalApi<Inputs> = {},
87
89
  ): Layer.Layer<Store<Out>, never, InputStores<Inputs>> =>
88
90
  // Scoped so the source subscriptions are released when the layer's scope
89
91
  // closes (each proof/test builds and disposes its own runtime).
@@ -102,7 +104,9 @@ export const live = <N extends string, Inputs extends ReadonlyArray<AnySource>,
102
104
  const args = sources.snapshot()
103
105
  const key = sources.keyOf(args)
104
106
  const prev = MutableRef.get(memo)
105
- if (prev !== undefined && sameKey(key, prev.key)) return prev.output
107
+ if (prev !== undefined && sameKey(key, prev.key)) {
108
+ return prev.output
109
+ }
106
110
  const fresh = compute(args)
107
111
  // With `reuse`, a recompute that lands value-equal to the previous
108
112
  // output returns the previous reference — the derived store's Equal
@@ -84,9 +84,11 @@ export const read = <N extends string, Inputs extends ReadonlyArray<AnySource>,
84
84
  family: CalcFamilyClass<N, Inputs, K, Out>,
85
85
  key: K,
86
86
  ): Effect.Effect<Out, never, FamilyStore<K, Out>> =>
87
- Effect.flatMap(family.store, (fs) => readTracked(fs.at(key)))
87
+ Effect.flatMap(family.store, (familyStore) => readTracked(familyStore.at(key)))
88
88
 
89
- export interface CalcFamilyLiveOptions<Inputs extends ReadonlyArray<AnySource>>
89
+ // Boundary config object: optional fields are the framework's public, JSON-like
90
+ // definition surface (`ExternalApi` postfix exempts them from `no-optional-fields`).
91
+ export interface CalcFamilyLiveOptionsExternalApi<Inputs extends ReadonlyArray<AnySource>>
90
92
  extends FamilyOptions {
91
93
  /**
92
94
  * Family-wide invalidation key over the SHARED inputs (the member key plays
@@ -107,7 +109,7 @@ export interface CalcFamilyLiveOptions<Inputs extends ReadonlyArray<AnySource>>
107
109
  export const live = <N extends string, Inputs extends ReadonlyArray<AnySource>, K, Out>(
108
110
  family: CalcFamilyClass<N, Inputs, K, Out>,
109
111
  compute: (key: K) => (inputs: InputsObject<Inputs>) => Out,
110
- options: CalcFamilyLiveOptions<Inputs> = {},
112
+ options: CalcFamilyLiveOptionsExternalApi<Inputs> = {},
111
113
  ): Layer.Layer<FamilyStore<K, Out>, never, InputStores<Inputs>> =>
112
114
  Layer.scoped(
113
115
  family.store,
@@ -122,7 +124,9 @@ export const live = <N extends string, Inputs extends ReadonlyArray<AnySource>,
122
124
 
123
125
  const dropEntry = (key: K): void => {
124
126
  const entry = entries.get(key)
125
- if (entry !== undefined) entry.unsubscribe()
127
+ if (entry !== undefined) {
128
+ entry.unsubscribe()
129
+ }
126
130
  entries.delete(key)
127
131
  subscribers.delete(key)
128
132
  }
@@ -137,7 +141,9 @@ export const live = <N extends string, Inputs extends ReadonlyArray<AnySource>,
137
141
  const args = sources.snapshot()
138
142
  const memoKey = sources.keyOf(args)
139
143
  const prev = MutableRef.get(memo)
140
- if (prev !== undefined && sameKey(memoKey, prev.key)) return prev.output
144
+ if (prev !== undefined && sameKey(memoKey, prev.key)) {
145
+ return prev.output
146
+ }
141
147
  const output = body(args)
142
148
  MutableRef.set(memo, { key: memoKey, output })
143
149
  return output
@@ -151,11 +157,14 @@ export const live = <N extends string, Inputs extends ReadonlyArray<AnySource>,
151
157
  const refCounted = (key: K, store: Store<Out>): Store<Out> => ({
152
158
  ...store,
153
159
  subscribe: (listener) => {
154
- subscribers.set(key, (subscribers.get(key) ?? 0) + 1)
160
+ const priorCount = subscribers.get(key)
161
+ subscribers.set(key, (priorCount === undefined ? 0 : priorCount) + 1)
155
162
  const off = store.subscribe(listener)
156
163
  const released = { done: false }
157
164
  return () => {
158
- if (released.done) return
165
+ if (released.done) {
166
+ return
167
+ }
159
168
  released.done = true
160
169
  off()
161
170
  const remaining = (subscribers.get(key) ?? 1) - 1
@@ -165,7 +174,10 @@ export const live = <N extends string, Inputs extends ReadonlyArray<AnySource>,
165
174
  }
166
175
  subscribers.delete(key)
167
176
  queueMicrotask(() => {
168
- if ((subscribers.get(key) ?? 0) === 0) dropEntry(key)
177
+ const liveCount = subscribers.get(key)
178
+ if (liveCount === undefined || liveCount === 0) {
179
+ dropEntry(key)
180
+ }
169
181
  })
170
182
  }
171
183
  },
@@ -174,7 +186,9 @@ export const live = <N extends string, Inputs extends ReadonlyArray<AnySource>,
174
186
  const familyStore: FamilyStore<K, Out> = {
175
187
  at: (key) => {
176
188
  const existing = entries.get(key)
177
- if (existing !== undefined) return existing.store
189
+ if (existing !== undefined) {
190
+ return existing.store
191
+ }
178
192
  const member = createMember(key)
179
193
  const created = evictWhenUnused
180
194
  ? { store: refCounted(key, member.store), unsubscribe: member.unsubscribe }
@@ -184,7 +198,7 @@ export const live = <N extends string, Inputs extends ReadonlyArray<AnySource>,
184
198
  },
185
199
  forget: dropEntry,
186
200
  clear: () => {
187
- for (const key of [...entries.keys()]) dropEntry(key)
201
+ ;[...entries.keys()].forEach((key) => dropEntry(key))
188
202
  },
189
203
  size: () => entries.size,
190
204
  }
@@ -63,7 +63,7 @@ export function composeCalcs(
63
63
  // Fold leaf-first: each upstream is provided into the accumulated downstream,
64
64
  // so dependency order is exactly the argument order.
65
65
  return upstream.reduce<Layer.Layer<unknown, unknown, unknown>>(
66
- (downstream, up) => Layer.provideMerge(downstream, up),
66
+ (downstream, upstreamLayer) => Layer.provideMerge(downstream, upstreamLayer),
67
67
  leaf,
68
68
  )
69
69
  }
@@ -37,16 +37,16 @@ export const empty = (fetching: boolean): QueryState<never, never> => ({
37
37
  * non-gated query, which always fetches) and `Idle` only for a switched-off
38
38
  * gateable query.
39
39
  */
40
- export const toAsyncData = <A, E>(s: QueryState<A, E>, gated: boolean): AnyAsyncData<A, E> =>
41
- Option.match(s.error, {
42
- onSome: (error) => AsyncData.error(error, s.isFetching),
40
+ export const toAsyncData = <A, E>(state: QueryState<A, E>, gated: boolean): AnyAsyncData<A, E> =>
41
+ Option.match(state.error, {
42
+ onSome: (error) => AsyncData.error(error, state.isFetching),
43
43
  onNone: () =>
44
- Option.match(s.data, {
45
- onSome: (value) => AsyncData.success(value, s.isFetching),
46
- onNone: () => (s.isFetching || !gated ? AsyncData.loading : AsyncData.idle),
44
+ Option.match(state.data, {
45
+ onSome: (payload) => AsyncData.success(payload, state.isFetching),
46
+ onNone: () => (state.isFetching || !gated ? AsyncData.loading : AsyncData.idle),
47
47
  }),
48
48
  })
49
49
 
50
50
  /** `Loading` in the React-Query sense: fetching with nothing to show yet. */
51
- export const isLoading = <A, E>(s: QueryState<A, E>): boolean =>
52
- s.isFetching && Option.isNone(s.data)
51
+ export const isLoading = <A, E>(state: QueryState<A, E>): boolean =>
52
+ state.isFetching && Option.isNone(state.data)