@playfast/reform-resource 0.0.6 → 1.0.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.
- package/README.md +10 -12
- package/package.json +1 -1
- package/src/asyncResource.ts +2 -1
- package/src/reform-resource.test.ts +5 -5
- package/src/resource.ts +8 -7
package/README.md
CHANGED
|
@@ -22,10 +22,14 @@ a one-shot, scoped load.
|
|
|
22
22
|
|
|
23
23
|
```typescript
|
|
24
24
|
import { Resource } from '@playfast/reform-resource'
|
|
25
|
-
import { Effect, Schema as S } from 'effect'
|
|
25
|
+
import { Effect, Option, Schema as S } from 'effect'
|
|
26
26
|
|
|
27
|
-
// Define — output
|
|
28
|
-
|
|
27
|
+
// Define — `output` shapes the ready value; `error` is required (`Option.none()`
|
|
28
|
+
// for an infallible load, `Option.some(schema)` to add a `failed` arm).
|
|
29
|
+
class Config extends Resource.make('Config', {
|
|
30
|
+
output: S.Struct({ url: S.String }),
|
|
31
|
+
error: Option.none(),
|
|
32
|
+
}) {}
|
|
29
33
|
|
|
30
34
|
// Wire — `acquire` may be scoped (acquire/release tied to the scene scope).
|
|
31
35
|
const ConfigLive = Resource.live(
|
|
@@ -40,13 +44,7 @@ const view = Effect.gen(function* () {
|
|
|
40
44
|
})
|
|
41
45
|
```
|
|
42
46
|
|
|
43
|
-
The value is a union discriminated by `isReady
|
|
47
|
+
The value is a union discriminated by `isReady` (`pending | ready`, plus a `failed`
|
|
48
|
+
arm only when an `error` schema is declared).
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
type AsyncResource<A, E = never> =
|
|
47
|
-
| { _tag: 'AsyncResource'; isReady: false; failed: false } // pending
|
|
48
|
-
| { _tag: 'AsyncResource'; isReady: true; value: A } // ready
|
|
49
|
-
| { _tag: 'AsyncResource'; isReady: false; failed: true; error: E } // only when E ≠ never
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
The public API is re-exported from [the package index](./src/index.ts).
|
|
50
|
+
See the [playbook](./playbook/api.doc.md) for the full API and `AsyncResource` type.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playfast/reform-resource",
|
|
3
3
|
"playbook": "./playbook",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "1.0.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"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.",
|
|
7
7
|
"keywords": [
|
package/src/asyncResource.ts
CHANGED
|
@@ -48,7 +48,7 @@ export type AsyncResource<A, E = never> =
|
|
|
48
48
|
export type AnyAsyncResource<A, E> = AsyncResourcePending | AsyncResourceReady<A> | AsyncResourceFailed<E>
|
|
49
49
|
|
|
50
50
|
const pending: AsyncResourcePending = { _tag: 'AsyncResource', isReady: false, failed: false }
|
|
51
|
-
const ready = <A>(
|
|
51
|
+
const ready = <A>(acquired: A): AsyncResourceReady<A> => ({ _tag: 'AsyncResource', isReady: true, value: acquired })
|
|
52
52
|
const failed = <E>(error: E): AsyncResourceFailed<E> => ({
|
|
53
53
|
_tag: 'AsyncResource',
|
|
54
54
|
isReady: false,
|
|
@@ -77,4 +77,5 @@ export const AsyncResource: AsyncResourceConstructors = { pending, ready, failed
|
|
|
77
77
|
* inline cast — the established `asyncData.narrowStore` pattern.
|
|
78
78
|
*/
|
|
79
79
|
export const narrowStore = <A, E>(store: Store<AnyAsyncResource<A, E>>): Store<AsyncResource<A, E>> =>
|
|
80
|
+
// oxlint-disable-next-line reform-rules/no-type-assertion -- sanctioned variance seam: the live store holds the full union; this is the one documented home for narrowing it to the definition-permitted arms (mirrors asyncData.narrowStore).
|
|
80
81
|
store as unknown as Store<AsyncResource<A, E>>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { expect, it } from '@effect/vitest'
|
|
2
|
-
import { Context, Data, Duration, Effect, Layer, Ref, Schema as S } from 'effect'
|
|
2
|
+
import { Context, Data, Duration, Effect, Layer, Option, Ref, Schema as S } from 'effect'
|
|
3
3
|
import { Resource } from './index'
|
|
4
4
|
|
|
5
5
|
// A Resource builds synchronously as `pending`, forks its load under the scene
|
|
@@ -11,7 +11,7 @@ const tick = (ms = 30) => Effect.sleep(Duration.millis(ms))
|
|
|
11
11
|
class Boom extends Data.TaggedError('Boom')<{ readonly message: string }> {}
|
|
12
12
|
|
|
13
13
|
it.live('builds pending then resolves to ready', () => {
|
|
14
|
-
class Num extends Resource.make('Num', { output: S.Number }) {}
|
|
14
|
+
class Num extends Resource.make('Num', { output: S.Number, error: Option.none() }) {}
|
|
15
15
|
const layer = Resource.live(Num, Effect.delay(Effect.succeed(7), Duration.millis(20)))
|
|
16
16
|
|
|
17
17
|
return Effect.gen(function* () {
|
|
@@ -26,7 +26,7 @@ it.live('builds pending then resolves to ready', () => {
|
|
|
26
26
|
})
|
|
27
27
|
|
|
28
28
|
it.live('a failing load resolves to the failed arm carrying the error', () => {
|
|
29
|
-
class Conn extends Resource.make('Conn', { output: S.Number, error: S.instanceOf(Boom) }) {}
|
|
29
|
+
class Conn extends Resource.make('Conn', { output: S.Number, error: Option.some(S.instanceOf(Boom)) }) {}
|
|
30
30
|
const layer = Resource.live(Conn, Effect.fail(new Boom({ message: 'nope' })))
|
|
31
31
|
|
|
32
32
|
return Effect.gen(function* () {
|
|
@@ -39,7 +39,7 @@ it.live('a failing load resolves to the failed arm carrying the error', () => {
|
|
|
39
39
|
})
|
|
40
40
|
|
|
41
41
|
it.live('notifies subscribers when it flips to ready', () => {
|
|
42
|
-
class Num extends Resource.make('Num', { output: S.Number }) {}
|
|
42
|
+
class Num extends Resource.make('Num', { output: S.Number, error: Option.none() }) {}
|
|
43
43
|
const layer = Resource.live(Num, Effect.succeed(1))
|
|
44
44
|
|
|
45
45
|
return Effect.gen(function* () {
|
|
@@ -58,7 +58,7 @@ it.live('notifies subscribers when it flips to ready', () => {
|
|
|
58
58
|
it.live('ties acquire/release to the scene scope', () =>
|
|
59
59
|
Effect.gen(function* () {
|
|
60
60
|
const released = yield* Ref.make(false)
|
|
61
|
-
class Conn extends Resource.make('Conn', { output: S.Number }) {}
|
|
61
|
+
class Conn extends Resource.make('Conn', { output: S.Number, error: Option.none() }) {}
|
|
62
62
|
const layer = Resource.live(
|
|
63
63
|
Conn,
|
|
64
64
|
Effect.acquireRelease(Effect.succeed(1), () => Ref.set(released, true)),
|
package/src/resource.ts
CHANGED
|
@@ -25,8 +25,8 @@ export interface ResourceManifest<N extends string, A, E> extends Manifest {
|
|
|
25
25
|
readonly kind: 'Resource'
|
|
26
26
|
readonly name: N
|
|
27
27
|
readonly output: Schema.Schema<A, any>
|
|
28
|
-
/** Schema of the failure.
|
|
29
|
-
readonly error
|
|
28
|
+
/** Schema of the failure. `None` ⇒ the load is infallible and there is no `Failed` arm. */
|
|
29
|
+
readonly error: Option.Option<Schema.Schema<E, any>>
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export interface ResourceClass<out N extends string, in out A, in out E>
|
|
@@ -41,8 +41,8 @@ export interface ResourceClass<out N extends string, in out A, in out E>
|
|
|
41
41
|
export interface ResourceConfig<A, E> {
|
|
42
42
|
/** Schema of the ready value. */
|
|
43
43
|
readonly output: Schema.Schema<A, any>
|
|
44
|
-
/** Schema of the failure.
|
|
45
|
-
readonly error
|
|
44
|
+
/** Schema of the failure. `None` ⇒ infallible: no `Failed` arm in the value type. */
|
|
45
|
+
readonly error: Option.Option<Schema.Schema<E, any>>
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
@@ -59,7 +59,7 @@ export const make = <const N extends string, A, E = never>(
|
|
|
59
59
|
kind: 'Resource',
|
|
60
60
|
name,
|
|
61
61
|
output: config.output,
|
|
62
|
-
|
|
62
|
+
error: config.error,
|
|
63
63
|
}
|
|
64
64
|
const read = Effect.flatMap(store, readTracked)
|
|
65
65
|
return yieldableClass(read, { manifest, store, name })
|
|
@@ -93,14 +93,15 @@ export const live = <N extends string, A, E, R>(
|
|
|
93
93
|
// a resource opened here lives until the scene unmounts.
|
|
94
94
|
Scope.extend(scope),
|
|
95
95
|
Effect.matchCauseEffect({
|
|
96
|
-
onSuccess: (
|
|
96
|
+
onSuccess: (acquired) => Effect.sync(() => store.set(AsyncResource.ready(acquired))),
|
|
97
97
|
onFailure: (cause) =>
|
|
98
98
|
Option.match(Cause.failureOption(cause), {
|
|
99
99
|
onSome: (error) => Effect.sync(() => store.set(AsyncResource.failed(error))),
|
|
100
100
|
onNone: () =>
|
|
101
101
|
Cause.isInterruptedOnly(cause)
|
|
102
102
|
? Effect.void
|
|
103
|
-
:
|
|
103
|
+
: // oxlint-disable-next-line reform-rules/no-effect-log -- reform-resource has no @play/effect-utils dep (same as reform core); the package logs via Effect directly.
|
|
104
|
+
Effect.logError(`reform-resource: load for '${resource.name}' failed`, cause),
|
|
104
105
|
}),
|
|
105
106
|
}),
|
|
106
107
|
),
|