/** * @since 1.0.0 */ import * as Context from "effect/Context" import * as Effect from "effect/Effect" import * as Exit from "effect/Exit" import * as FiberHandle from "effect/FiberHandle" import { dual } from "effect/Function" import * as Hash from "effect/Hash" import * as Layer from "effect/Layer" import * as Mailbox from "effect/Mailbox" import type { ReadonlyRecord } from "effect/Record" import * as Scope from "effect/Scope" import * as Stream from "effect/Stream" /** * @since 1.0.0 * @category tags */ export class Reactivity extends Context.Tag("@effect/experimental/Reactivity")< Reactivity, Reactivity.Service >() {} /** * @since 1.0.0 * @category constructors */ export const make = Effect.sync(() => { const handlers = new Map void>>() const unsafeInvalidate = (keys: ReadonlyArray | ReadonlyRecord>): void => { if (Array.isArray(keys)) { for (let i = 0; i < keys.length; i++) { const set = handlers.get(stringOrHash(keys[i])) if (set === undefined) continue for (const run of set) run() } } else { const record = keys as ReadonlyRecord> for (const key in record) { const hashes = idHashes(key, record[key]) for (let i = 0; i < hashes.length; i++) { const set = handlers.get(hashes[i]) if (set === undefined) continue for (const run of set) run() } const set = handlers.get(key) if (set !== undefined) { for (const run of set) run() } } } } const invalidate = ( keys: ReadonlyArray | ReadonlyRecord> ): Effect.Effect => Effect.sync(() => unsafeInvalidate(keys)) const mutation = ( keys: ReadonlyArray | ReadonlyRecord>, effect: Effect.Effect ): Effect.Effect => Effect.zipLeft(effect, invalidate(keys)) const unsafeRegister = ( keys: ReadonlyArray | ReadonlyRecord>, handler: () => void ): () => void => { const resolvedKeys = Array.isArray(keys) ? keys.map(stringOrHash) : recordHashes(keys as any) for (let i = 0; i < resolvedKeys.length; i++) { let set = handlers.get(resolvedKeys[i]) if (set === undefined) { set = new Set() handlers.set(resolvedKeys[i], set) } set.add(handler) } return () => { for (let i = 0; i < resolvedKeys.length; i++) { const set = handlers.get(resolvedKeys[i])! set.delete(handler) if (set.size === 0) { handlers.delete(resolvedKeys[i]) } } } } const query = ( keys: ReadonlyArray | ReadonlyRecord>, effect: Effect.Effect ): Effect.Effect, never, R | Scope.Scope> => Effect.gen(function*() { const scope = yield* Effect.scope const results = yield* Mailbox.make() const runFork = yield* FiberHandle.makeRuntime() let running = false let pending = false const handleExit = (exit: Exit.Exit) => { if (exit._tag === "Failure") { results.unsafeDone(Exit.failCause(exit.cause)) } else { results.unsafeOffer(exit.value) } if (pending) { pending = false runFork(effect).addObserver(handleExit) } else { running = false } } function run() { if (running) { pending = true return } running = true runFork(effect).addObserver(handleExit) } const cancel = unsafeRegister(keys, run) yield* Scope.addFinalizer(scope, Effect.sync(cancel)) run() return results as Mailbox.ReadonlyMailbox }) const stream = ( tables: ReadonlyArray | ReadonlyRecord>, effect: Effect.Effect ): Stream.Stream> => query(tables, effect).pipe( Effect.map(Mailbox.toStream), Stream.unwrapScoped ) return Reactivity.of({ mutation, query, stream, unsafeInvalidate, invalidate, unsafeRegister }) }) /** * @since 1.0.0 * @category accessors */ export const mutation: { /** * @since 1.0.0 * @category accessors */ ( keys: ReadonlyArray | ReadonlyRecord> ): (effect: Effect.Effect) => Effect.Effect /** * @since 1.0.0 * @category accessors */ ( effect: Effect.Effect, keys: ReadonlyArray | ReadonlyRecord> ): Effect.Effect } = dual(2, ( effect: Effect.Effect, keys: ReadonlyArray | ReadonlyRecord> ): Effect.Effect => Effect.flatMap(Reactivity, (r) => r.mutation(keys, effect))) /** * @since 1.0.0 * @category accessors */ export const query: { /** * @since 1.0.0 * @category accessors */ ( keys: ReadonlyArray | ReadonlyRecord> ): ( effect: Effect.Effect ) => Effect.Effect, never, R | Scope.Scope | Reactivity> /** * @since 1.0.0 * @category accessors */ ( effect: Effect.Effect, keys: ReadonlyArray | ReadonlyRecord> ): Effect.Effect, never, R | Scope.Scope | Reactivity> } = dual(2, ( effect: Effect.Effect, keys: ReadonlyArray | ReadonlyRecord> ): Effect.Effect, never, R | Scope.Scope | Reactivity> => Effect.flatMap(Reactivity, (r) => r.query(keys, effect))) /** * @since 1.0.0 * @category accessors */ export const stream: { /** * @since 1.0.0 * @category accessors */ ( keys: ReadonlyArray | ReadonlyRecord> ): (effect: Effect.Effect) => Stream.Stream | Reactivity> /** * @since 1.0.0 * @category accessors */ ( effect: Effect.Effect, keys: ReadonlyArray | ReadonlyRecord> ): Stream.Stream | Reactivity> } = dual(2, ( effect: Effect.Effect, keys: ReadonlyArray | ReadonlyRecord> ): Stream.Stream | Reactivity> => Reactivity.pipe( Effect.flatMap((r) => r.query(keys, effect)), Effect.map(Mailbox.toStream), Stream.unwrapScoped )) /** * @since 1.0.0 * @category accessors */ export const invalidate = ( keys: ReadonlyArray | ReadonlyRecord> ): Effect.Effect => Effect.flatMap(Reactivity, (r) => r.invalidate(keys)) /** * @since 1.0.0 * @category layers */ export const layer: Layer.Layer = Layer.scoped(Reactivity, make) /** * @since 1.0.0 * @category model */ export declare namespace Reactivity { /** * @since 1.0.0 * @category model */ export interface Service { readonly unsafeInvalidate: (keys: ReadonlyArray | ReadonlyRecord>) => void readonly unsafeRegister: ( keys: ReadonlyArray | ReadonlyRecord>, handler: () => void ) => () => void readonly invalidate: ( keys: ReadonlyArray | ReadonlyRecord> ) => Effect.Effect readonly mutation: ( keys: ReadonlyArray | ReadonlyRecord>, effect: Effect.Effect ) => Effect.Effect readonly query: ( keys: ReadonlyArray | ReadonlyRecord>, effect: Effect.Effect ) => Effect.Effect, never, R | Scope.Scope> readonly stream: ( keys: ReadonlyArray | ReadonlyRecord>, effect: Effect.Effect ) => Stream.Stream> } } function stringOrHash(u: unknown): string | number { return typeof u === "string" ? u : Hash.hash(u) } const idHashes = (keyHash: number | string, ids: ReadonlyArray): ReadonlyArray => { const hashes: Array = new Array(ids.length) for (let i = 0; i < ids.length; i++) { hashes[i] = `${keyHash}:${stringOrHash(ids[i])}` } return hashes } const recordHashes = (record: ReadonlyRecord>): ReadonlyArray => { const hashes: Array = [] for (const key in record) { hashes.push(key) for (const idHash of idHashes(key, record[key])) { hashes.push(idHash) } } return hashes }