subinstance proposal

This commit is contained in:
Víctor Oliva 2023-06-29 20:19:43 +02:00
parent e57de5dc3a
commit e719931523
6 changed files with 214 additions and 9 deletions

View File

@ -15,11 +15,11 @@ export function createRoot<KeyValue, KeyName extends string>(
export function createRoot<KeyValue = never, KeyName extends string = "">(
keyName?: KeyName,
): RootNode<KeyValue, KeyName> {
const internalNode = createStateNode<
null,
RootNodeKey<KeyName, KeyValue>,
null
>(keyName ? [keyName] : [], [], () => of(null))
const internalNode = createStateNode<RootNodeKey<KeyName, KeyValue>, null>(
keyName ? [keyName] : [],
[],
() => of(null),
)
internalNode.public.getState$ = () => {
throw new Error("RootNode doesn't have value")

View File

@ -1,5 +1,6 @@
export * from "./create-root"
export * from "./route-state"
export * from "./subinstance"
export * from "./substate"
export * from "./types"
export { StatePromise } from "./internal/promises"

View File

@ -47,9 +47,9 @@ interface GetObservableFn<K> {
): Observable<T>
}
export function createStateNode<T, K extends KeysBaseType, R>(
export function createStateNode<K extends KeysBaseType, R>(
keysOrder: Array<keyof K>,
parents: Array<InternalStateNode<T, K>>,
parents: Array<InternalStateNode<unknown, any>>,
instanceCreator: (
getContext: <R>(node: InternalStateNode<R, K>) => R,
getObservable: GetObservableFn<K>,
@ -67,7 +67,7 @@ export function createStateNode<T, K extends KeysBaseType, R>(
}
const getContext = <TC>(
otherNode: InternalStateNode<TC, K>,
otherNode: InternalStateNode<TC, any>,
key: K,
visited = new Set<InternalStateNode<any, any>>(),
) => {

View File

@ -0,0 +1,55 @@
import { Subject, filter, map, startWith } from "rxjs"
import { InstanceUpdate, createRoot, subinstance } from "./"
describe("subinstance", () => {
it("works", () => {
const root = createRoot()
const instance$ = new Subject<InstanceUpdate<string>>()
const updates$ = new Subject<{ key: string; value: string }>()
const instanceNode = subinstance(
root,
"keyName",
() => instance$,
(id) =>
updates$.pipe(
filter((v) => v.key === id),
map((v) => v.value),
startWith(id),
),
)
root.run()
instance$.next({
type: "add",
key: "a",
})
expect(instanceNode.getValue({ keyName: "a" })).toEqual("a")
instance$.next({
type: "add",
key: "b",
})
expect(instanceNode.getValue({ keyName: "a" })).toEqual("a")
expect(instanceNode.getValue({ keyName: "b" })).toEqual("b")
updates$.next({
key: "a",
value: "new A value",
})
expect(instanceNode.getValue({ keyName: "a" })).toEqual("new A value")
expect(instanceNode.getValue({ keyName: "b" })).toEqual("b")
instance$.next({
type: "remove",
key: "a",
})
expect(() => instanceNode.getValue({ keyName: "a" })).toThrow()
expect(instanceNode.getValue({ keyName: "b" })).toEqual("b")
instance$.next({
type: "remove",
key: "b",
})
expect(() => instanceNode.getValue({ keyName: "b" })).toThrow()
})
})

View File

@ -0,0 +1,149 @@
import { Observable, map, scan, startWith } from "rxjs"
import { NestedMap, createStateNode, getInternals } from "./internal"
import { substate } from "./substate"
import {
CtxFn,
GetObservableFn,
GetValueFn,
KeysBaseType,
StateNode,
isSignal,
} from "./types"
export interface InstanceUpdate<K> {
type: "add" | "remove"
key: K
}
type MergeKey<K extends KeysBaseType, KN extends string, KV> = {
[key in keyof K | KN]: (K & Record<KN, KV>)[key]
}
export type InstanceCtxFn<T, K extends KeysBaseType, Id> = (
id: Id,
ctxValue: GetValueFn,
ctxObservable: GetObservableFn<K>,
key: K,
) => Observable<T>
export function subinstance<
T,
K extends KeysBaseType,
KN extends string,
KV,
R,
>(
parent: StateNode<T, K>,
keyName: KN,
keySelector: CtxFn<InstanceUpdate<KV>, K>,
instanceObs: InstanceCtxFn<R, MergeKey<K, KN, KV>, KV>,
): StateNode<R, MergeKey<K, KN, KV>> {
const instanceKeys = substate(
parent,
(ctx, getObs, key) => {
const keys = Object.assign(new Set<KV>(), {
lastUpdate: null,
} as {
lastUpdate: InstanceUpdate<KV> | null
})
return keySelector(ctx, getObs, key).pipe(
scan((acc, change) => {
acc.lastUpdate = change
if (change.type === "add") {
acc.add(change.key)
} else {
acc.delete(change.key)
}
return acc
}, keys),
startWith(keys),
)
},
() => false,
)
const parentInternals = getInternals(parent)
const result = createStateNode<MergeKey<K, KN, KV>, R>(
[...parentInternals.keysOrder, keyName],
[parentInternals],
(ctx, obs, key) =>
// TODO common pattern, mapping the CtxFn from internal to external
instanceObs(
key[keyName],
(node) => ctx(getInternals(node)),
((node, keys) =>
obs(
isSignal(node) ? node : getInternals(node),
keys,
)) as GetObservableFn<MergeKey<K, KN, KV>>,
key,
),
)
const parentInstanceWatches = new NestedMap<K[keyof K], () => void>()
function watchParentInstance(key: K) {
const sub = instanceKeys
.getState$(key)
.pipe(map((v, i) => [v, i] as const))
.subscribe(([v, i]) => {
if (i === 0) {
for (let instanceKey of v) {
result.addInstance({
...key,
[keyName]: instanceKey,
})
}
for (let instanceKey of v) {
result.activateInstance({
...key,
[keyName]: instanceKey,
})
}
} else {
if (v.lastUpdate?.type === "add") {
result.addInstance({
...key,
[keyName]: v.lastUpdate.key,
})
result.activateInstance({
...key,
[keyName]: v.lastUpdate.key,
})
} else if (v.lastUpdate?.type === "remove") {
result.removeInstance({
...key,
[keyName]: v.lastUpdate.key,
})
}
}
})
parentInstanceWatches.set(
parentInternals.keysOrder.map((k) => key[k]),
() => sub.unsubscribe(),
)
}
function stopWatchParentInstance(key: K) {
const orderedKey = parentInternals.keysOrder.map((k) => key[k])
const teardown = parentInstanceWatches.get(orderedKey)
parentInstanceWatches.delete(orderedKey)
teardown?.()
}
// TODO this pattern is common on all operators... maybe it can be abstracted away? watch parent instances, do something on them, tear down their subscriptions.
for (let instance of parentInternals.getInstances()) {
watchParentInstance(instance.key)
}
parentInternals.instanceChange$.subscribe((change) => {
if (change.type === "added") {
watchParentInstance(change.key)
} else if (change.type === "removed") {
stopWatchParentInstance(change.key)
}
})
return Object.assign(result.public, {
instanceKeys,
})
}

View File

@ -27,7 +27,7 @@ export function isSignal<T, CK extends KeysBaseType>(
)
}
interface GetObservableFn<K> {
export interface GetObservableFn<K> {
<T, CK extends KeysBaseType>(
other: K extends CK ? StateNode<T, CK> | Signal<T, CK> : never,
): Observable<T>