diff --git a/.changeset/reset-function-idle-ttl.md b/.changeset/reset-function-idle-ttl.md new file mode 100644 index 00000000..821dd57e --- /dev/null +++ b/.changeset/reset-function-idle-ttl.md @@ -0,0 +1,5 @@ +--- +"@effect-atom/atom": patch +--- + +Ensure atom ttl is 0 for internal intermediate state atoms. diff --git a/packages/atom/src/Atom.ts b/packages/atom/src/Atom.ts index 2b765b4d..d1ee7a37 100644 --- a/packages/atom/src/Atom.ts +++ b/packages/atom/src/Atom.ts @@ -167,6 +167,28 @@ export interface WriteContext { set(this: WriteContext, atom: Writable, value: W): void } +/** + * @since 1.0.0 + * @category combinators + */ +export const setIdleTTL: { + (duration: Duration.DurationInput): >(self: A) => A + >(self: A, duration: Duration.DurationInput): A +} = dual< + (duration: Duration.DurationInput) => >(self: A) => A, + >(self: A, duration: Duration.DurationInput) => A +>(2, (self, durationInput) => { + const duration = Duration.decode(durationInput) + const isFinite = Duration.isFinite(duration) + return Object.assign(Object.create(Object.getPrototypeOf(self)), { + ...self, + keepAlive: !isFinite, + idleTTL: isFinite ? Duration.toMillis(duration) : undefined + }) +}) + +const removeTtl = setIdleTTL(0) + const AtomProto = { [TypeId]: TypeId, pipe() { @@ -217,7 +239,7 @@ const RuntimeProto = { readonly disableAccumulation?: boolean readonly initialValue?: ReadonlyArray }) { - const pullSignal = state(0) + const pullSignal = removeTtl(state(0)) const pullAtom = readable((get) => { const previous = get.self>() const runtimeResult = get(this) @@ -236,7 +258,7 @@ const RuntimeProto = { subscriptionRef(this: AtomRuntime, ref: any) { return makeSubRef( - readable((get) => { + removeTtl(readable((get) => { const previous = get.self>() const runtimeResult = get(this) if (runtimeResult._tag !== "Success") { @@ -246,7 +268,7 @@ const RuntimeProto = { return SubscriptionRef.SubscriptionRefTypeId in value ? value : makeEffect(get, value, Result.initial(true), runtimeResult.value) - }), + })), (get, ref) => { const runtime = Result.getOrThrow(get(this)) return readSubscribable(get, ref, runtime) @@ -677,11 +699,11 @@ export const context: (options: { factory.addGlobalLayer = (layer: Layer.Layer) => { globalLayer = Layer.provideMerge(globalLayer, Layer.provide(layer, Reactivity.layer)) } - const reactivityAtom = make( + const reactivityAtom = removeTtl(make( Effect.scopeWith((scope) => Layer.buildWithMemoMap(Reactivity.layer, options.memoMap, scope)).pipe( Effect.map(EffectContext.get(Reactivity.Reactivity)) ) - ) + )) factory.withReactivity = (keys: ReadonlyArray | ReadonlyRecord>) => >(atom: A): A => @@ -1007,7 +1029,7 @@ export const fnSync: { const makeFnSync = (f: (arg: Arg, get: FnContext) => A, options?: { readonly initialValue?: A }): Writable | A, Arg> => { - const argAtom = state<[number, Arg]>([0, undefined as any]) + const argAtom = removeTtl(state<[number, Arg]>([0, undefined as any])) const hasInitialValue = options?.initialValue !== undefined return writable(function(get) { ;(get as any).isFn = true @@ -1100,16 +1122,16 @@ function makeResultFn( readonly concurrent?: boolean | undefined } ) { - const argAtom = state<[number, Arg | Interrupt]>([0, undefined as any]) + const argAtom = removeTtl(state<[number, Arg | Interrupt]>([0, undefined as any])) const initialValue = options?.initialValue !== undefined ? Result.success(options.initialValue) : Result.initial() const fibersAtom = options?.concurrent - ? make((get) => { + ? removeTtl(readable((get) => { const fibers = new Set>() get.addFinalizer(() => fibers.forEach((f) => f.unsafeInterruptAsFork(FiberId.none))) return fibers - }) + })) : undefined function read(get: Context, runtime?: Runtime.Runtime): Result.Result { @@ -1202,12 +1224,10 @@ export const pull = ( readonly disableAccumulation?: boolean | undefined } ): Writable, void> => { - const pullSignal = state(0) - const pullAtom = readable( - makeRead(function(get) { - return makeStreamPullEffect(get, pullSignal, create, options) - }) - ) + const pullSignal = removeTtl(state(0)) + const pullAtom = readable(makeRead(function(get) { + return makeStreamPullEffect(get, pullSignal, create, options) + })) return makeStreamPull(pullSignal, pullAtom) } @@ -1452,26 +1472,6 @@ export const withLabel: { label: [name, new Error().stack?.split("\n")[5] ?? ""] })) -/** - * @since 1.0.0 - * @category combinators - */ -export const setIdleTTL: { - (duration: Duration.DurationInput): >(self: A) => A - >(self: A, duration: Duration.DurationInput): A -} = dual< - (duration: Duration.DurationInput) => >(self: A) => A, - >(self: A, duration: Duration.DurationInput) => A ->(2, (self, durationInput) => { - const duration = Duration.decode(durationInput) - const isFinite = Duration.isFinite(duration) - return Object.assign(Object.create(Object.getPrototypeOf(self)), { - ...self, - keepAlive: !isFinite, - idleTTL: isFinite ? Duration.toMillis(duration) : undefined - }) -}) - /** * @since 1.0.0 * @category combinators @@ -1593,12 +1593,12 @@ export const debounce: { */ export const optimistic = (self: Atom): Writable>> => { let counter = 0 - const writeAtom = state( + const writeAtom = removeTtl(state( [ counter, undefined as any as Atom> ] as const - ) + )) return writable( (get) => { let lastValue = get.once(self) @@ -1714,7 +1714,7 @@ export const optimisticFn: { | ((set: (result: NoInfer) => void) => AtomResultFn) } ): AtomResultFn => { - const transition = state>(Result.initial()) + const transition = removeTtl(state>(Result.initial())) return fn((arg: OW, get) => { let value = options.reducer(get(self), arg) if (Result.isResult(value)) { diff --git a/packages/atom/test/Atom.test.ts b/packages/atom/test/Atom.test.ts index a70a4868..afc925b7 100644 --- a/packages/atom/test/Atom.test.ts +++ b/packages/atom/test/Atom.test.ts @@ -748,6 +748,46 @@ describe("Atom", () => { expect(r.get(state3)).toEqual(0) }) + it("idleTTL fn", async () => { + const fn = Atom.fn((n: number) => Effect.succeed(n + 1)).pipe( + Atom.setIdleTTL(0) + ) + const r = Registry.make({ defaultIdleTTL: 2000 }) + + let result = r.get(fn) + assert(Result.isInitial(result)) + + r.set(fn, 1) + result = r.get(fn) + assert(Result.isSuccess(result)) + expect(result.value).toEqual(2) + + await new Promise((resolve) => resolve(null)) + + result = r.get(fn) + assert(Result.isInitial(result)) + }) + + it("idleTTL fnSync", async () => { + const fn = Atom.fnSync((n: number) => n + 1).pipe( + Atom.setIdleTTL(0) + ) + const r = Registry.make({ defaultIdleTTL: 2000 }) + + let result = r.get(fn) + assert(Option.isNone(result)) + + r.set(fn, 1) + result = r.get(fn) + assert(Option.isSome(result)) + expect(result.value).toEqual(2) + + await new Promise((resolve) => resolve(null)) + + result = r.get(fn) + assert(Option.isNone(result)) + }) + it("fn", async () => { const count = Atom.fnSync((n: number) => n).pipe(Atom.keepAlive) const r = Registry.make()