@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.
- package/package.json +1 -1
- package/src/boundary/boundary.ts +16 -7
- package/src/calc/asyncCalc.ts +84 -37
- package/src/calc/asyncData.ts +8 -4
- package/src/calc/calc.ts +7 -3
- package/src/calc/calcFamily.ts +24 -10
- package/src/calc/compose.ts +1 -1
- package/src/calc/queryState.ts +8 -8
- package/src/channel/channel.ts +63 -37
- package/src/compose/composition.ts +20 -1
- package/src/compose/provide.ts +5 -1
- package/src/compose/slot.ts +4 -2
- package/src/compose/structure.ts +43 -19
- package/src/compose/ui.ts +21 -2
- package/src/compose/ui.typecheck.ts +4 -4
- package/src/definition/definition.ts +20 -7
- package/src/feature/feature.test.ts +4 -4
- package/src/feature/feature.ts +30 -16
- package/src/feature/feature.typecheck.ts +2 -2
- package/src/internal/capture.ts +1 -0
- package/src/internal/errors.ts +9 -4
- package/src/internal/inspect.ts +4 -4
- package/src/internal/queryDriver.ts +102 -53
- package/src/internal/reuse.ts +67 -30
- package/src/internal/scheduler.ts +35 -23
- package/src/internal/sources.ts +14 -8
- package/src/internal/stateRegistry.ts +3 -1
- package/src/internal/store.ts +10 -8
- package/src/internal/track.ts +3 -1
- package/src/procedure/procedure.ts +2 -2
- package/src/reducer/reducer.ts +17 -9
- package/src/remote/remoteState.test.ts +188 -1
- package/src/remote/remoteState.ts +112 -51
- package/src/remote/remoteState.typecheck.ts +4 -1
- package/src/runtime/bus.ts +3 -1
- package/src/runtime/hardening.test.ts +1 -1
- package/src/runtime/loop.ts +67 -46
- package/src/runtime/queries.ts +3 -1
- package/src/scene/scene.ts +16 -8
- package/src/state/state.ts +11 -10
- package/src/state/stateFamily.ts +27 -11
- package/src/state/stateGroup.ts +22 -9
- package/src/synced/syncedStore.ts +15 -9
- package/src/wire/tree.ts +66 -30
- package/src/wire/triggers.ts +9 -8
package/src/state/stateFamily.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Context, Effect, Layer, type Schema } from 'effect'
|
|
1
|
+
import { Context, Effect, Layer, Option, type Schema } from 'effect'
|
|
2
2
|
import { type Manifest, definitionClass } from '../definition/definition'
|
|
3
3
|
import { resolveScheduler, type Scheduler } from '../internal/scheduler'
|
|
4
4
|
import { claimStateTag } from '../internal/stateRegistry'
|
|
@@ -31,7 +31,7 @@ export interface FamilyStore<K, V> {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/** Options for a family's keyed store. */
|
|
34
|
-
export interface
|
|
34
|
+
export interface FamilyOptionsExternalApi {
|
|
35
35
|
/**
|
|
36
36
|
* Drop a key's store automatically once it has no live subscribers (checked on
|
|
37
37
|
* the next microtask, so a same-commit re-subscribe — e.g. a key that just moved
|
|
@@ -44,6 +44,7 @@ export interface FamilyOptions {
|
|
|
44
44
|
*/
|
|
45
45
|
readonly evictWhenUnused?: boolean
|
|
46
46
|
}
|
|
47
|
+
export type FamilyOptions = FamilyOptionsExternalApi
|
|
47
48
|
|
|
48
49
|
const makeFamilyStore = <K, V>(
|
|
49
50
|
seed: (key: K) => V,
|
|
@@ -54,6 +55,8 @@ const makeFamilyStore = <K, V>(
|
|
|
54
55
|
const evictWhenUnused = options.evictWhenUnused === true
|
|
55
56
|
// Live subscriber count per key — maintained only when eviction is on.
|
|
56
57
|
const subscribers = new Map<K, number>()
|
|
58
|
+
const liveSubscriberCount = (key: K): number =>
|
|
59
|
+
Option.getOrElse(Option.fromNullable(subscribers.get(key)), () => 0)
|
|
57
60
|
|
|
58
61
|
// Wrap a store's `subscribe` to ref-count, evicting the key when it falls idle.
|
|
59
62
|
// `get`/`set`/`getVersion` are delegated unchanged, so the loop writes and the
|
|
@@ -61,11 +64,13 @@ const makeFamilyStore = <K, V>(
|
|
|
61
64
|
const refCounted = (key: K, store: Store<V>): Store<V> => ({
|
|
62
65
|
...store,
|
|
63
66
|
subscribe: (listener) => {
|
|
64
|
-
subscribers.set(key, (
|
|
67
|
+
subscribers.set(key, liveSubscriberCount(key) + 1)
|
|
65
68
|
const off = store.subscribe(listener)
|
|
66
69
|
const released = { done: false }
|
|
67
70
|
return () => {
|
|
68
|
-
if (released.done)
|
|
71
|
+
if (released.done) {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
69
74
|
released.done = true
|
|
70
75
|
off()
|
|
71
76
|
const remaining = (subscribers.get(key) ?? 1) - 1
|
|
@@ -75,7 +80,9 @@ const makeFamilyStore = <K, V>(
|
|
|
75
80
|
}
|
|
76
81
|
subscribers.delete(key)
|
|
77
82
|
queueMicrotask(() => {
|
|
78
|
-
if ((
|
|
83
|
+
if (liveSubscriberCount(key) === 0) {
|
|
84
|
+
entries.delete(key)
|
|
85
|
+
}
|
|
79
86
|
})
|
|
80
87
|
}
|
|
81
88
|
},
|
|
@@ -84,7 +91,9 @@ const makeFamilyStore = <K, V>(
|
|
|
84
91
|
return {
|
|
85
92
|
at: (key) => {
|
|
86
93
|
const existing = entries.get(key)
|
|
87
|
-
if (existing !== undefined)
|
|
94
|
+
if (existing !== undefined) {
|
|
95
|
+
return existing
|
|
96
|
+
}
|
|
88
97
|
const base = makeStore(seed(key), scheduler)
|
|
89
98
|
const created = evictWhenUnused ? refCounted(key, base) : base
|
|
90
99
|
entries.set(key, created)
|
|
@@ -102,7 +111,7 @@ const makeFamilyStore = <K, V>(
|
|
|
102
111
|
}
|
|
103
112
|
}
|
|
104
113
|
|
|
105
|
-
export interface
|
|
114
|
+
export interface StateFamilyManifestExternalApi<N extends string, K, V> extends Manifest {
|
|
106
115
|
readonly kind: 'StateFamily'
|
|
107
116
|
readonly name: N
|
|
108
117
|
readonly key: Schema.Schema<K, any>
|
|
@@ -110,6 +119,7 @@ export interface StateFamilyManifest<N extends string, K, V> extends Manifest {
|
|
|
110
119
|
readonly title?: string
|
|
111
120
|
readonly description?: string
|
|
112
121
|
}
|
|
122
|
+
export type StateFamilyManifest<N extends string, K, V> = StateFamilyManifestExternalApi<N, K, V>
|
|
113
123
|
|
|
114
124
|
export interface StateFamilyClass<out N extends string, in out K, in out V> {
|
|
115
125
|
/** Instance carries the key phantom so `Family['Key']` resolves in `keyOf` types. */
|
|
@@ -126,11 +136,16 @@ export type FamilyValue<F> = F extends StateFamilyClass<any, any, infer V> ? V :
|
|
|
126
136
|
* Normalized keyed state: one logical State per key, backed by a single family
|
|
127
137
|
* store. Scales with the collection without re-rendering unrelated entries.
|
|
128
138
|
*/
|
|
139
|
+
export interface StateFamilyMakeOptionsExternalApi {
|
|
140
|
+
readonly title?: string
|
|
141
|
+
readonly description?: string
|
|
142
|
+
}
|
|
143
|
+
|
|
129
144
|
export const make = <const N extends string, K, V>(
|
|
130
145
|
name: N,
|
|
131
146
|
key: Schema.Schema<K, any>,
|
|
132
|
-
|
|
133
|
-
options:
|
|
147
|
+
valueSchema: Schema.Schema<V, any>,
|
|
148
|
+
options: StateFamilyMakeOptionsExternalApi = {},
|
|
134
149
|
): StateFamilyClass<N, K, V> => {
|
|
135
150
|
const identifier = `reform/family/${name}`
|
|
136
151
|
claimStateTag(identifier)
|
|
@@ -139,7 +154,7 @@ export const make = <const N extends string, K, V>(
|
|
|
139
154
|
kind: 'StateFamily',
|
|
140
155
|
name,
|
|
141
156
|
key,
|
|
142
|
-
value,
|
|
157
|
+
value: valueSchema,
|
|
143
158
|
...(options.title !== undefined ? { title: options.title } : {}),
|
|
144
159
|
...(options.description !== undefined ? { description: options.description } : {}),
|
|
145
160
|
}
|
|
@@ -154,7 +169,7 @@ export const read = <N extends string, K, V>(
|
|
|
154
169
|
family: StateFamilyClass<N, K, V>,
|
|
155
170
|
key: K,
|
|
156
171
|
): Effect.Effect<V, never, FamilyStore<K, V>> =>
|
|
157
|
-
Effect.flatMap(family.store, (
|
|
172
|
+
Effect.flatMap(family.store, (familyStore) => readTracked(familyStore.at(key)))
|
|
158
173
|
|
|
159
174
|
/**
|
|
160
175
|
* Allocate a family's keyed store — `StateFamily.live(ItemUi, seed)`. The seed
|
|
@@ -166,6 +181,7 @@ export const live = <N extends string, K, V>(
|
|
|
166
181
|
initial: V | ((key: K) => V),
|
|
167
182
|
options: FamilyOptions = {},
|
|
168
183
|
): Layer.Layer<FamilyStore<K, V>> => {
|
|
184
|
+
// oxlint-disable-next-line reform-rules/no-type-assertion -- value-or-factory union can't be discriminated for a generic V without a cast
|
|
169
185
|
const seed: (key: K) => V = typeof initial === 'function' ? (initial as (key: K) => V) : () => initial
|
|
170
186
|
return Layer.effect(
|
|
171
187
|
family.store,
|
package/src/state/stateGroup.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Layer } from 'effect'
|
|
1
|
+
import { Array as Arr, Layer, Option } from 'effect'
|
|
2
2
|
import { definitionClass } from '../definition/definition'
|
|
3
3
|
import { DuplicateRegistration, UnknownGroupState } from '../internal/errors'
|
|
4
4
|
// A group's members each carry a `store` tag, so the shared `StoresOf` (used for
|
|
@@ -77,13 +77,21 @@ export const make = <const Members extends ReadonlyArray<AnyState>>(
|
|
|
77
77
|
): StateGroupClass<Members> => {
|
|
78
78
|
// `byName` already de-dupes, so a smaller map than the member list means two
|
|
79
79
|
// members claimed one name — unaddressable, and a silent footgun on merge.
|
|
80
|
-
const names = members.map((
|
|
81
|
-
const duplicate =
|
|
82
|
-
|
|
80
|
+
const names = members.map((member) => member.manifest.name)
|
|
81
|
+
const duplicate = Arr.findFirst(names, (name, index) =>
|
|
82
|
+
Option.exists(
|
|
83
|
+
Arr.findFirstIndex(names, (candidate) => candidate === name),
|
|
84
|
+
(firstIndex) => firstIndex !== index,
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
if (Option.isSome(duplicate)) {
|
|
88
|
+
// oxlint-disable-next-line reform-rules/no-throw -- definition-time invariant; sync factory has no Effect context (see internal/errors.ts)
|
|
89
|
+
throw new DuplicateRegistration({ kind: 'state in group', name: duplicate.value })
|
|
90
|
+
}
|
|
83
91
|
return definitionClass<StateGroupClass<Members>>({
|
|
84
92
|
kind: 'StateGroup' as const,
|
|
85
93
|
members,
|
|
86
|
-
byName: new Map(members.map((
|
|
94
|
+
byName: new Map(members.map((member) => [member.manifest.name, member] as const)),
|
|
87
95
|
})
|
|
88
96
|
}
|
|
89
97
|
|
|
@@ -102,8 +110,11 @@ export const select = <Members extends ReadonlyArray<AnyState>, N extends StateN
|
|
|
102
110
|
name: N,
|
|
103
111
|
): StateToken<N, ValueForName<Members, N>> => {
|
|
104
112
|
const member = group.byName.get(name)
|
|
105
|
-
if (member === undefined)
|
|
106
|
-
|
|
113
|
+
if (member === undefined) {
|
|
114
|
+
// oxlint-disable-next-line reform-rules/no-throw -- definition-time invariant; sync lookup has no Effect context (see internal/errors.ts)
|
|
115
|
+
throw new UnknownGroupState({ name })
|
|
116
|
+
}
|
|
117
|
+
return new StateToken(name, member.store)
|
|
107
118
|
}
|
|
108
119
|
|
|
109
120
|
/**
|
|
@@ -118,8 +129,10 @@ export const live = <Members extends ReadonlyArray<AnyState>>(
|
|
|
118
129
|
// reflection boundary (string key into a mapped type). Each member's store
|
|
119
130
|
// layer is then merged; the union of stores is exactly `StoresOf<Members>`,
|
|
120
131
|
// which `reduce`'s single-layer accumulator can't express, so restate it.
|
|
132
|
+
// oxlint-disable-next-line reform-rules/no-type-assertion -- reflection boundary: index a mapped seed type by runtime member name
|
|
121
133
|
const seedRecord = seeds as Record<string, unknown>
|
|
134
|
+
// oxlint-disable-next-line reform-rules/no-type-assertion -- union-of-stores Rout can't be expressed by reduce's single-layer accumulator
|
|
122
135
|
return group.members
|
|
123
|
-
.map((
|
|
124
|
-
.reduce((
|
|
136
|
+
.map((member) => stateLive(member, seedRecord[member.manifest.name]))
|
|
137
|
+
.reduce((accumulator, layer) => Layer.merge(accumulator, layer)) as Layer.Layer<StoresOf<Members>>
|
|
125
138
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Context, Effect, Layer, type Schema, type Scope } from 'effect'
|
|
1
|
+
import { Context, Effect, Layer, Option, type Schema, type Scope } from 'effect'
|
|
2
2
|
import { type Manifest, yieldableClass } from '../definition/definition'
|
|
3
3
|
import { resolveScheduler } from '../internal/scheduler'
|
|
4
4
|
import { makeDerivedStore, type Store } from '../internal/store'
|
|
@@ -31,8 +31,8 @@ export interface SyncedStoreManifest<N extends string, A> extends Manifest {
|
|
|
31
31
|
readonly kind: 'SyncedStore'
|
|
32
32
|
readonly name: N
|
|
33
33
|
readonly schema: Schema.Schema<A, any>
|
|
34
|
-
readonly title
|
|
35
|
-
readonly description
|
|
34
|
+
readonly title: Option.Option<string>
|
|
35
|
+
readonly description: Option.Option<string>
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export interface SyncedStoreClass<out N extends string, in out A>
|
|
@@ -46,6 +46,15 @@ export interface SyncedStoreClass<out N extends string, in out A>
|
|
|
46
46
|
readonly name: N
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* A minimal store-carrier — satisfied by a `SyncedStoreClass` and by any
|
|
51
|
+
* companion primitive (e.g. `@playfast/reform-db`'s `DbQuery`) that allocates
|
|
52
|
+
* its own `Store` tag, so `SyncedStore.live` is reusable beyond `SyncedStore`.
|
|
53
|
+
*/
|
|
54
|
+
export interface StoreCarrier<A> {
|
|
55
|
+
readonly store: Context.Tag<Store<A>, Store<A>>
|
|
56
|
+
}
|
|
57
|
+
|
|
49
58
|
export type AnySyncedStore = SyncedStoreClass<string, any>
|
|
50
59
|
export type SyncedValue<S> = S extends SyncedStoreClass<string, infer A> ? A : never
|
|
51
60
|
|
|
@@ -65,8 +74,8 @@ export const make = <const N extends string, A>(
|
|
|
65
74
|
kind: 'SyncedStore',
|
|
66
75
|
name,
|
|
67
76
|
schema,
|
|
68
|
-
|
|
69
|
-
|
|
77
|
+
title: Option.fromNullable(options.title),
|
|
78
|
+
description: Option.fromNullable(options.description),
|
|
70
79
|
}
|
|
71
80
|
const read = Effect.flatMap(store, readTracked)
|
|
72
81
|
return yieldableClass(read, { manifest, store, name })
|
|
@@ -81,10 +90,7 @@ export const make = <const N extends string, A>(
|
|
|
81
90
|
* the layer's scope.
|
|
82
91
|
*/
|
|
83
92
|
export const live = <A, R>(
|
|
84
|
-
|
|
85
|
-
// companion primitive (e.g. `@playfast/reform-db`'s `DbQuery`) that allocates
|
|
86
|
-
// its own `Store` tag, so the seam is reusable beyond `SyncedStore` itself.
|
|
87
|
-
def: { readonly store: Context.Tag<Store<A>, Store<A>> },
|
|
93
|
+
def: StoreCarrier<A>,
|
|
88
94
|
acquire: Effect.Effect<SyncedSource<A>, never, R>,
|
|
89
95
|
): Layer.Layer<Store<A>, never, Exclude<R, Scope.Scope>> =>
|
|
90
96
|
Layer.scoped(
|
package/src/wire/tree.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Match } from 'effect'
|
|
1
|
+
import { Array, Match, Option, Order, Record as Rec } from 'effect'
|
|
2
|
+
import { sort as sortArray } from 'effect/Array'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* The serializable model of a rendered UI tree, and the pure diff/apply over it.
|
|
@@ -18,8 +19,9 @@ export type WireProp =
|
|
|
18
19
|
| { readonly _tag: 'Data'; readonly name: string; readonly value: unknown }
|
|
19
20
|
| { readonly _tag: 'Event'; readonly name: string; readonly handle: string }
|
|
20
21
|
|
|
21
|
-
/** One rendered UI contract instance, identified stably across frames.
|
|
22
|
-
|
|
22
|
+
/** One rendered UI contract instance, identified stably across frames. The serializable
|
|
23
|
+
* wire-boundary shape (hence the `ExternalApi` postfix; `null` is part of the JSON contract). */
|
|
24
|
+
export interface WireNodeExternalApi {
|
|
23
25
|
/** Stable identity across renders — the unit of diffing. */
|
|
24
26
|
readonly id: string
|
|
25
27
|
/** The UI contract name (`UiCapture.name`) the client looks up a presentation by. */
|
|
@@ -42,6 +44,9 @@ export interface WireNode {
|
|
|
42
44
|
readonly props: ReadonlyArray<WireProp>
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
/** Public alias preserving the established name across the codebase. */
|
|
48
|
+
export type WireNode = WireNodeExternalApi
|
|
49
|
+
|
|
45
50
|
export type WireTree = ReadonlyArray<WireNode>
|
|
46
51
|
|
|
47
52
|
/** A change to apply to a client's tree: upsert a node, or drop one by id. */
|
|
@@ -49,34 +54,54 @@ export type WirePatch =
|
|
|
49
54
|
| { readonly _tag: 'Upsert'; readonly node: WireNode }
|
|
50
55
|
| { readonly _tag: 'Delete'; readonly id: string }
|
|
51
56
|
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
interface EqualPair {
|
|
58
|
+
readonly left: unknown
|
|
59
|
+
readonly right: unknown
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const isRecord = (candidate: unknown): candidate is Record<string, unknown> =>
|
|
63
|
+
typeof candidate === 'object' && candidate !== null
|
|
64
|
+
|
|
65
|
+
const arraysEqual = (left: ReadonlyArray<unknown>, right: ReadonlyArray<unknown>): boolean =>
|
|
66
|
+
left.length === right.length &&
|
|
67
|
+
left.every((element, index) => deepEqual({ left: element, right: right[index] }))
|
|
54
68
|
|
|
55
|
-
const recordsEqual = (
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
return
|
|
69
|
+
const recordsEqual = (left: Record<string, unknown>, right: Record<string, unknown>): boolean => {
|
|
70
|
+
const leftKeys = Rec.keys(left)
|
|
71
|
+
const rightKeys = Rec.keys(right)
|
|
72
|
+
return (
|
|
73
|
+
leftKeys.length === rightKeys.length &&
|
|
74
|
+
leftKeys.every((key) => deepEqual({ left: left[key], right: right[key] }))
|
|
75
|
+
)
|
|
59
76
|
}
|
|
60
77
|
|
|
61
78
|
/** Structural equality over serializable wire values (and the tagged props that carry them). */
|
|
62
|
-
const deepEqual = (
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
|
|
79
|
+
const deepEqual = ({ left, right }: EqualPair): boolean => {
|
|
80
|
+
if (left === right) {
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
if (left === null || right === null) {
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(left)) {
|
|
87
|
+
return Array.isArray(right) && arraysEqual(left, right)
|
|
88
|
+
}
|
|
89
|
+
if (Array.isArray(right)) {
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
if (isRecord(left) && isRecord(right)) {
|
|
93
|
+
return recordsEqual(left, right)
|
|
69
94
|
}
|
|
70
95
|
return false
|
|
71
96
|
}
|
|
72
97
|
|
|
73
|
-
const nodesEqual = (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
deepEqual(
|
|
98
|
+
const nodesEqual = (leftNode: WireNode, rightNode: WireNode): boolean =>
|
|
99
|
+
leftNode.name === rightNode.name &&
|
|
100
|
+
leftNode.parentId === rightNode.parentId &&
|
|
101
|
+
leftNode.childIndex === rightNode.childIndex &&
|
|
102
|
+
leftNode.slot === rightNode.slot &&
|
|
103
|
+
leftNode.key === rightNode.key &&
|
|
104
|
+
deepEqual({ left: leftNode.props, right: rightNode.props })
|
|
80
105
|
|
|
81
106
|
const isUnchanged = (node: WireNode, previousById: ReadonlyMap<string, WireNode>): boolean => {
|
|
82
107
|
const previous = previousById.get(node.id)
|
|
@@ -102,11 +127,14 @@ export const diff = (previous: WireTree, next: WireTree): ReadonlyArray<WirePatc
|
|
|
102
127
|
return [...deletes, ...upserts]
|
|
103
128
|
}
|
|
104
129
|
|
|
105
|
-
const upsertNode = (state: WireTree, node: WireNode): WireTree =>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
130
|
+
const upsertNode = (state: WireTree, node: WireNode): WireTree =>
|
|
131
|
+
Option.match(
|
|
132
|
+
Array.findFirstIndex(state, (existing) => existing.id === node.id),
|
|
133
|
+
{
|
|
134
|
+
onNone: () => [...state, node],
|
|
135
|
+
onSome: (index) => [...state.slice(0, index), node, ...state.slice(index + 1)],
|
|
136
|
+
},
|
|
137
|
+
)
|
|
110
138
|
|
|
111
139
|
/** Fold patches into a client's tree (the receiving side's reducer). */
|
|
112
140
|
export const apply = (state: WireTree, patches: ReadonlyArray<WirePatch>): WireTree =>
|
|
@@ -120,10 +148,18 @@ export const apply = (state: WireTree, patches: ReadonlyArray<WirePatch>): WireT
|
|
|
120
148
|
state,
|
|
121
149
|
)
|
|
122
150
|
|
|
151
|
+
const bySiblingOrder = Order.mapInput(Order.number, (node: WireNode) => node.childIndex)
|
|
152
|
+
|
|
123
153
|
/** The root nodes of a tree, in sibling order. */
|
|
124
154
|
export const roots = (tree: WireTree): WireTree =>
|
|
125
|
-
|
|
155
|
+
sortArray(
|
|
156
|
+
tree.filter((node) => node.parentId === null),
|
|
157
|
+
bySiblingOrder,
|
|
158
|
+
)
|
|
126
159
|
|
|
127
160
|
/** The children of a node, in sibling order. */
|
|
128
161
|
export const childrenOf = (tree: WireTree, parentId: string): WireTree =>
|
|
129
|
-
|
|
162
|
+
sortArray(
|
|
163
|
+
tree.filter((node) => node.parentId === parentId),
|
|
164
|
+
bySiblingOrder,
|
|
165
|
+
)
|
package/src/wire/triggers.ts
CHANGED
|
@@ -73,16 +73,17 @@ export const make: Effect.Effect<TriggerRegistryApi> = Effect.gen(function* () {
|
|
|
73
73
|
): Effect.Effect<void> =>
|
|
74
74
|
Ref.update(entries, (map) => new Map(map).set(handle, { trigger, schema }))
|
|
75
75
|
|
|
76
|
-
const invoke = (
|
|
76
|
+
const invoke = Effect.fn('invoke')(function* (
|
|
77
77
|
handle: TriggerHandle,
|
|
78
78
|
encodedPayload: unknown,
|
|
79
|
-
): Effect.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
79
|
+
): Effect.fn.Return<void, UnknownTrigger | ParseResult.ParseError> {
|
|
80
|
+
const entry = (yield* Ref.get(entries)).get(handle)
|
|
81
|
+
if (entry === undefined) {
|
|
82
|
+
return yield* Effect.fail(new UnknownTrigger({ handle }))
|
|
83
|
+
}
|
|
84
|
+
const payload = yield* Schema.decodeUnknown(entry.schema)(encodedPayload)
|
|
85
|
+
entry.trigger(payload)
|
|
86
|
+
})
|
|
86
87
|
|
|
87
88
|
const revoke = (handle: TriggerHandle): Effect.Effect<void> =>
|
|
88
89
|
Ref.update(entries, (map) => {
|