@playfast/reform-resource 1.2.0 → 1.2.1

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.
@@ -1 +1 @@
1
- {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,QAAQ,EAIb,KAAK,KAAK,EAEX,MAAM,2BAA2B,CAAA;AAClC,OAAO,EAAS,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AAClF,OAAO,EAAyB,aAAa,EAAe,MAAM,iBAAiB,CAAA;AAEnF,KAAK,SAAS,CAAC,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAA;AAEvF,MAAM,WAAW,gBAAgB,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,CAAE,SAAQ,QAAQ;IACxE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;IACzB,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;IAChB,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,CAAA;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAA;CAC5C;AAED,MAAM,WAAW,aAAa,CAAC,GAAG,CAAC,CAAC,SAAS,MAAM,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAE,SAAQ,MAAM,CAAC,MAAM,CAC5F,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,EACnB,KAAK,EACL,KAAK,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAC3B;IACC,QAAQ,EAAE,CAAA;IACV,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;IAC5C,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;IACnF,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;CACjB;AAED,MAAM,WAAW,cAAc,CAAC,CAAC,EAAE,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,CAAA;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAA;CAC5C;AAED,eAAO,MAAM,IAAI,GAAI,KAAK,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,GAAG,KAAK,EACvD,MAAM,CAAC,EACP,QAAQ,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,KAC3B,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAUvB,CAAA;AAED,eAAO,MAAM,IAAI,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAC5C,UAAU,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAChC,SAAS,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,KAC5C,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CA4BtE,CAAA"}
1
+ {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,QAAQ,EAIb,KAAK,KAAK,EAEX,MAAM,2BAA2B,CAAA;AAClC,OAAO,EAEL,OAAO,EACP,MAAM,EAEN,KAAK,EACL,MAAM,EACN,KAAK,MAAM,EACX,KAAK,EACN,MAAM,QAAQ,CAAA;AACf,OAAO,EAAyB,aAAa,EAAe,MAAM,iBAAiB,CAAA;AAEnF,KAAK,SAAS,CAAC,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAA;AAEvF,MAAM,WAAW,gBAAgB,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,CAAE,SAAQ,QAAQ;IACxE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;IACzB,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;IAChB,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,CAAA;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAA;CAC5C;AAED,MAAM,WAAW,aAAa,CAAC,GAAG,CAAC,CAAC,SAAS,MAAM,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAE,SAAQ,MAAM,CAAC,MAAM,CAC5F,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,EACnB,KAAK,EACL,KAAK,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAC3B;IACC,QAAQ,EAAE,CAAA;IACV,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;IAC5C,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;IACnF,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;CACjB;AAED,MAAM,WAAW,cAAc,CAAC,CAAC,EAAE,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,CAAA;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAA;CAC5C;AAED,eAAO,MAAM,IAAI,GAAI,KAAK,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,GAAG,KAAK,EACvD,MAAM,CAAC,EACP,QAAQ,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,KAC3B,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAUvB,CAAA;AAED,eAAO,MAAM,IAAI,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAC5C,UAAU,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAChC,SAAS,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,KAC5C,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAgCtE,CAAA"}
package/dist/resource.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { makeStore, readTracked, resolveScheduler, yieldableClass, } from '@playfast/reform/internal';
2
- import { Cause, Context, Effect, Layer, Option, Scope } from 'effect';
2
+ import { Cause, Context, Effect, ExecutionStrategy, Layer, Option, Scope, } from 'effect';
3
3
  import { AsyncResource, narrowStore } from './asyncResource.js';
4
4
  export const make = (name, config) => {
5
5
  const store = Context.GenericTag(`reform/resource/${name}`);
@@ -17,9 +17,13 @@ export const live = (resource, acquire) => Layer.scoped(resource.store, Effect.g
17
17
  const scope = yield* Effect.scope;
18
18
  // Sync build seeds pending so layer mount never trips AsyncSceneLayer; store holds full union, tag sees narrowed arms.
19
19
  const store = makeStore(AsyncResource.pending, scheduler);
20
+ // Forked before the load fiber so it finalizes AFTER it: a scope releases in
21
+ // reverse order, so unmount interrupts the fiber that is still using the
22
+ // resource and only then releases the resource itself.
23
+ const loadScope = yield* Scope.fork(scope, ExecutionStrategy.sequential);
20
24
  yield* Effect.forkScoped(acquire.pipe(
21
- // Finalizers attach to scene scope, not the load fiber — resource lives until unmount.
22
- Scope.extend(scope), Effect.matchCauseEffect({
25
+ // Finalizers outlive the load fiber — the resource lives until unmount.
26
+ Scope.extend(loadScope), Effect.matchCauseEffect({
23
27
  onSuccess: (acquired) => Effect.sync(() => store.set(AsyncResource.ready(acquired))),
24
28
  onFailure: (cause) => Option.match(Cause.failureOption(cause), {
25
29
  onSome: (error) => Effect.sync(() => store.set(AsyncResource.failed(error))),
@@ -1 +1 @@
1
- {"version":3,"file":"resource.js","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,SAAS,EACT,WAAW,EACX,gBAAgB,EAEhB,cAAc,GACf,MAAM,2BAA2B,CAAA;AAClC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAe,KAAK,EAAE,MAAM,QAAQ,CAAA;AAClF,OAAO,EAAyB,aAAa,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AA2BnF,MAAM,CAAC,MAAM,IAAI,GAAG,CAClB,IAAO,EACP,MAA4B,EACJ,EAAE;IAC1B,MAAM,KAAK,GAAG,OAAO,CAAC,UAAU,CAA6B,mBAAmB,IAAI,EAAE,CAAC,CAAA;IACvF,MAAM,QAAQ,GAA8B;QAC1C,IAAI,EAAE,UAAU;QAChB,IAAI;QACJ,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,KAAK,EAAE,MAAM,CAAC,KAAK;KACpB,CAAA;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,WAAW,CAAC,CAAA;IAC/C,OAAO,cAAc,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;AACxD,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,IAAI,GAAG,CAClB,QAAgC,EAChC,OAA6C,EAC4B,EAAE,CAC3E,KAAK,CAAC,MAAM,CACV,QAAQ,CAAC,KAAK,EACd,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAClB,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,gBAAgB,CAAA;IACzC,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAA;IACjC,uHAAuH;IACvH,MAAM,KAAK,GAAG,SAAS,CAAyB,aAAa,CAAC,OAAO,EAAE,SAAS,CAAC,CAAA;IACjF,KAAK,CAAC,CAAC,MAAM,CAAC,UAAU,CACtB,OAAO,CAAC,IAAI;IACV,uFAAuF;IACvF,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,EACnB,MAAM,CAAC,gBAAgB,CAAC;QACtB,SAAS,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;QACpF,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE,CACnB,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE;YACvC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;YAC5E,MAAM,EAAE,GAAG,EAAE,CACX,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC;gBAC5B,CAAC,CAAC,MAAM,CAAC,IAAI;gBACb,CAAC,CAAC,oKAAoK;oBACpK,MAAM,CAAC,QAAQ,CAAC,8BAA8B,QAAQ,CAAC,IAAI,UAAU,EAAE,KAAK,CAAC;SACpF,CAAC;KACL,CAAC,CACH,CACF,CAAA;IACD,OAAO,WAAW,CAAC,KAAK,CAAC,CAAA;AAC3B,CAAC,CAAC,CACH,CAAA"}
1
+ {"version":3,"file":"resource.js","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,SAAS,EACT,WAAW,EACX,gBAAgB,EAEhB,cAAc,GACf,MAAM,2BAA2B,CAAA;AAClC,OAAO,EACL,KAAK,EACL,OAAO,EACP,MAAM,EACN,iBAAiB,EACjB,KAAK,EACL,MAAM,EAEN,KAAK,GACN,MAAM,QAAQ,CAAA;AACf,OAAO,EAAyB,aAAa,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AA2BnF,MAAM,CAAC,MAAM,IAAI,GAAG,CAClB,IAAO,EACP,MAA4B,EACJ,EAAE;IAC1B,MAAM,KAAK,GAAG,OAAO,CAAC,UAAU,CAA6B,mBAAmB,IAAI,EAAE,CAAC,CAAA;IACvF,MAAM,QAAQ,GAA8B;QAC1C,IAAI,EAAE,UAAU;QAChB,IAAI;QACJ,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,KAAK,EAAE,MAAM,CAAC,KAAK;KACpB,CAAA;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,WAAW,CAAC,CAAA;IAC/C,OAAO,cAAc,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;AACxD,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,IAAI,GAAG,CAClB,QAAgC,EAChC,OAA6C,EAC4B,EAAE,CAC3E,KAAK,CAAC,MAAM,CACV,QAAQ,CAAC,KAAK,EACd,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAClB,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,gBAAgB,CAAA;IACzC,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAA;IACjC,uHAAuH;IACvH,MAAM,KAAK,GAAG,SAAS,CAAyB,aAAa,CAAC,OAAO,EAAE,SAAS,CAAC,CAAA;IACjF,6EAA6E;IAC7E,yEAAyE;IACzE,uDAAuD;IACvD,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,iBAAiB,CAAC,UAAU,CAAC,CAAA;IACxE,KAAK,CAAC,CAAC,MAAM,CAAC,UAAU,CACtB,OAAO,CAAC,IAAI;IACV,wEAAwE;IACxE,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,EACvB,MAAM,CAAC,gBAAgB,CAAC;QACtB,SAAS,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;QACpF,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE,CACnB,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE;YACvC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;YAC5E,MAAM,EAAE,GAAG,EAAE,CACX,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC;gBAC5B,CAAC,CAAC,MAAM,CAAC,IAAI;gBACb,CAAC,CAAC,oKAAoK;oBACpK,MAAM,CAAC,QAAQ,CAAC,8BAA8B,QAAQ,CAAC,IAAI,UAAU,EAAE,KAAK,CAAC;SACpF,CAAC;KACL,CAAC,CACH,CACF,CAAA;IACD,OAAO,WAAW,CAAC,KAAK,CAAC,CAAA;AAC3B,CAAC,CAAC,CACH,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playfast/reform-resource",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Async resources for Reform — a layer that builds synchronously, loads in the background, and flips a reactive store from pending to ready/failed. The sanctioned home for async work that would otherwise break reform's synchronous scene build.",
5
5
  "keywords": [
6
6
  "async",
@@ -0,0 +1,93 @@
1
+ import { expect, it } from '@effect/vitest'
2
+ import { makeFlightGate, until } from '@playfast/reform/testkit/flight.testkit'
3
+ import { Deferred, Effect, Exit, Fiber, Layer, Option, Schema as S, Scope } from 'effect'
4
+ import { Resource } from './index'
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // `Resource.live` runs `acquire` on a forked fiber but `Scope.extend`s its
8
+ // finalizers onto the *layer* scope (resource.ts:63-66) so the resource outlives
9
+ // the load. Both the fork's interrupt and the acquire's release therefore live on
10
+ // the same scope — and a scope releases finalizers in reverse registration order.
11
+ // The fork is registered first (so it is interrupted LAST) and the release
12
+ // second (so it runs FIRST): on unmount the resource is closed while the fiber
13
+ // that opened it, and is still using it, is left running.
14
+ //
15
+ // It only shows once the close is genuinely asynchronous — which every real one
16
+ // is (a socket, a worker, a wasm db, an http client).
17
+ // ---------------------------------------------------------------------------
18
+
19
+ it.live('a resource is released while its own load fiber is still using it', () => {
20
+ class Conn extends Resource.make('HuntConn', { output: S.Number, error: Option.none() }) {}
21
+
22
+ const handle = { open: true }
23
+ const marks = { queries: 0, queriesAfterClose: 0 }
24
+ const query = () => {
25
+ marks.queries += 1
26
+ if (!handle.open) {
27
+ marks.queriesAfterClose += 1
28
+ }
29
+ }
30
+ const order: Array<string> = []
31
+
32
+ // the load fiber's next unit of work, and the parked half of an async close
33
+ const nextStep = Effect.runSync(Deferred.make<void>())
34
+ const closeFinishes = Effect.runSync(Deferred.make<void>())
35
+ const loaded = makeFlightGate()
36
+
37
+ const layer = Resource.live(
38
+ Conn,
39
+ Effect.gen(function* () {
40
+ yield* Effect.acquireRelease(Effect.succeed(handle), () =>
41
+ Effect.sync(() => {
42
+ order.push('release-start')
43
+ handle.open = false
44
+ }).pipe(
45
+ // a real close is asynchronous; the fiber that opened the connection
46
+ // is still schedulable for the whole of it
47
+ Effect.zipRight(Deferred.await(closeFinishes)),
48
+ Effect.zipRight(Effect.sync(() => order.push('release-end'))),
49
+ ),
50
+ )
51
+ query()
52
+ yield* loaded.through(Effect.void)
53
+ yield* Deferred.await(nextStep).pipe(
54
+ Effect.onInterrupt(() => Effect.sync(() => order.push('load-interrupted'))),
55
+ )
56
+ query()
57
+ return 1
58
+ }),
59
+ )
60
+
61
+ return Effect.gen(function* () {
62
+ const scope = yield* Scope.make()
63
+ yield* Scope.extend(Layer.build(layer), scope)
64
+ yield* loaded.awaitEntry(1)
65
+
66
+ // CONTROL: while the scene is mounted the load fiber uses a live connection
67
+ expect(marks.queries).toBe(1)
68
+ expect(marks.queriesAfterClose).toBe(0)
69
+ expect(handle.open).toBe(true)
70
+
71
+ // unmount
72
+ const closing = yield* Effect.fork(Scope.close(scope, Exit.void))
73
+ yield* until(
74
+ () => handle.open,
75
+ (open) => open === false,
76
+ )
77
+
78
+ // the connection is closed; hand the load fiber its next unit of work
79
+ yield* Deferred.succeed(nextStep, undefined)
80
+ yield* until(
81
+ () => marks.queries,
82
+ (queries) => queries > 1,
83
+ 20,
84
+ )
85
+
86
+ yield* Deferred.succeed(closeFinishes, undefined)
87
+ yield* Fiber.join(closing)
88
+
89
+ // the load fiber must be stopped before the resource it opened is released
90
+ expect(marks.queriesAfterClose).toBe(0)
91
+ expect(order[0]).toBe('load-interrupted')
92
+ })
93
+ })
package/src/resource.ts CHANGED
@@ -6,7 +6,16 @@ import {
6
6
  type Store,
7
7
  yieldableClass,
8
8
  } from '@playfast/reform/internal'
9
- import { Cause, Context, Effect, Layer, Option, type Schema, Scope } from 'effect'
9
+ import {
10
+ Cause,
11
+ Context,
12
+ Effect,
13
+ ExecutionStrategy,
14
+ Layer,
15
+ Option,
16
+ type Schema,
17
+ Scope,
18
+ } from 'effect'
10
19
  import { type AnyAsyncResource, AsyncResource, narrowStore } from './asyncResource'
11
20
 
12
21
  type AnySchema<A> = Schema.Schema<A, Schema.Schema.Encoded<Schema.Schema.AnyNoContext>>
@@ -60,10 +69,14 @@ export const live = <N extends string, A, E, R>(
60
69
  const scope = yield* Effect.scope
61
70
  // Sync build seeds pending so layer mount never trips AsyncSceneLayer; store holds full union, tag sees narrowed arms.
62
71
  const store = makeStore<AnyAsyncResource<A, E>>(AsyncResource.pending, scheduler)
72
+ // Forked before the load fiber so it finalizes AFTER it: a scope releases in
73
+ // reverse order, so unmount interrupts the fiber that is still using the
74
+ // resource and only then releases the resource itself.
75
+ const loadScope = yield* Scope.fork(scope, ExecutionStrategy.sequential)
63
76
  yield* Effect.forkScoped(
64
77
  acquire.pipe(
65
- // Finalizers attach to scene scope, not the load fiber — resource lives until unmount.
66
- Scope.extend(scope),
78
+ // Finalizers outlive the load fiber — the resource lives until unmount.
79
+ Scope.extend(loadScope),
67
80
  Effect.matchCauseEffect({
68
81
  onSuccess: (acquired) => Effect.sync(() => store.set(AsyncResource.ready(acquired))),
69
82
  onFailure: (cause) =>