effectweb 0.3.0 → 0.4.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 (59) hide show
  1. package/README.md +35 -4
  2. package/dist/AsyncContent.d.ts +1 -1
  3. package/dist/AsyncContent.js +18 -35
  4. package/dist/boundary.d.ts +11 -0
  5. package/dist/boundary.js +66 -0
  6. package/dist/cache-internals.d.ts +4 -0
  7. package/dist/cache.d.ts +21 -5
  8. package/dist/cache.js +178 -25
  9. package/dist/collection.d.ts +1 -1
  10. package/dist/collection.js +1 -3
  11. package/dist/component.d.ts +3 -0
  12. package/dist/component.js +73 -10
  13. package/dist/dom.d.ts +35 -24
  14. package/dist/dom.js +177 -128
  15. package/dist/effectEvent.d.ts +5 -4
  16. package/dist/effectEvent.js +29 -38
  17. package/dist/form.d.ts +2 -1
  18. package/dist/http.d.ts +2 -1
  19. package/dist/http.js +1 -1
  20. package/dist/index.d.ts +7 -4
  21. package/dist/index.js +6 -3
  22. package/dist/infinite-query.d.ts +42 -0
  23. package/dist/infinite-query.js +150 -0
  24. package/dist/jsx-runtime.d.ts +6 -0
  25. package/dist/jsx-runtime.js +14 -1
  26. package/dist/jsx.d.ts +2 -1
  27. package/dist/keyed-tasks.d.ts +30 -0
  28. package/dist/keyed-tasks.js +102 -0
  29. package/dist/lazy.d.ts +2 -1
  30. package/dist/lazy.js +20 -7
  31. package/dist/load.d.ts +1 -1
  32. package/dist/load.js +1 -1
  33. package/dist/mount.d.ts +13 -2
  34. package/dist/mount.js +82 -21
  35. package/dist/owner.d.ts +9 -5
  36. package/dist/owner.js +27 -9
  37. package/dist/pages.d.ts +16 -15
  38. package/dist/pages.js +8 -5
  39. package/dist/program.d.ts +14 -2
  40. package/dist/program.js +96 -36
  41. package/dist/query-internals.d.ts +5 -3
  42. package/dist/query.d.ts +7 -1
  43. package/dist/query.js +7 -3
  44. package/dist/resource.js +3 -1
  45. package/dist/runtime.d.ts +2 -1
  46. package/dist/runtime.js +3 -1
  47. package/dist/session.js +23 -7
  48. package/dist/settlement.d.ts +8 -0
  49. package/dist/settlement.js +32 -0
  50. package/dist/sharing.js +16 -13
  51. package/dist/snapshot.d.ts +1 -1
  52. package/dist/snapshot.js +31 -25
  53. package/dist/task.d.ts +1 -1
  54. package/dist/task.js +3 -1
  55. package/dist/tasks.d.ts +1 -1
  56. package/dist/tasks.js +3 -1
  57. package/dist/testing.d.ts +5 -4
  58. package/dist/testing.js +4 -2
  59. package/package.json +9 -1
package/dist/resource.js CHANGED
@@ -1,5 +1,7 @@
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';
package/dist/runtime.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Snapshot } from './snapshot.js';
2
- import { Context, Effect } from 'effect';
2
+ import * as Context from 'effect/Context';
3
+ import * as Effect from 'effect/Effect';
3
4
  import { type Command, type RunningProgram, type Transition } from './program.js';
4
5
  /** A binding to application-owned services, not a new scope or service lifetime. */
5
6
  export interface UiRuntime<R> {
package/dist/runtime.js CHANGED
@@ -1,4 +1,6 @@
1
- import { Context, Effect, Stream } from 'effect';
1
+ import * as Context from 'effect/Context';
2
+ import * as Effect from 'effect/Effect';
3
+ import * as Stream from 'effect/Stream';
2
4
  import { program } from './program.js';
3
5
  export function uiRuntime(context) {
4
6
  const provide = (effect) => Effect.provideContext(effect, context);
package/dist/session.js CHANGED
@@ -38,7 +38,10 @@ export function queryResource(context, definition) {
38
38
  let revision = 0;
39
39
  const initial = AsyncResult.initial();
40
40
  const listeners = new Set();
41
- const read = () => atom && !disposed && generation === internal.registry.get(internal.generation)
41
+ const read = () => atom &&
42
+ !disposed &&
43
+ !internal.disposed() &&
44
+ generation === internal.registry.get(internal.generation)
42
45
  ? internal.registry.get(atom)
43
46
  : initial;
44
47
  let published;
@@ -102,7 +105,7 @@ export function queryResource(context, definition) {
102
105
  });
103
106
  return {
104
107
  select(args) {
105
- if (disposed)
108
+ if (disposed || internal.disposed())
106
109
  return;
107
110
  const nextGeneration = internal.registry.get(internal.generation);
108
111
  const nextKey = args === undefined ? undefined : encodeQueryKey(args);
@@ -118,7 +121,12 @@ export function queryResource(context, definition) {
118
121
  atom = nextAtom;
119
122
  if (nextAtom) {
120
123
  // Subscription setup itself can execute a synchronous Effect and reenter selection.
121
- const release = internal.registry.subscribe(nextAtom, notify);
124
+ const releaseUse = internal.retain(nextAtom);
125
+ const unsubscribe = internal.registry.subscribe(nextAtom, notify);
126
+ const release = () => {
127
+ unsubscribe();
128
+ releaseUse();
129
+ };
122
130
  if (disposed || selected !== revision) {
123
131
  release();
124
132
  return;
@@ -139,9 +147,10 @@ export function queryResource(context, definition) {
139
147
  refresh: () => {
140
148
  if (atom &&
141
149
  !disposed &&
150
+ !internal.disposed() &&
142
151
  generation === internal.registry.get(internal.generation) &&
143
152
  !internal.registry.get(atom).waiting)
144
- internal.registry.refresh(atom);
153
+ internal.refresh(atom);
145
154
  },
146
155
  dispose: () => {
147
156
  if (disposed)
@@ -183,10 +192,17 @@ export function projectionCache() {
183
192
  }
184
193
  export function sessionGroup(sessions, changed) {
185
194
  const scope = lifetime();
186
- for (const session of sessions) {
195
+ for (const session of sessions)
187
196
  scope.add(() => session.dispose());
188
- if (session.subscribe)
189
- scope.add(session.subscribe(changed));
197
+ try {
198
+ for (const session of sessions) {
199
+ if (session.subscribe)
200
+ scope.add(session.subscribe(changed));
201
+ }
202
+ }
203
+ catch (error) {
204
+ scope.dispose();
205
+ throw error;
190
206
  }
191
207
  return {
192
208
  refresh: () => {
@@ -0,0 +1,8 @@
1
+ /** Internal completion accounting, including reentrant acquisition before a fiber is available. */
2
+ export declare class Settlement {
3
+ private pending;
4
+ private waiters;
5
+ begin(): () => void;
6
+ wait(): Effect.Effect<void>;
7
+ }
8
+ import * as Effect from 'effect/Effect';
@@ -0,0 +1,32 @@
1
+ /** Internal completion accounting, including reentrant acquisition before a fiber is available. */
2
+ export class Settlement {
3
+ pending = 0;
4
+ waiters = new Set();
5
+ begin() {
6
+ this.pending++;
7
+ let finished = false;
8
+ return () => {
9
+ if (finished)
10
+ return;
11
+ finished = true;
12
+ if (--this.pending)
13
+ return;
14
+ const waiters = [...this.waiters];
15
+ this.waiters.clear();
16
+ for (const done of waiters)
17
+ done();
18
+ };
19
+ }
20
+ wait() {
21
+ return Effect.callback((resume) => {
22
+ if (!this.pending)
23
+ return resume(Effect.void);
24
+ const done = () => resume(Effect.void);
25
+ this.waiters.add(done);
26
+ return Effect.sync(() => {
27
+ this.waiters.delete(done);
28
+ });
29
+ });
30
+ }
31
+ }
32
+ import * as Effect from 'effect/Effect';
package/dist/sharing.js CHANGED
@@ -60,28 +60,31 @@ function share(previous, next, fields) {
60
60
  oldKeys.some((key) => !Object.hasOwn(Object.getOwnPropertyDescriptor(previous, key), 'value')))
61
61
  return next;
62
62
  let equal = keys.length === oldKeys.length && (!array || value.length === old.length);
63
- let unchangedNext = true;
64
- const shared = array ? [] : {};
65
- if (array)
66
- shared.length = value.length;
63
+ let changed;
67
64
  for (const key of keys) {
68
65
  const custom = fields && Object.hasOwn(fields, key) ? fields[key] : undefined;
69
66
  const item = custom && Object.hasOwn(old, key)
70
67
  ? custom(old[key], value[key])
71
68
  : share(Object.hasOwn(old, key) ? old[key] : undefined, value[key], undefined);
72
- // Define an own data property, including __proto__, without invoking setters.
73
- Object.defineProperty(shared, key, {
74
- value: item,
75
- writable: true,
76
- enumerable: true,
77
- configurable: true,
78
- });
79
69
  if (!Object.hasOwn(old, key) || !Object.is(item, old[key]))
80
70
  equal = false;
81
71
  if (!Object.is(item, value[key]))
82
- unchangedNext = false;
72
+ (changed ??= new Map()).set(key, item);
73
+ }
74
+ let result = equal ? previous : next;
75
+ if (!equal && changed) {
76
+ const shared = array ? [] : {};
77
+ if (array)
78
+ shared.length = value.length;
79
+ for (const key of keys)
80
+ Object.defineProperty(shared, key, {
81
+ value: changed.has(key) ? changed.get(key) : value[key],
82
+ writable: true,
83
+ enumerable: true,
84
+ configurable: true,
85
+ });
86
+ result = shared;
83
87
  }
84
- const result = equal ? previous : unchangedNext ? next : shared;
85
88
  if (fields)
86
89
  return result;
87
90
  if (!pairs) {
@@ -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,30 +1,36 @@
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
- if (!value || typeof value !== 'object' || checked.has(value))
6
- return value;
7
- // AsyncResult is an immutable Effect wrapper, but its successful UI data is plain data.
8
- if (isAsyncResult(value)) {
9
- checked.add(value);
10
- if (value._tag === 'Success')
11
- protectSnapshot(value.value);
12
- else if (value._tag === 'Failure' && value.previousSuccess._tag === 'Some')
13
- protectSnapshot(value.previousSuccess.value);
14
- return value;
15
- }
16
- const prototype = Object.getPrototypeOf(value);
17
- if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null)
18
- return value;
19
- // Mark before walking to allow cyclic data and reuse already checked shared branches.
20
- checked.add(value);
21
- const descriptors = Object.getOwnPropertyDescriptors(value);
22
- for (const key of Reflect.ownKeys(descriptors)) {
23
- const descriptor = Reflect.get(descriptors, key);
24
- // Never execute getters or traverse closures, class instances, Effects or DOM objects.
25
- if ('value' in descriptor)
26
- protectSnapshot(descriptor.value);
27
- }
28
- Object.freeze(value);
5
+ const seen = new Set();
6
+ const plain = [];
7
+ const visit = (item) => {
8
+ if (!item || typeof item !== 'object' || protectedValues.has(item) || seen.has(item))
9
+ return;
10
+ seen.add(item);
11
+ // Effect wrappers remain opaque, but their successful UI data is plain data.
12
+ if (isAsyncResult(item)) {
13
+ if (item._tag === 'Success')
14
+ visit(item.value);
15
+ else if (item._tag === 'Failure' && item.previousSuccess._tag === 'Some')
16
+ visit(item.previousSuccess.value);
17
+ return;
18
+ }
19
+ const prototype = Object.getPrototypeOf(item);
20
+ if (!Array.isArray(item) && prototype !== Object.prototype && prototype !== null)
21
+ return;
22
+ for (const key of Reflect.ownKeys(item)) {
23
+ const descriptor = Object.getOwnPropertyDescriptor(item, key);
24
+ if (Object.hasOwn(descriptor, 'value'))
25
+ visit(descriptor.value);
26
+ }
27
+ plain.push(item);
28
+ };
29
+ // Traverse stored data without invoking application getters.
30
+ visit(value);
31
+ for (const item of plain)
32
+ Object.freeze(item);
33
+ for (const item of seen)
34
+ protectedValues.add(item);
29
35
  return value;
30
36
  }
package/dist/task.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Snapshot } from './snapshot.js';
2
- import { Effect } from 'effect';
2
+ import * as Effect from 'effect/Effect';
3
3
  import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
4
4
  import type { View } from './dom.js';
5
5
  import { type Send, type TaskPolicy } from './program.js';
package/dist/task.js CHANGED
@@ -1,5 +1,7 @@
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 { programView } from './component.js';
5
7
  import { effectCommand, } from './program.js';
package/dist/tasks.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Snapshot } from './snapshot.js';
2
2
  import type { ModelOwner, TaskPolicy } from './owner.js';
3
- import { Effect } from 'effect';
3
+ import * as Effect from 'effect/Effect';
4
4
  import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
5
5
  import type { View } from './dom.js';
6
6
  import { type CommandSlot, type Send, type RunningProgram } from './program.js';
package/dist/tasks.js CHANGED
@@ -1,4 +1,5 @@
1
- import { Effect, Option } from 'effect';
1
+ import * as Effect from 'effect/Effect';
2
+ import * as Option from 'effect/Option';
2
3
  import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
3
4
  import { programView } from './component.js';
4
5
  import { commandSlot, effectCommand, program, } from './program.js';
@@ -137,6 +138,7 @@ function taskBuilder(definition, runtime) {
137
138
  receive,
138
139
  controls,
139
140
  view: (view) => programView({
141
+ ...(definition.identity ? { identity: definition.identity } : {}),
140
142
  create,
141
143
  receive: (source, props) => receive(source, props),
142
144
  view,
package/dist/testing.d.ts CHANGED
@@ -1,12 +1,13 @@
1
- import { Effect } from 'effect';
1
+ import * as Effect from 'effect/Effect';
2
2
  import type { CommandSlot, Program, RunningProgram } from './program.js';
3
3
  import { type UiRuntime } from './runtime.js';
4
4
  /** Program inspection and controlled Effect execution with application-owned services. */
5
5
  export interface ProgramDriver<M, Msg, R = never> extends Program<M, Msg> {
6
+ readonly close: RunningProgram<M, Msg>['close'];
6
7
  readonly activeSlots: RunningProgram<M, Msg>['activeSlots'];
7
- readonly awaitSlot: (slot: CommandSlot) => Promise<void>;
8
- readonly awaitIdle: () => Promise<void>;
9
- readonly run: <A, E>(effect: Effect.Effect<A, E, R>) => Promise<A>;
8
+ readonly awaitSlot: (slot: CommandSlot) => Effect.Effect<void>;
9
+ readonly awaitIdle: () => Effect.Effect<void>;
10
+ readonly run: <A, E>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E>;
10
11
  }
11
12
  export declare function programDriver<M, Msg>(source: RunningProgram<M, Msg>): ProgramDriver<M, Msg>;
12
13
  export declare function programDriver<M, Msg, R>(source: RunningProgram<M, Msg>, runtime: UiRuntime<R>): ProgramDriver<M, Msg, R>;
package/dist/testing.js CHANGED
@@ -1,4 +1,5 @@
1
- import { Cause, Effect } from 'effect';
1
+ import * as Cause from 'effect/Cause';
2
+ import * as Effect from 'effect/Effect';
2
3
  import { defaultUiRuntime } from './runtime.js';
3
4
  export function programDriver(source, runtime) {
4
5
  return makeDriver(source, runtime ?? defaultUiRuntime);
@@ -9,10 +10,11 @@ function makeDriver(source, runtime) {
9
10
  send: source.send,
10
11
  subscribe: source.subscribe,
11
12
  dispose: source.dispose,
13
+ close: source.close,
12
14
  activeSlots: source.activeSlots,
13
15
  awaitSlot: (slot) => source.awaitIdle(slot),
14
16
  awaitIdle: () => source.awaitIdle(),
15
- run: (effect) => Effect.runPromise(runtime.provide(effect)),
17
+ run: runtime.provide,
16
18
  };
17
19
  }
18
20
  /** A reusable controlled request, with cancellation visible to tests. No renderer or private messages. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effectweb",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Immutable Effect models and compiled JSX with direct DOM rendering",
5
5
  "homepage": "https://github.com/DerpyCrabs/EffectWeb",
6
6
  "bugs": {
@@ -127,6 +127,14 @@
127
127
  "./http": {
128
128
  "types": "./dist/http.d.ts",
129
129
  "import": "./dist/http.js"
130
+ },
131
+ "./infinite-query": {
132
+ "types": "./dist/infinite-query.d.ts",
133
+ "import": "./dist/infinite-query.js"
134
+ },
135
+ "./keyed-tasks": {
136
+ "types": "./dist/keyed-tasks.d.ts",
137
+ "import": "./dist/keyed-tasks.js"
130
138
  }
131
139
  },
132
140
  "publishConfig": {