effectweb 0.3.1 → 0.5.0

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.
Files changed (66) hide show
  1. package/dist/AsyncContent.d.ts +1 -1
  2. package/dist/AsyncContent.js +18 -35
  3. package/dist/cache-internals.d.ts +3 -2
  4. package/dist/cache.d.ts +15 -12
  5. package/dist/cache.js +51 -10
  6. package/dist/collection.d.ts +1 -1
  7. package/dist/collection.js +1 -3
  8. package/dist/component.d.ts +1 -1
  9. package/dist/component.js +9 -4
  10. package/dist/dom.d.ts +64 -31
  11. package/dist/dom.js +237 -121
  12. package/dist/effectEvent.d.ts +10 -7
  13. package/dist/effectEvent.js +50 -46
  14. package/dist/form.d.ts +2 -1
  15. package/dist/http.d.ts +2 -1
  16. package/dist/http.js +1 -1
  17. package/dist/index.d.ts +10 -8
  18. package/dist/index.js +10 -8
  19. package/dist/infinite-query.d.ts +12 -8
  20. package/dist/infinite-query.js +21 -13
  21. package/dist/jsx-runtime.d.ts +6 -0
  22. package/dist/jsx-runtime.js +14 -1
  23. package/dist/jsx.d.ts +4 -1
  24. package/dist/keyed-tasks.d.ts +8 -6
  25. package/dist/keyed-tasks.js +29 -24
  26. package/dist/lazy.d.ts +4 -2
  27. package/dist/lazy.js +16 -10
  28. package/dist/load.d.ts +1 -1
  29. package/dist/load.js +1 -1
  30. package/dist/mount.d.ts +23 -8
  31. package/dist/mount.js +116 -27
  32. package/dist/owner.d.ts +13 -8
  33. package/dist/owner.js +37 -41
  34. package/dist/pages.d.ts +1 -1
  35. package/dist/pages.js +2 -1
  36. package/dist/program.d.ts +20 -8
  37. package/dist/program.js +44 -22
  38. package/dist/query-internals.d.ts +5 -3
  39. package/dist/query.d.ts +17 -6
  40. package/dist/query.js +6 -3
  41. package/dist/render.d.ts +17 -0
  42. package/dist/render.js +48 -0
  43. package/dist/resource.d.ts +4 -3
  44. package/dist/resource.js +12 -6
  45. package/dist/runtime.d.ts +12 -1
  46. package/dist/runtime.js +27 -5
  47. package/dist/session.d.ts +3 -2
  48. package/dist/session.js +15 -5
  49. package/dist/settlement.d.ts +8 -1
  50. package/dist/settlement.js +20 -8
  51. package/dist/snapshot.d.ts +1 -1
  52. package/dist/snapshot.js +10 -12
  53. package/dist/source.d.ts +38 -0
  54. package/dist/source.js +218 -0
  55. package/dist/task.d.ts +1 -1
  56. package/dist/task.js +3 -1
  57. package/dist/tasks.d.ts +9 -6
  58. package/dist/tasks.js +7 -4
  59. package/dist/testing.d.ts +5 -4
  60. package/dist/testing.js +4 -2
  61. package/package.json +10 -3
  62. package/README.md +0 -101
  63. package/dist/intrinsic.d.ts +0 -2
  64. package/dist/intrinsic.js +0 -120
  65. package/dist/slotIdentity.d.ts +0 -1
  66. package/dist/slotIdentity.js +0 -2
package/dist/query.d.ts CHANGED
@@ -1,5 +1,7 @@
1
- import type { Effect } from 'effect';
1
+ import type * as Effect from 'effect/Effect';
2
+ import type * as Scope from 'effect/Scope';
2
3
  import { type QueryDefinition } from './query-internals.js';
4
+ import type { Snapshot } from './snapshot.js';
3
5
  /** Canonical data identity: plain objects, dense arrays, and finite scalar values. */
4
6
  export type QueryKey = string | number | boolean | null | undefined | readonly QueryKey[] | {
5
7
  readonly [key: string]: QueryKey;
@@ -14,18 +16,27 @@ declare const queryType: unique symbol;
14
16
  /** An opaque typed definition. Every request argument participates in cache identity. */
15
17
  export interface Query<Args, A, E = never, R = never> {
16
18
  readonly name: string;
17
- readonly [queryType]: (args: Args) => Effect.Effect<A, E, R>;
19
+ readonly [queryType]: (args: Args) => Effect.Effect<A, E, R | Scope.Scope>;
18
20
  }
19
21
  type Definition<Args, A, E, R> = Omit<QueryDefinition<Args, A, E, R>, 'staleTime'> & {
20
22
  readonly staleTime?: number;
21
23
  /** Custom projections can silently reuse a different request's result. */
22
24
  readonly key?: never;
23
25
  };
24
- export declare function query<Args, A, E = never, R = never>(definition: Definition<Args, A, E, R> & (Args extends QueryArgs<Args> ? unknown : {
25
- readonly nonSerializableQueryArguments: never;
26
- })): Query<Args, A, E, R>;
26
+ /** Encode the complete argument value explicitly when its representation is not plain data. */
27
+ export type QueryEncoding<Args> = [Args] extends [QueryArgs<Args>] ? {
28
+ readonly encode?: (args: Snapshot<Args>) => QueryKey;
29
+ } : {
30
+ readonly encode: (args: Snapshot<Args>) => QueryKey;
31
+ };
32
+ /** Shared identity contract for lookup, selection, writes, invalidation and pagination. */
33
+ export declare function encodeQueryArguments<Args, A, E, R>(definition: Query<Args, A, E, R>, args: Args | Snapshot<Args>): string;
34
+ export declare function query<Args, A, E = never, R = never>(definition: Definition<Args, A, E, R> & {
35
+ readonly encode: (args: Snapshot<Args>) => QueryKey;
36
+ }): Query<Args, A, E, R>;
37
+ export declare function query<Args, A, E = never, R = never>(definition: Definition<Args, A, E, R> & QueryEncoding<Args>): Query<Args, A, E, R>;
27
38
  export declare function query<A, E = never, R = never>(definition: Definition<true, A, E, R> & {
28
- readonly load: () => Effect.Effect<A, E, R>;
39
+ readonly load: () => Effect.Effect<A, E, R | Scope.Scope>;
29
40
  }): Query<true, A, E, R>;
30
41
  declare const groupType: unique symbol;
31
42
  export type QueryGroup = symbol & {
package/dist/query.js CHANGED
@@ -1,4 +1,4 @@
1
- import { registerQuery } from './query-internals.js';
1
+ import { registerQuery, queryDefinition } from './query-internals.js';
2
2
  /** Shared by cache lookup and resource reconciliation. Object property order is irrelevant. */
3
3
  export function encodeQueryKey(key) {
4
4
  const ancestors = new Set();
@@ -53,9 +53,12 @@ export function encodeQueryKey(key) {
53
53
  };
54
54
  return encode(key);
55
55
  }
56
+ /** Shared identity contract for lookup, selection, writes, invalidation and pagination. */
57
+ export function encodeQueryArguments(definition, args) {
58
+ const config = queryDefinition(definition);
59
+ return encodeQueryKey(config.encode ? config.encode(args) : args);
60
+ }
56
61
  export function query(definition) {
57
- if ('key' in definition)
58
- throw new TypeError('Query identity includes all arguments. Remove key and provide services through the Effect environment.');
59
62
  const staleTime = definition.staleTime ?? Infinity;
60
63
  if (Number.isNaN(staleTime) || staleTime < 0)
61
64
  throw new RangeError('Query staleTime must be nonnegative.');
@@ -0,0 +1,17 @@
1
+ import * as Effect from 'effect/Effect';
2
+ import * as Scope from 'effect/Scope';
3
+ import { type Mounted, type View } from './dom.js';
4
+ import type { Source } from './source.js';
5
+ import type { Send } from './program.js';
6
+ import { type ReportError } from './errors.js';
7
+ /**
8
+ * Mount values or Effect-based view setup in a child resource scope.
9
+ * The surrounding Effect owns the mount; the source keeps its own lifetime.
10
+ */
11
+ export declare function mount<M, Message = never, E = never, R = never>(parent: Node, definition: View<M, Message> | Effect.Effect<View<M, Message>, E, R>, source: Source<M> & ([Message] extends [never] ? {
12
+ readonly send?: Send<Message>;
13
+ } : {
14
+ readonly send: Send<Message>;
15
+ }), options?: {
16
+ readonly onError?: ReportError;
17
+ }): Effect.Effect<Mounted, E, R | Scope.Scope>;
package/dist/render.js ADDED
@@ -0,0 +1,48 @@
1
+ import * as Effect from 'effect/Effect';
2
+ import * as Exit from 'effect/Exit';
3
+ import * as Scope from 'effect/Scope';
4
+ import { mountViewWithSettlement, unboundSend } from './dom.js';
5
+ import { makeUiRuntime } from './runtime.js';
6
+ import { reportError, reportSafely } from './errors.js';
7
+ import { Settlement } from './settlement.js';
8
+ /**
9
+ * Mount values or Effect-based view setup in a child resource scope.
10
+ * The surrounding Effect owns the mount; the source keeps its own lifetime.
11
+ */
12
+ export function mount(parent, definition, source, options = {}) {
13
+ return Effect.gen(function* () {
14
+ const lifetime = yield* Effect.acquireRelease(Effect.sync(() => {
15
+ const scope = Scope.makeUnsafe();
16
+ let closingExit;
17
+ const closed = Effect.runSync(Effect.cached(Effect.uninterruptible(Effect.suspend(() => Scope.close(scope, closingExit ?? Exit.void)))));
18
+ const beginClose = (exit) => {
19
+ closingExit ??= exit;
20
+ return closed;
21
+ };
22
+ const close = (exit) => Effect.suspend(() => beginClose(exit));
23
+ return { scope, close, beginClose };
24
+ }), (lifetime, exit) => lifetime.close(exit));
25
+ return yield* Effect.gen(function* () {
26
+ const runtime = yield* makeUiRuntime();
27
+ const view = Effect.isEffect(definition) ? yield* definition : definition;
28
+ const settlement = new Settlement(runtime);
29
+ // Acquisition can throw after starting child cleanup, before a Mounted exists.
30
+ yield* Effect.addFinalizer(() => settlement.wait());
31
+ const mounted = yield* Effect.acquireRelease(Effect.sync(() => mountViewWithSettlement(parent, view, source, { ...options, send: source.send ?? unboundSend }, settlement)), (mounted, exit) => Effect.suspend(() => {
32
+ settlement.exit = exit;
33
+ return mounted.close();
34
+ }));
35
+ const close = lifetime.close(Exit.void);
36
+ const dispose = () => {
37
+ // Claim the exit before user unsubscriptions can reenter application shutdown.
38
+ const closed = lifetime.beginClose(Exit.void);
39
+ mounted.dispose();
40
+ Effect.runFork(closed).addObserver((exit) => {
41
+ if (exit._tag === 'Failure')
42
+ reportSafely(options.onError ?? reportError, exit.cause);
43
+ });
44
+ };
45
+ return Object.assign(dispose, { dispose, close: () => close });
46
+ }).pipe(Scope.provide(lifetime.scope), Effect.onError((cause) => lifetime.close(Exit.failCause(cause))));
47
+ });
48
+ }
@@ -1,7 +1,8 @@
1
1
  import type { Snapshot } from './snapshot.js';
2
+ import type * as Scope from 'effect/Scope';
2
3
  import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
3
4
  import type { UiLoad } from './load.js';
4
- import { type UiRuntime } from './runtime.js';
5
+ import type { UiRuntime } from './runtime.js';
5
6
  import type { View } from './dom.js';
6
7
  export interface ResourceModel<Props, A, E = unknown> {
7
8
  readonly props: Props;
@@ -17,11 +18,11 @@ export declare const resourceError: (result: AsyncResult.AsyncResult<unknown, un
17
18
  export declare function resourceComponent<Props, A, E = unknown, R = never>(definition: {
18
19
  request: (props: Snapshot<Props>) => {
19
20
  key: string;
20
- load: () => UiLoad<A, E, R>;
21
+ load: () => UiLoad<A, E, R | Scope.Scope>;
21
22
  delay?: number;
22
23
  } | undefined;
23
24
  view: View<ResourceModel<Props, A, E>, ResourceMessage>;
24
- } & ([R] extends [never] ? {
25
+ } & ([Exclude<R, Scope.Scope>] extends [never] ? {
25
26
  runtime?: UiRuntime<R>;
26
27
  } : {
27
28
  runtime: UiRuntime<R>;
package/dist/resource.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { commandSlot } from './program.js';
2
- import { Cause, Effect, Option } from 'effect';
2
+ import * as Cause from 'effect/Cause';
3
+ import * as Effect from 'effect/Effect';
4
+ import * as Option from 'effect/Option';
3
5
  import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
4
6
  import { loadEffect } from './load.js';
5
7
  import { component } from './component.js';
6
- import { defaultUiRuntime } from './runtime.js';
7
8
  import { effectCommand } from './program.js';
8
9
  const commandLoad = commandSlot('load');
9
10
  export const available = (result) => Option.getOrUndefined(AsyncResult.value(result));
@@ -15,7 +16,7 @@ export const resourceError = (result) => {
15
16
  };
16
17
  /** A replaceable request owns one result. Key changes clear it; refreshes preserve the last success. */
17
18
  export function resourceComponent(definition) {
18
- const runtime = definition.runtime ?? defaultUiRuntime;
19
+ const runtime = definition.runtime;
19
20
  const request = (model, retry = false) => {
20
21
  const selected = definition.request(model.props);
21
22
  if (!retry && selected?.key === model.key)
@@ -32,9 +33,14 @@ export function resourceComponent(definition) {
32
33
  result: AsyncResult.waiting(selected.key === model.key ? model.result : AsyncResult.initial()),
33
34
  },
34
35
  commands: [
35
- effectCommand(commandLoad, () => runtime.provide(selected.delay
36
- ? Effect.sleep(selected.delay).pipe(Effect.andThen(loadEffect(selected.load)))
37
- : loadEffect(selected.load)), {
36
+ effectCommand(commandLoad, () => {
37
+ const work = selected.delay
38
+ ? Effect.sleep(selected.delay).pipe(Effect.andThen(loadEffect(selected.load)))
39
+ : loadEffect(selected.load);
40
+ return runtime
41
+ ? runtime.provideScoped(work)
42
+ : work;
43
+ }, {
38
44
  policy: 'replace',
39
45
  onSuccess: (value) => ({ type: 'Loaded', value }),
40
46
  onFailure: (cause) => ({ type: 'Failed', cause }),
package/dist/runtime.d.ts CHANGED
@@ -1,10 +1,19 @@
1
1
  import type { Snapshot } from './snapshot.js';
2
- import { Context, Effect } from 'effect';
2
+ import * as Context from 'effect/Context';
3
+ import * as Clock from 'effect/Clock';
4
+ import * as Effect from 'effect/Effect';
5
+ import type * as Fiber from 'effect/Fiber';
6
+ import * as Scope from 'effect/Scope';
3
7
  import { type Command, type RunningProgram, type Transition } from './program.js';
4
8
  /** A binding to application-owned services, not a new scope or service lifetime. */
5
9
  export interface UiRuntime<R> {
6
10
  readonly context: Context.Context<R>;
11
+ readonly clock: Clock.Clock;
12
+ /** Execute work with a fresh resource scope and the captured application context. */
13
+ readonly runFork: <A, E>(effect: Effect.Effect<A, E, R | Scope.Scope>) => Fiber.Fiber<A, E>;
7
14
  readonly provide: <A, E>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E>;
15
+ /** Supply services while preserving the resource owner's scope. */
16
+ readonly provideScoped: <A, E>(effect: Effect.Effect<A, E, R | Scope.Scope>) => Effect.Effect<A, E, Scope.Scope>;
8
17
  readonly command: <M>(command: Command<M, R>) => Command<M>;
9
18
  readonly program: <M, Msg>(options: {
10
19
  initial: M | Snapshot<M>;
@@ -14,4 +23,6 @@ export interface UiRuntime<R> {
14
23
  }) => RunningProgram<M, Msg>;
15
24
  }
16
25
  export declare function uiRuntime<R>(context: Context.Context<R>): UiRuntime<R>;
26
+ /** Capture the current Effect environment. The surrounding scope owns all started fibers. */
27
+ export declare const makeUiRuntime: <R = never>() => Effect.Effect<UiRuntime<R>, never, R | Scope.Scope>;
17
28
  export declare const defaultUiRuntime: UiRuntime<never>;
package/dist/runtime.js CHANGED
@@ -1,18 +1,40 @@
1
- import { Context, Effect, Stream } from 'effect';
1
+ import * as Context from 'effect/Context';
2
+ import * as Clock from 'effect/Clock';
3
+ import * as Effect from 'effect/Effect';
4
+ import * as FiberSet from 'effect/FiberSet';
5
+ import * as Scope from 'effect/Scope';
6
+ import * as Stream from 'effect/Stream';
2
7
  import { program } from './program.js';
3
8
  export function uiRuntime(context) {
9
+ return createRuntime(context, Effect.runForkWith(context));
10
+ }
11
+ /** Capture the current Effect environment. The surrounding scope owns all started fibers. */
12
+ export const makeUiRuntime = () => Effect.gen(function* () {
13
+ const context = yield* Effect.context();
14
+ const fibers = yield* FiberSet.make();
15
+ const run = yield* FiberSet.runtime(fibers)();
16
+ return createRuntime(context, run);
17
+ });
18
+ function createRuntime(context, run) {
4
19
  const provide = (effect) => Effect.provideContext(effect, context);
5
20
  const command = (value) => value.stream
6
- ? { ...value, stream: value.stream.pipe(Stream.provideContext(context)) }
21
+ ? {
22
+ ...value,
23
+ stream: Stream.unwrap(Effect.map(Effect.scope, (scope) => value.stream.pipe(Stream.provideContext(Context.add(context, Scope.Scope, scope))))),
24
+ }
7
25
  : value.action
8
- ? { ...value, action: provide(value.action) }
9
- : { ...value, effect: provide(value.effect) };
26
+ ? { ...value, action: provide(Effect.scoped(value.action)) }
27
+ : { ...value, effect: provide(Effect.scoped(value.effect)) };
10
28
  return {
11
29
  context,
30
+ clock: Context.get(context, Clock.Clock),
31
+ runFork: (effect) => run(Effect.scoped(effect)),
12
32
  provide,
33
+ provideScoped: (effect) => Effect.flatMap(Effect.scope, (scope) => provide(Scope.provide(effect, scope))),
13
34
  command,
14
35
  program: (options) => program({
15
36
  ...options,
37
+ runtime: { runFork: (effect) => run(Effect.scoped(effect)) },
16
38
  update: (model, message) => {
17
39
  const next = options.update(model, message);
18
40
  return {
@@ -24,4 +46,4 @@ export function uiRuntime(context) {
24
46
  }),
25
47
  };
26
48
  }
27
- export const defaultUiRuntime = uiRuntime(Context.empty());
49
+ export const defaultUiRuntime = /* @__PURE__ */ uiRuntime(Context.empty());
package/dist/session.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type * as Scope from 'effect/Scope';
1
2
  import type { Snapshot } from './snapshot.js';
2
3
  import { type Query } from './query.js';
3
4
  import type { DisposableOwner } from './owner.js';
@@ -28,8 +29,8 @@ export interface QueryResource<Args, A, E = never> {
28
29
  export declare function queryResource<Args, A, E, R>(context: {
29
30
  cache: QueryCache<R>;
30
31
  changed?: () => void;
31
- }, definition: Query<Args, A, E, NoInfer<R>>): QueryResource<Args, A, E>;
32
- export declare function observeQuery<Args, A, E, R>(owner: DisposableOwner, cache: QueryCache<R>, definition: Query<Args, A, E, NoInfer<R>>, changed: (result: AsyncResult.AsyncResult<Snapshot<A>, E>) => void): QueryResource<Args, A, E>;
32
+ }, definition: Query<Args, A, E, NoInfer<R> | Scope.Scope>): QueryResource<Args, A, E>;
33
+ export declare function observeQuery<Args, A, E, R>(owner: DisposableOwner, cache: QueryCache<R>, definition: Query<Args, A, E, NoInfer<R> | Scope.Scope>, changed: (result: AsyncResult.AsyncResult<Snapshot<A>, E>) => void): QueryResource<Args, A, E>;
33
34
  /** Application selectors run once per explicit input version, with optional prior output sharing. */
34
35
  export declare function projectionCache(): {
35
36
  invalidate: () => void;
package/dist/session.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { runAll, reportError, reportSafely } from './errors.js';
2
- import { encodeQueryKey } from './query.js';
2
+ import { encodeQueryArguments } from './query.js';
3
3
  import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
4
4
  import * as Atom from 'effect/unstable/reactivity/Atom';
5
5
  import { cacheInternals } from './cache-internals.js';
@@ -108,11 +108,14 @@ export function queryResource(context, definition) {
108
108
  if (disposed || internal.disposed())
109
109
  return;
110
110
  const nextGeneration = internal.registry.get(internal.generation);
111
- const nextKey = args === undefined ? undefined : encodeQueryKey(args);
111
+ const nextKey = args === undefined ? undefined : encodeQueryArguments(definition, args);
112
112
  if (nextKey === key && nextGeneration === generation)
113
113
  return;
114
114
  const selected = ++revision;
115
115
  disconnect();
116
+ // Releasing the previous query can run finalizers that select or dispose again.
117
+ if (disposed || selected !== revision)
118
+ return;
116
119
  key = nextKey;
117
120
  generation = nextGeneration;
118
121
  const nextAtom = args === undefined ? undefined : internal.query(definition, args);
@@ -192,10 +195,17 @@ export function projectionCache() {
192
195
  }
193
196
  export function sessionGroup(sessions, changed) {
194
197
  const scope = lifetime();
195
- for (const session of sessions) {
198
+ for (const session of sessions)
196
199
  scope.add(() => session.dispose());
197
- if (session.subscribe)
198
- scope.add(session.subscribe(changed));
200
+ try {
201
+ for (const session of sessions) {
202
+ if (session.subscribe)
203
+ scope.add(session.subscribe(changed));
204
+ }
205
+ }
206
+ catch (error) {
207
+ scope.dispose();
208
+ throw error;
199
209
  }
200
210
  return {
201
211
  refresh: () => {
@@ -1,7 +1,14 @@
1
1
  /** Internal completion accounting, including reentrant acquisition before a fiber is available. */
2
2
  export declare class Settlement {
3
+ readonly runtime?: UiRuntime<never> | undefined;
4
+ constructor(runtime?: UiRuntime<never> | undefined);
5
+ /** Exit of the owning mount; element replacement/removal uses a successful exit. */
6
+ exit: Exit.Exit<unknown, unknown>;
3
7
  private pending;
4
8
  private waiters;
5
9
  begin(): () => void;
6
- wait(): Promise<void>;
10
+ wait(): Effect.Effect<void>;
7
11
  }
12
+ import * as Effect from 'effect/Effect';
13
+ import * as Exit from 'effect/Exit';
14
+ import type { UiRuntime } from './runtime.js';
@@ -1,7 +1,13 @@
1
1
  /** Internal completion accounting, including reentrant acquisition before a fiber is available. */
2
2
  export class Settlement {
3
+ runtime;
4
+ constructor(runtime) {
5
+ this.runtime = runtime;
6
+ }
7
+ /** Exit of the owning mount; element replacement/removal uses a successful exit. */
8
+ exit = Exit.void;
3
9
  pending = 0;
4
- waiters = [];
10
+ waiters = new Set();
5
11
  begin() {
6
12
  this.pending++;
7
13
  let finished = false;
@@ -11,17 +17,23 @@ export class Settlement {
11
17
  finished = true;
12
18
  if (--this.pending)
13
19
  return;
14
- for (const done of this.waiters)
20
+ const waiters = [...this.waiters];
21
+ this.waiters.clear();
22
+ for (const done of waiters)
15
23
  done();
16
- this.waiters.length = 0;
17
24
  };
18
25
  }
19
26
  wait() {
20
- return new Promise((done) => {
21
- if (this.pending)
22
- this.waiters.push(done);
23
- else
24
- done();
27
+ return Effect.callback((resume) => {
28
+ if (!this.pending)
29
+ return resume(Effect.void);
30
+ const done = () => resume(Effect.void);
31
+ this.waiters.add(done);
32
+ return Effect.sync(() => {
33
+ this.waiters.delete(done);
34
+ });
25
35
  });
26
36
  }
27
37
  }
38
+ import * as Effect from 'effect/Effect';
39
+ import * as Exit from 'effect/Exit';
@@ -1,4 +1,4 @@
1
- import type { Effect } from 'effect';
1
+ import type * as Effect from 'effect/Effect';
2
2
  import type * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
3
3
  /** Mark an application service as opaque to snapshot typing; this is a type-only brand. */
4
4
  export declare const snapshotOpaque: unique symbol;
package/dist/snapshot.js CHANGED
@@ -1,19 +1,19 @@
1
1
  import { isAsyncResult } from 'effect/unstable/reactivity/AsyncResult';
2
- const checked = new WeakSet();
2
+ const protectedValues = new WeakSet();
3
3
  /** Protect published plain data in every build. Opaque objects keep their own lifecycle. */
4
4
  export function protectSnapshot(value) {
5
5
  const seen = new Set();
6
6
  const plain = [];
7
- const validate = (item) => {
8
- if (!item || typeof item !== 'object' || checked.has(item) || seen.has(item))
7
+ const visit = (item) => {
8
+ if (!item || typeof item !== 'object' || protectedValues.has(item) || seen.has(item))
9
9
  return;
10
10
  seen.add(item);
11
11
  // Effect wrappers remain opaque, but their successful UI data is plain data.
12
12
  if (isAsyncResult(item)) {
13
13
  if (item._tag === 'Success')
14
- validate(item.value);
14
+ visit(item.value);
15
15
  else if (item._tag === 'Failure' && item.previousSuccess._tag === 'Some')
16
- validate(item.previousSuccess.value);
16
+ visit(item.previousSuccess.value);
17
17
  return;
18
18
  }
19
19
  const prototype = Object.getPrototypeOf(item);
@@ -21,18 +21,16 @@ export function protectSnapshot(value) {
21
21
  return;
22
22
  for (const key of Reflect.ownKeys(item)) {
23
23
  const descriptor = Object.getOwnPropertyDescriptor(item, key);
24
- if (!Object.hasOwn(descriptor, 'value'))
25
- throw new Error(`Snapshot data cannot contain an accessor (${String(key)}). Materialize its value before publication; keep services in opaque instances.`);
26
- validate(descriptor.value);
24
+ if (Object.hasOwn(descriptor, 'value'))
25
+ visit(descriptor.value);
27
26
  }
28
27
  plain.push(item);
29
28
  };
30
- // Validate the complete graph before caching any proof. A rejected cyclic graph
31
- // must not make a later publication skip validation through a previously seen child.
32
- validate(value);
29
+ // Traverse stored data without invoking application getters.
30
+ visit(value);
33
31
  for (const item of plain)
34
32
  Object.freeze(item);
35
33
  for (const item of seen)
36
- checked.add(item);
34
+ protectedValues.add(item);
37
35
  return value;
38
36
  }
@@ -0,0 +1,38 @@
1
+ import * as Effect from 'effect/Effect';
2
+ import type * as Scope from 'effect/Scope';
3
+ import * as Stream from 'effect/Stream';
4
+ import * as SubscriptionRef from 'effect/SubscriptionRef';
5
+ import type * as Atom from 'effect/unstable/reactivity/Atom';
6
+ import * as AtomRegistry from 'effect/unstable/reactivity/AtomRegistry';
7
+ import { type Snapshot } from './snapshot.js';
8
+ /** A current immutable value and its publications. Observation does not own the producer. */
9
+ export interface Source<A> {
10
+ readonly model: () => Snapshot<A>;
11
+ readonly subscribe: (listener: (value: Snapshot<A>) => void) => () => void;
12
+ }
13
+ /** Select explicit inputs. Equal results retain their identity and do not notify subscribers. */
14
+ export declare function mapSource<A, B>(source: Source<A>, project: (value: Snapshot<A>) => B | Snapshot<B>, equals?: (previous: Snapshot<B>, next: Snapshot<B>) => boolean): Source<B>;
15
+ /** Observe a snapshot stream in the current Effect scope. Handle typed stream errors as values upstream. */
16
+ export declare const fromStream: <A, R>(stream: Stream.Stream<A, never, R>, initial: A | Snapshot<A>) => Effect.Effect<Source<A>, never, R | Scope.Scope>;
17
+ /** Observe the existing ref; closing the observation leaves the ref and its other users alive. */
18
+ export declare const fromSubscriptionRef: <A>(ref: SubscriptionRef.SubscriptionRef<A>) => Effect.Effect<Source<A>, never, Scope.Scope>;
19
+ export declare function fromAtom<A>(atom: Atom.Atom<A>): Effect.Effect<Source<A>, never, AtomRegistry.AtomRegistry | Scope.Scope>;
20
+ export declare function fromAtom<A>(atom: Atom.Atom<A>, registry: AtomRegistry.AtomRegistry): Effect.Effect<Source<A>, never, Scope.Scope>;
21
+ /** A batched publication boundary for independently owned sessions with explicit invalidation. */
22
+ export declare function projectionSource<Model>(options: {
23
+ invalidate?: () => void;
24
+ refresh?: () => void;
25
+ project: () => Model | Snapshot<Model>;
26
+ reconcile?: (previous: Snapshot<Model>, next: Model | Snapshot<Model>) => Model | Snapshot<Model>;
27
+ afterPublish?: () => void;
28
+ }): {
29
+ readonly disposed: boolean;
30
+ changed: () => void;
31
+ watch(source: {
32
+ subscribe: (changed: () => void) => () => void;
33
+ }): () => void;
34
+ start(): void;
35
+ model(this: void): Snapshot<Model>;
36
+ subscribe(this: void, listener: (model: Snapshot<Model>) => void): () => void;
37
+ dispose(): void;
38
+ };