Skip to content

Commit ff0f297

Browse files
Compact child registry coordination
1 parent 5d37ead commit ff0f297

3 files changed

Lines changed: 194 additions & 115 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@typeonce/effect-machine": patch
3+
---
4+
5+
Reduce child-capable process memory and lifecycle overhead with a compact synchronous registry, while lazily allocating ordered child-change publication only when observed.

src/internal/machineRuntime.ts

Lines changed: 124 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,13 @@ import * as Deferred from "effect/Deferred"
1111
import * as Effect from "effect/Effect"
1212
import * as Exit from "effect/Exit"
1313
import * as Fiber from "effect/Fiber"
14-
import * as HashMap from "effect/HashMap"
1514
import * as MutableRef from "effect/MutableRef"
1615
import * as Option from "effect/Option"
1716
import * as PubSub from "effect/PubSub"
1817
import * as Queue from "effect/Queue"
1918
import * as Ref from "effect/Ref"
2019
import * as Scope from "effect/Scope"
2120
import * as Stream from "effect/Stream"
22-
import * as SubscriptionRef from "effect/SubscriptionRef"
2321
import * as SynchronizedRef from "effect/SynchronizedRef"
2422
import type * as Take from "effect/Take"
2523
import { ChildAlreadyExistsError, StoppedError } from "./machineErrors.js"
@@ -56,12 +54,15 @@ export const compiledProcess: unique symbol = Symbol.for("effect/Machine/compile
5654
interface ChildRegistrySnapshot {
5755
readonly closed: boolean
5856
readonly revision: number
59-
readonly children: HashMap.HashMap<ChildKey, ChildEntry>
57+
readonly children: ReadonlyMap<ChildKey, ChildEntry>
6058
}
6159

62-
interface ChildRegistry extends ChildRegistrySnapshot {
63-
readonly changes: SubscriptionRef.SubscriptionRef<ChildRegistrySnapshot> | undefined
64-
readonly scope: Scope.Closeable | undefined
60+
interface ChildRegistry {
61+
closed: boolean
62+
revision: number
63+
readonly children: Map<ChildKey, ChildEntry>
64+
changes: PubSub.PubSub<ChildRegistrySnapshot> | undefined
65+
scope: Scope.Closeable | undefined
6566
}
6667

6768
export type RuntimeSnapshot<State, Error = never, Output = never> =
@@ -385,33 +386,43 @@ const makeChildRuntime = (
385386
self: ProcessAddress<any>,
386387
options: StartInternalOptions
387388
): Effect.Effect<ChildRuntime> =>
388-
Effect.gen(function*() {
389-
const registry = yield* SynchronizedRef.make<ChildRegistry>({
389+
Effect.sync(() => {
390+
// Child-registry decisions are synchronous and every access below runs in
391+
// one Effect.sync / Effect.suspend step. Keep the unobserved representation
392+
// compact; a replay PubSub is installed only when childChanges is used.
393+
const registry: ChildRegistry = {
390394
closed: false,
391395
revision: 0,
392-
children: HashMap.empty(),
396+
children: new Map(),
393397
changes: undefined,
394398
scope: undefined
395-
})
399+
}
396400

397401
const snapshot = (registry: ChildRegistry): ChildRegistrySnapshot => ({
398402
closed: registry.closed,
399403
revision: registry.revision,
400-
children: registry.children
404+
children: new Map(registry.children)
401405
})
402406

407+
const publishRegistryChange = (): void => {
408+
if (registry.changes !== undefined) {
409+
PubSub.publishUnsafe(registry.changes, snapshot(registry))
410+
}
411+
}
412+
403413
const close = <A, E>(_exit: Exit.Exit<A, E>): Effect.Effect<void> =>
404-
SynchronizedRef.modify(registry, (current) => {
405-
if (current.closed) {
406-
return [undefined, current] as const
414+
Effect.sync(() => {
415+
if (registry.closed) {
416+
return undefined
407417
}
408-
if (current.scope === undefined) {
409-
return [undefined, { ...current, closed: true }] as const
418+
registry.closed = true
419+
if (registry.scope === undefined) {
420+
return undefined
410421
}
411-
const children = Array.from(HashMap.values(current.children)).flatMap((entry) =>
422+
const children = Array.from(registry.children.values()).flatMap((entry) =>
412423
entry._tag === "Started" ? [entry.ref] : []
413424
)
414-
return [{ children, scope: current.scope }, { ...current, closed: true }] as const
425+
return { children, scope: registry.scope }
415426
}).pipe(
416427
Effect.flatMap((resources) =>
417428
resources === undefined
@@ -426,57 +437,48 @@ const makeChildRuntime = (
426437
)
427438
)
428439

429-
const getOrCreateScope: Effect.Effect<Scope.Closeable | undefined> = SynchronizedRef.modifyEffect(
430-
registry,
431-
(current) => {
432-
if (current.closed || current.scope !== undefined) {
433-
return Effect.succeed([current.scope, current] as const)
434-
}
435-
return Scope.make("parallel").pipe(
436-
Effect.map((scope) => [scope, { ...current, scope }] as const)
437-
)
440+
const getOrCreateScope: Effect.Effect<Scope.Closeable | undefined> = Effect.sync(() => {
441+
if (registry.closed) {
442+
return undefined
438443
}
439-
)
444+
if (registry.scope === undefined) {
445+
registry.scope = Scope.makeUnsafe("parallel")
446+
}
447+
return registry.scope
448+
})
440449

441450
const reserve = (
442451
key: ChildKey,
443452
token: symbol
444453
): Effect.Effect<boolean, ChildAlreadyExistsError> =>
445-
SynchronizedRef.modifyEffect(registry, (current) => {
446-
if (current.closed) {
447-
return Effect.succeed([false, current] as const)
454+
Effect.suspend(() => {
455+
if (registry.closed) {
456+
return Effect.succeed(false)
448457
}
449-
if (typeof key === "string" && HashMap.has(current.children, key)) {
458+
if (typeof key === "string" && registry.children.has(key)) {
450459
return Effect.fail(new ChildAlreadyExistsError({ id: key }))
451460
}
452-
return Effect.succeed(
453-
[
454-
true,
455-
{ ...current, children: HashMap.set(current.children, key, { _tag: "Starting", token }) }
456-
] as const
457-
)
461+
registry.children.set(key, { _tag: "Starting", token })
462+
return Effect.succeed(true)
458463
})
459464

460465
const unregister = (
461466
key: ChildKey,
462467
token: symbol
463468
): Effect.Effect<void> =>
464-
SynchronizedRef.modifyEffect(registry, (current) => {
465-
const entry = HashMap.get(current.children, key)
466-
if (Option.isNone(entry) || entry.value.token !== token) {
467-
return Effect.succeed([undefined, current] as const)
469+
Effect.sync(() => {
470+
const entry = registry.children.get(key)
471+
if (entry === undefined || entry.token !== token) {
472+
return
468473
}
469474
const observable = typeof key === "string"
470-
const next = {
471-
...current,
472-
revision: observable ? current.revision + 1 : current.revision,
473-
children: HashMap.remove(current.children, key)
475+
if (observable) {
476+
registry.revision += 1
477+
}
478+
registry.children.delete(key)
479+
if (observable) {
480+
publishRegistryChange()
474481
}
475-
return observable && next.changes !== undefined
476-
? SubscriptionRef.set(next.changes, snapshot(next)).pipe(
477-
Effect.as([undefined, next] as const)
478-
)
479-
: Effect.succeed([undefined, next] as const)
480482
})
481483

482484
const register = (
@@ -485,31 +487,26 @@ const makeChildRuntime = (
485487
ref: MachineRef<any, any, any, any>,
486488
descriptor: ChildDescriptor | undefined
487489
): Effect.Effect<boolean> =>
488-
SynchronizedRef.modifyEffect(registry, (current) => {
489-
const entry = HashMap.get(current.children, key)
490+
Effect.sync(() => {
491+
const entry = registry.children.get(key)
490492
if (
491-
current.closed ||
492-
Option.isNone(entry) ||
493-
entry.value._tag !== "Starting" ||
494-
entry.value.token !== token
493+
registry.closed ||
494+
entry === undefined ||
495+
entry._tag !== "Starting" ||
496+
entry.token !== token
495497
) {
496-
return Effect.succeed([false, current] as const)
498+
return false
497499
}
498500
const observable = typeof key === "string"
499-
const next = {
500-
...current,
501-
revision: observable ? current.revision + 1 : current.revision,
502-
children: HashMap.set(
503-
HashMap.remove(current.children, key),
504-
key,
505-
{ _tag: "Started", token, descriptor, ref }
506-
)
501+
if (observable) {
502+
registry.revision += 1
507503
}
508-
return observable && next.changes !== undefined
509-
? SubscriptionRef.set(next.changes, snapshot(next)).pipe(
510-
Effect.as([true, next] as const)
511-
)
512-
: Effect.succeed([true, next] as const)
504+
registry.children.delete(key)
505+
registry.children.set(key, { _tag: "Started", token, descriptor, ref })
506+
if (observable) {
507+
publishRegistryChange()
508+
}
509+
return true
513510
})
514511

515512
const matches = (
@@ -524,31 +521,47 @@ const makeChildRuntime = (
524521

525522
const get: ChildRuntime["get"] = (child) => {
526523
const id = typeof child === "string" ? child : child.id
527-
return SynchronizedRef.get(registry).pipe(
528-
Effect.map((registry) => {
529-
if (registry.closed) {
530-
return Option.none()
531-
}
532-
const entry = HashMap.get(registry.children, id)
533-
return Option.isSome(entry) && matches(entry.value, child)
534-
? Option.some(entry.value.ref)
535-
: Option.none()
536-
})
537-
)
524+
return Effect.sync(() => {
525+
if (registry.closed) {
526+
return Option.none()
527+
}
528+
const entry = registry.children.get(id)
529+
return entry !== undefined && matches(entry, child)
530+
? Option.some(entry.ref)
531+
: Option.none()
532+
})
538533
}
539534

540535
const changes: ChildRuntime["changes"] = (child) => {
541536
const id = typeof child === "string" ? child : child.id
542537
return Stream.unwrap(
543-
SynchronizedRef.modifyEffect(registry, (current) => {
544-
if (current.closed) {
545-
return Effect.succeed([undefined, current] as const)
538+
Effect.suspend(() => {
539+
if (registry.closed) {
540+
return Effect.succeed(undefined)
546541
}
547-
if (current.changes !== undefined) {
548-
return Effect.succeed([current.changes, current] as const)
542+
if (registry.changes !== undefined) {
543+
return Effect.succeed(registry.changes)
549544
}
550-
return SubscriptionRef.make(snapshot(current)).pipe(
551-
Effect.map((changes) => [changes, { ...current, changes }] as const)
545+
return PubSub.unbounded<ChildRegistrySnapshot>({ replay: 1 }).pipe(
546+
Effect.flatMap((candidate) =>
547+
Effect.sync(() => {
548+
if (registry.closed) {
549+
return [undefined, true] as const
550+
}
551+
if (registry.changes !== undefined) {
552+
return [registry.changes, true] as const
553+
}
554+
registry.changes = candidate
555+
PubSub.publishUnsafe(candidate, snapshot(registry))
556+
return [candidate, false] as const
557+
}).pipe(
558+
Effect.flatMap(([changes, discardCandidate]) =>
559+
discardCandidate
560+
? PubSub.shutdown(candidate).pipe(Effect.as(changes))
561+
: Effect.succeed(changes)
562+
)
563+
)
564+
)
552565
)
553566
}).pipe(
554567
Effect.flatMap((changes) => {
@@ -559,45 +572,41 @@ const makeChildRuntime = (
559572
if (registry.closed) {
560573
return Option.none()
561574
}
562-
const entry = HashMap.get(registry.children, id)
563-
return Option.isSome(entry) && matches(entry.value, child)
564-
? Option.some(entry.value.ref)
575+
const entry = registry.children.get(id)
576+
return entry !== undefined && matches(entry, child)
577+
? Option.some(entry.ref)
565578
: Option.none()
566579
}
567-
return Effect.succeed(SubscriptionRef.changes(changes).pipe(Stream.map(select)))
580+
return Effect.succeed(Stream.fromPubSub(changes).pipe(Stream.map(select)))
568581
})
569582
)
570583
)
571584
}
572585

573586
const sendTo = (child: ChildSelector, event: unknown): Effect.Effect<void, StoppedError> => {
574587
const id = typeof child === "string" ? child : child.id
575-
return SynchronizedRef.get(registry).pipe(
576-
Effect.flatMap((registry) => {
577-
if (registry.closed) {
578-
return Effect.void
579-
}
580-
const entry = HashMap.get(registry.children, id)
581-
return Option.isSome(entry) && matches(entry.value, child)
582-
? entry.value.ref.send(event)
583-
: Effect.void
584-
})
585-
)
588+
return Effect.suspend(() => {
589+
if (registry.closed) {
590+
return Effect.void
591+
}
592+
const entry = registry.children.get(id)
593+
return entry !== undefined && matches(entry, child)
594+
? entry.ref.send(event)
595+
: Effect.void
596+
})
586597
}
587598

588599
const stop = (child: ChildSelector): Effect.Effect<void> => {
589600
const id = typeof child === "string" ? child : child.id
590-
return SynchronizedRef.get(registry).pipe(
591-
Effect.flatMap((registry) => {
592-
if (registry.closed) {
593-
return Effect.void
594-
}
595-
const entry = HashMap.get(registry.children, id)
596-
return Option.isSome(entry) && matches(entry.value, child)
597-
? entry.value.ref.stop
598-
: Effect.void
599-
})
600-
)
601+
return Effect.suspend(() => {
602+
if (registry.closed) {
603+
return Effect.void
604+
}
605+
const entry = registry.children.get(id)
606+
return entry !== undefined && matches(entry, child)
607+
? entry.ref.stop
608+
: Effect.void
609+
})
601610
}
602611

603612
function spawn<ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError = never>(

0 commit comments

Comments
 (0)