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/source.js ADDED
@@ -0,0 +1,218 @@
1
+ import * as Effect from 'effect/Effect';
2
+ import * as Cause from 'effect/Cause';
3
+ import * as Stream from 'effect/Stream';
4
+ import * as SubscriptionRef from 'effect/SubscriptionRef';
5
+ import * as AtomRegistry from 'effect/unstable/reactivity/AtomRegistry';
6
+ import { protectSnapshot } from './snapshot.js';
7
+ import { reportError, reportSafely } from './errors.js';
8
+ /** Select explicit inputs. Equal results retain their identity and do not notify subscribers. */
9
+ export function mapSource(source, project, equals = Object.is) {
10
+ let initialized = false;
11
+ let previousInput;
12
+ let current;
13
+ const select = (input) => {
14
+ if (initialized && Object.is(previousInput, input))
15
+ return current;
16
+ const next = protectSnapshot(project(input));
17
+ if (!initialized || !equals(current, next))
18
+ current = next;
19
+ previousInput = input;
20
+ initialized = true;
21
+ return current;
22
+ };
23
+ const model = () => select(source.model());
24
+ return {
25
+ model,
26
+ subscribe(listener) {
27
+ let previous = model();
28
+ return source.subscribe((value) => {
29
+ const next = select(value);
30
+ if (Object.is(previous, next))
31
+ return;
32
+ previous = next;
33
+ listener(next);
34
+ });
35
+ },
36
+ };
37
+ }
38
+ function publication(initial) {
39
+ let current = protectSnapshot(initial);
40
+ let disposed = false;
41
+ const listeners = new Set();
42
+ const source = {
43
+ model: () => current,
44
+ subscribe: (listener) => {
45
+ if (disposed)
46
+ return () => { };
47
+ listeners.add(listener);
48
+ return () => {
49
+ listeners.delete(listener);
50
+ };
51
+ },
52
+ };
53
+ return {
54
+ source,
55
+ publish(value) {
56
+ if (disposed || Object.is(current, value))
57
+ return;
58
+ current = protectSnapshot(value);
59
+ const next = current;
60
+ // oxlint-disable-next-line unicorn/no-useless-spread -- Reentrant subscriptions must not join the publication in progress.
61
+ for (const listener of [...listeners]) {
62
+ if (disposed || current !== next)
63
+ break;
64
+ if (listeners.has(listener)) {
65
+ try {
66
+ listener(next);
67
+ }
68
+ catch (error) {
69
+ reportSafely(reportError, error);
70
+ }
71
+ }
72
+ }
73
+ },
74
+ dispose() {
75
+ disposed = true;
76
+ listeners.clear();
77
+ },
78
+ };
79
+ }
80
+ /** Observe a snapshot stream in the current Effect scope. Handle typed stream errors as values upstream. */
81
+ export const fromStream = (stream, initial) => Effect.gen(function* () {
82
+ const state = yield* Effect.acquireRelease(Effect.sync(() => publication(initial)), (state) => Effect.sync(() => state.dispose()));
83
+ yield* Stream.runForEach(stream, (value) => Effect.sync(() => state.publish(value))).pipe(Effect.catchCause((cause) => Effect.sync(() => {
84
+ if (!Cause.hasInterruptsOnly(cause))
85
+ reportSafely(reportError, cause);
86
+ })), Effect.forkScoped({ startImmediately: true }));
87
+ return state.source;
88
+ });
89
+ /** Observe the existing ref; closing the observation leaves the ref and its other users alive. */
90
+ export const fromSubscriptionRef = (ref) => Effect.gen(function* () {
91
+ const initial = yield* SubscriptionRef.get(ref);
92
+ return yield* fromStream(SubscriptionRef.changes(ref), initial);
93
+ });
94
+ export function fromAtom(atom, provided) {
95
+ return Effect.gen(function* () {
96
+ const registry = provided ?? (yield* AtomRegistry.AtomRegistry);
97
+ const state = yield* Effect.acquireRelease(Effect.sync(() => publication(registry.get(atom))), (state) => Effect.sync(() => state.dispose()));
98
+ yield* Effect.acquireRelease(Effect.sync(() => registry.subscribe(atom, (value) => state.publish(value), { immediate: true })), (unsubscribe) => Effect.sync(unsubscribe));
99
+ return state.source;
100
+ });
101
+ }
102
+ /** A batched publication boundary for independently owned sessions with explicit invalidation. */
103
+ export function projectionSource(options) {
104
+ const listeners = new Set();
105
+ const subscriptions = new Set();
106
+ let published;
107
+ let started = false;
108
+ let disposed = false;
109
+ let queued = false;
110
+ let revision = 0;
111
+ const changed = () => {
112
+ if (disposed)
113
+ return;
114
+ revision++;
115
+ options.invalidate?.();
116
+ if (!started || queued)
117
+ return;
118
+ queued = true;
119
+ queueMicrotask(flush);
120
+ };
121
+ function flush() {
122
+ queued = false;
123
+ if (disposed)
124
+ return;
125
+ options.refresh?.();
126
+ if (disposed)
127
+ return;
128
+ const version = revision;
129
+ const value = options.project();
130
+ const next = protectSnapshot(options.reconcile ? options.reconcile(published, value) : value);
131
+ if (disposed || version !== revision)
132
+ return;
133
+ if (next !== published) {
134
+ published = next;
135
+ // oxlint-disable-next-line unicorn/no-useless-spread -- Reentrant subscriptions start with the next publication.
136
+ for (const listener of [...listeners]) {
137
+ if (disposed)
138
+ break;
139
+ if (listeners.has(listener)) {
140
+ try {
141
+ listener(next);
142
+ }
143
+ catch (error) {
144
+ reportSafely(reportError, error);
145
+ }
146
+ }
147
+ }
148
+ }
149
+ if (!disposed)
150
+ options.afterPublish?.();
151
+ }
152
+ return {
153
+ get disposed() {
154
+ return disposed;
155
+ },
156
+ changed,
157
+ watch(source) {
158
+ if (disposed)
159
+ return () => { };
160
+ const unsubscribe = source.subscribe(changed);
161
+ let active = true;
162
+ const stop = () => {
163
+ if (!active)
164
+ return;
165
+ active = false;
166
+ subscriptions.delete(stop);
167
+ unsubscribe();
168
+ };
169
+ if (disposed)
170
+ stop();
171
+ else
172
+ subscriptions.add(stop);
173
+ return stop;
174
+ },
175
+ start() {
176
+ if (disposed || started)
177
+ return;
178
+ options.refresh?.();
179
+ if (disposed)
180
+ return;
181
+ const version = revision;
182
+ published = protectSnapshot(options.project());
183
+ started = true;
184
+ if (version !== revision)
185
+ changed();
186
+ },
187
+ model() {
188
+ if (!started)
189
+ throw new Error('Start projection publication before reading its model');
190
+ return published;
191
+ },
192
+ subscribe(listener) {
193
+ if (disposed)
194
+ return () => { };
195
+ listeners.add(listener);
196
+ return () => {
197
+ listeners.delete(listener);
198
+ };
199
+ },
200
+ dispose() {
201
+ if (disposed)
202
+ return;
203
+ disposed = true;
204
+ listeners.clear();
205
+ const errors = [];
206
+ for (const stop of [...subscriptions].reverse()) {
207
+ try {
208
+ stop();
209
+ }
210
+ catch (error) {
211
+ errors.push(error);
212
+ }
213
+ }
214
+ if (errors.length)
215
+ throw new AggregateError(errors, 'Projection subscription cleanup failed');
216
+ },
217
+ };
218
+ }
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,13 +1,14 @@
1
1
  import type { Snapshot } from './snapshot.js';
2
- import type { ModelOwner, TaskPolicy } from './owner.js';
3
- import { Effect } from 'effect';
2
+ import type { TaskPolicy } from './owner.js';
3
+ import * as Effect from 'effect/Effect';
4
+ import type * as Scope from 'effect/Scope';
4
5
  import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
5
6
  import type { View } from './dom.js';
6
7
  import { type CommandSlot, type Send, type RunningProgram } from './program.js';
7
8
  import { type UiRuntime } from './runtime.js';
8
9
  export interface TaskDefinition<Model, Input, A, E, R = never> {
9
10
  readonly policy: Exclude<TaskPolicy, 'parallel'>;
10
- readonly run: (model: Snapshot<Model>, input: Input) => Effect.Effect<A, E, R>;
11
+ readonly run: (model: Snapshot<Model>, input: Input) => Effect.Effect<A, E, R | Scope.Scope>;
11
12
  /** Domain identity, compared after fields or props change. Resets this slot only. */
12
13
  readonly identity?: (model: Snapshot<Model>) => unknown;
13
14
  }
@@ -55,19 +56,21 @@ interface ControllerTask<R> {
55
56
  /** Actions sharing a slot share cancellation and concurrency rules. Defaults to a fresh operation identity for this definition. */
56
57
  readonly slot?: CommandSlot;
57
58
  }
58
- export declare function defineTasks<Owner extends Pick<ModelOwner<object>, 'run'>, T extends Record<string, ControllerTask<Effect.Services<Parameters<Owner['run']>[1]>>>>(owner: Owner, definitions: T): {
59
+ export declare function defineTasks<Owner extends {
60
+ readonly run: (slot: CommandSlot, effect: Effect.Effect<never>, policy: TaskPolicy) => void;
61
+ }, T extends Record<string, ControllerTask<Effect.Services<Parameters<Owner['run']>[1]>>>>(owner: Owner, definitions: T): {
59
62
  readonly [K in keyof T]: (...args: Parameters<T[K]['run']>) => void;
60
63
  };
61
64
  export declare function defineTasks<Props, State extends object, R>(definition: Init<Props, State> & {
62
65
  runtime: UiRuntime<R>;
63
66
  }): ReturnType<typeof taskBuilder<Props, State, R>>;
64
67
  export declare function defineTasks<Props, State extends object>(definition: Init<Props, State>): ReturnType<typeof taskBuilder<Props, State, never>>;
65
- declare function taskBuilder<Props, State extends object, R>(definition: Init<Props, State>, runtime: UiRuntime<R>): {
68
+ declare function taskBuilder<Props, State extends object, R>(definition: Init<Props, State>, runtime: UiRuntime<R> | undefined): {
66
69
  tasks<T extends Definitions<State & {
67
70
  readonly props: Props;
68
71
  }, R>>(definitions: T): {
69
72
  slot: (name: keyof T) => CommandSlot;
70
- create: (props: Props | Snapshot<Props>) => RunningProgram<TasksModel<Props, State, T>, TasksMessage<State, T>>;
73
+ create: (props: Props | Snapshot<Props>, ownerRuntime?: UiRuntime<never>) => RunningProgram<TasksModel<Props, State, T>, TasksMessage<State, T>>;
71
74
  receive: (source: RunningProgram<TasksModel<Props, State, T>, TasksMessage<State, T>>, props: Props | Snapshot<Props>) => void;
72
75
  controls: (send: Send<TasksMessage<State, T>>) => {
73
76
  run: <K extends keyof T>(task: K, ...input: InputArguments<T[K]>) => void;
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';
@@ -13,7 +14,7 @@ export function defineTasks(definition, definitions) {
13
14
  }
14
15
  return actions;
15
16
  }
16
- return taskBuilder(definition, definition.runtime ?? defaultUiRuntime);
17
+ return taskBuilder(definition, definition.runtime);
17
18
  }
18
19
  function stopWaiting(result) {
19
20
  if (!result.waiting)
@@ -46,9 +47,11 @@ function taskBuilder(definition, runtime) {
46
47
  return { model, cancel };
47
48
  };
48
49
  const owners = new WeakMap();
49
- const create = (props) => {
50
+ const create = (props, ownerRuntime = defaultUiRuntime) => {
51
+ const execution = runtime ?? ownerRuntime;
50
52
  const source = program({
51
53
  initial: init(props),
54
+ runtime: ownerRuntime,
52
55
  ...(definition.name ? { name: definition.name } : {}),
53
56
  update: (snapshot, message) => {
54
57
  // Internal immutable reconstruction retains the declared domain types.
@@ -72,7 +75,7 @@ function taskBuilder(definition, runtime) {
72
75
  model: withResult(model, message.task, AsyncResult.waiting(previous)),
73
76
  commands: [
74
77
  {
75
- ...effectCommand(slot(message.task), () => runtime.provide(task.run(snapshot, message.input)), {
78
+ ...effectCommand(slot(message.task), () => execution.provideScoped(task.run(snapshot, message.input)), {
76
79
  policy: task.policy,
77
80
  onSuccess: (value) => ({
78
81
  type: 'Settled',
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.1",
3
+ "version": "0.5.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": {
@@ -14,8 +14,7 @@
14
14
  },
15
15
  "files": [
16
16
  "dist",
17
- "LICENSE",
18
- "README.md"
17
+ "LICENSE"
19
18
  ],
20
19
  "type": "module",
21
20
  "sideEffects": false,
@@ -96,6 +95,14 @@
96
95
  "types": "./dist/runtime.d.ts",
97
96
  "import": "./dist/runtime.js"
98
97
  },
98
+ "./source": {
99
+ "types": "./dist/source.d.ts",
100
+ "import": "./dist/source.js"
101
+ },
102
+ "./render": {
103
+ "types": "./dist/render.d.ts",
104
+ "import": "./dist/render.js"
105
+ },
99
106
  "./form": {
100
107
  "types": "./dist/form.d.ts",
101
108
  "import": "./dist/form.js"
package/README.md DELETED
@@ -1,101 +0,0 @@
1
- # effectweb
2
-
3
- Immutable Effect models and JSX compiled to direct DOM updates, without signals, proxies, or virtual DOM.
4
-
5
- Use with `@effectweb/compiler/vite` and Effect `4.0.0-rc.112`. The package includes programs, named tasks, async presentation, query caching, DOM lifetimes, and `effectweb/testing` helpers.
6
-
7
- See [setup and example](https://github.com/DerpyCrabs/EffectWeb#vite-setup).
8
-
9
- Runtime and compiler versions advance together. Client-side only. Persistence and multi-tab coordination belong to the application.
10
-
11
- ## Query identity and account ownership
12
-
13
- A query definition has its own identity. Within one cache, all request arguments form its key automatically:
14
-
15
- ```ts
16
- const page = query({
17
- name: 'message-page',
18
- load: (args: { accountId: string; threadId: string; cursor?: string }) =>
19
- api.messages(args.accountId, args.threadId, args.cursor),
20
- });
21
- ```
22
-
23
- Request arguments use the same canonical encoding for cache lookup, prefetch, writes, invalidation, and `queryResource.select`. Object property order is ignored; array order matters. Strings, finite numbers, booleans, `null`, and explicit `undefined` are supported recursively in dense arrays and plain objects. Missing properties differ from properties containing `undefined`, and `0` differs from `-0`. Functions, symbols, bigint, nonfinite numbers, cycles, class instances, accessors, nonenumerable properties, sparse arrays, and arrays with extra properties throw `TypeError`. Repeated references to the same plain object are supported. Published argument objects are frozen and are not cloned.
24
-
25
- Pass every load-relevant value as an argument, including account, filters, pagination, locale, and permissions when they affect the result. Custom `key` projections are rejected. Provide service instances through the Effect environment instead of query arguments. Separate query definitions do not share entries even if their names and arguments match. `select(undefined)` disables selection; an explicit `undefined` inside an argument object remains part of its identity. A query without arguments uses `true` for selection and prefetch.
26
-
27
- Give authenticated data an account lifetime:
28
-
29
- ```ts
30
- const account = modelOwner({ accountId }, { runtime });
31
- const cache = account.own(makeQueryCache(runtime));
32
- const messages = observeQuery(account, cache, page, (result) => {
33
- // Publish the immutable result into application state.
34
- });
35
- messages.select({ accountId, threadId });
36
-
37
- // Sign out or switch account: dispose this lifetime, then create the next one.
38
- account.dispose();
39
- ```
40
-
41
- Independent caches isolate account data even when services derive authentication from their environment. If an application deliberately reuses a cache across accounts, include account identity in every authenticated query key and call `cache.resetResources()` at the boundary before selecting the new account. Reset interrupts old resources and returns existing query observers to their initial state; select again to enter the new generation. Disposing only a query observer releases its subscription; it does not erase shared cached values. Own shared caches at the account or application scope, not in individual views.
42
-
43
- Use `cache.setQueryData(query, args, value)` to seed or replace a cached success, and `cache.updateQueryData(query, args, update)` to change an existing success. The updater receives readonly data; returning `undefined` skips the write. Both publish to current observers and supersede pending loads for those arguments. Use `setQueryData` to store a successful `undefined` value. Query definitions and cache registries are opaque; use the public cache and observation methods.
44
-
45
- ## Write concurrency
46
-
47
- `modelOwner.run(slot, effect, policy)` and `defineTasks(owner, definitions)` support these policies:
48
-
49
- | Policy | Behavior for an occupied slot |
50
- | --------------- | ----------------------------------------------------------------------- |
51
- | `drop` | Ignore the new request. |
52
- | `replace` | Cancel active work and discard pending requests; start the new request. |
53
- | `parallel` | Start the new request alongside active work. |
54
- | `queue` | Append the request; run it after earlier work finishes. |
55
- | `latest-queued` | Keep active work; replace all pending requests with the newest request. |
56
-
57
- Every command requires an explicit `policy`. Create stable operation identities with `commandSlot('save')`; equal diagnostic names do not share a slot. Component `defineTasks(...).tasks(...)` and `taskComponent` support all except `parallel`, since their single result slot represents serial work. Command mapping and runtime service provisioning preserve the policy. Actions sharing a slot share concurrency and cancellation. A queued request waits for every active request in that slot if policies are mixed; keep a consistent policy per slot for predictable write behavior.
58
-
59
- Use `queue` when each accepted operation matters, such as appending messages. Use `latest-queued` when saving complete document snapshots and only the newest pending snapshot matters:
60
-
61
- ```ts
62
- const writes = defineTasks(owner, {
63
- save: { policy: 'latest-queued', run: (document: Snapshot<Document>) => storage.save(document) },
64
- });
65
-
66
- writes.save(owner.read().document);
67
- ```
68
-
69
- Component tasks capture the model snapshot and input when `Run` is submitted, including requests that wait in a queue. Editing fields afterward does not replace that captured model. Controller tasks retain their supplied arguments; their factories execute when work starts. Pass `owner.read()` data as arguments to capture submission state, or read inside the Effect when execution-time state is intended. Effects and inputs are retained, not deep-cloned; keep supplied data immutable. Dropped and coalesced pending factories are never invoked.
70
-
71
- Queue progress continues after successes, typed failures, or defects. Component results remain `waiting` while more work is pending, publish each settlement, and retain the latest successful value if a later write fails. Action failures use the owner's error reporter. `cancel`, component reset or identity change, and disposal discard pending requests and interrupt active work; stale command completions cannot publish afterward. `awaitIdle` includes pending work. Cancellation cannot undo an external write that already completed. Transactions admit their whole command batch before starting Effects, so replacements and cancellation can remove superseded work without executing it.
72
-
73
- ## Lazy views and portals
74
-
75
- `lazyView(() => fromPromise(() => import('./Reader').then((module) => module.Reader)))` loads a view when mounted. It accepts typed `pending` and `failure` views and a `runtime` when the loader requires services. Each unresolved placement owns its load; unmounting interrupts it. Successful definitions are cached for future mounts. Render a lazy view through `ViewBinding` with its current model and sender.
76
-
77
- `<Portal mount={model.dialogHost}>...</Portal>` renders into a supplied HTML or SVG element. Omitting `mount` uses the document body. Portal content retains its owner, events, and cleanup; changing between HTML and SVG targets rebuilds content in the correct namespace.
78
-
79
- ## Inspect source dependencies
80
-
81
- Mount a development panel before mounting the application so it sees initial evaluations:
82
-
83
- ```ts
84
- import { mountBindingInspector } from 'effectweb/diagnostics';
85
-
86
- const removeInspector = mountBindingInspector(document.querySelector<HTMLElement>('#inspector')!);
87
- // Mount the application here. On teardown or HMR:
88
- // removeInspector();
89
- ```
90
-
91
- The live, filterable table shows original file/line/column, source expressions, inferred snapshot dependencies, the latest changed dependency names, and derive/binding evaluation counts. It uses development compiler metadata; production builds emit none. Counts include initial evaluations, aggregate instances of the same source expression, and measure evaluations rather than actual DOM writes. Change reasons use reference/value equality, without retaining previous or next values.
92
-
93
- For custom tooling, `inspectBindings({ limit: 200 })` returns `entries()`, `subscribe(listener)`, `clear()`, and `dispose()`. Entries are immutable metadata, newest first, with at most 1000 source records. Least recently updated sources are evicted and start fresh if seen again. `dispose()` unsubscribes and clears retained metadata. `mountBindingInspector(element, inspector)` can share an inspector; removing that panel leaves the supplied inspector running. Low-level `observeBindings` remains available.
94
-
95
- The inspector covers instrumented derivations and text/attribute bindings; it is not a snapshot recorder, time-travel debugger, or complete profile of branch/list reconciliation. Source labels describe the compiler's inferred dependencies, not a proof that an opaque helper has no hidden state.
96
-
97
- ## Immutable inputs and outputs
98
-
99
- Published `Snapshot<T>` values are recursively readonly, including nested arrays/tuples and async success data. View/slot composition, component inputs, owner patches, and task field updates accept readonly branches without casts. Query cache reads, subscriptions, and prefetch publish the same readonly data. Plain objects and arrays are frozen before publication in every build; snapshot protection cannot be disabled.
100
-
101
- Functions, Effects, DOM nodes, and explicitly marked service classes retain their own API. See the [authoring guide](https://github.com/DerpyCrabs/EffectWeb/blob/main/docs/authoring.md) for migration examples, `SnapshotOpaque`, state ownership, form composition, and reconciling async loads with live updates.
@@ -1,2 +0,0 @@
1
- /** Preserve receiver identity and JavaScript call/optional-chain semantics. */
2
- export declare function intrinsic<T>(receiver: T, path: readonly (string | null)[], method: string): T;
package/dist/intrinsic.js DELETED
@@ -1,120 +0,0 @@
1
- import { compiledSlots } from './slotIdentity.js';
2
- // Compiler implementation detail. An untyped receiver's method name is not
3
- // evidence that it is a standard data operation. Validate its descriptor before
4
- // invoking it, without running a user getter in the process.
5
- const prototypes = [
6
- Array.prototype,
7
- String.prototype,
8
- Number.prototype,
9
- Boolean.prototype,
10
- BigInt.prototype,
11
- Function.prototype,
12
- Date.prototype,
13
- RegExp.prototype,
14
- Map.prototype,
15
- Set.prototype,
16
- URL.prototype,
17
- URLSearchParams.prototype,
18
- Blob.prototype,
19
- File.prototype,
20
- Object.getPrototypeOf(Uint8Array.prototype),
21
- Intl.DateTimeFormat.prototype,
22
- Intl.NumberFormat.prototype,
23
- Intl.Collator.prototype,
24
- Intl.RelativeTimeFormat.prototype,
25
- Intl.PluralRules.prototype,
26
- Intl.ListFormat.prototype,
27
- Intl.Segmenter.prototype,
28
- ];
29
- const operations = new Map();
30
- for (const prototype of prototypes) {
31
- for (const [name, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(prototype))) {
32
- if (name === 'constructor')
33
- continue;
34
- const entries = operations.get(name) ?? [];
35
- entries.push(descriptor);
36
- operations.set(name, entries);
37
- }
38
- }
39
- /** Preserve receiver identity and JavaScript call/optional-chain semantics. */
40
- export function intrinsic(receiver, path, method) {
41
- checkPath(receiver, path, 0, method);
42
- return receiver;
43
- }
44
- function checkPath(target, path, index, method) {
45
- if (target === null || target === undefined)
46
- return;
47
- if (index === path.length) {
48
- checkIntrinsic(target, method);
49
- return;
50
- }
51
- const field = path[index];
52
- if (field === null) {
53
- if (Array.isArray(target)) {
54
- for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(target))) {
55
- if (!/^(0|[1-9]\d*)$/u.test(key))
56
- continue;
57
- if (!Object.hasOwn(descriptor, 'value'))
58
- throw new Error('EffectWeb render data cannot use an array accessor. Pass immutable values.');
59
- checkPath(descriptor.value, path, index + 1, method);
60
- }
61
- }
62
- else if (target instanceof Map) {
63
- for (const value of Map.prototype.values.call(target))
64
- checkPath(value, path, index + 1, method);
65
- }
66
- else if (target instanceof Set) {
67
- for (const value of Set.prototype.values.call(target))
68
- checkPath(value, path, index + 1, method);
69
- }
70
- return;
71
- }
72
- let object = Object(target);
73
- while (object) {
74
- const descriptor = Object.getOwnPropertyDescriptor(object, field);
75
- if (descriptor) {
76
- if (Object.hasOwn(descriptor, 'value')) {
77
- checkPath(descriptor.value, path, index + 1, method);
78
- }
79
- else if (descriptor.get &&
80
- operations.get(field)?.some((operation) => operation.get === descriptor.get)) {
81
- // Native getters are checked by identity before execution. File/Blob
82
- // metadata and collection sizes expose data through readonly accessors.
83
- checkPath(descriptor.get.call(target), path, index + 1, method);
84
- }
85
- else {
86
- throw new Error('EffectWeb render data cannot use an accessor to supply a standard data operation. Pass immutable data or a checked helper.');
87
- }
88
- return;
89
- }
90
- object = Object.getPrototypeOf(object);
91
- }
92
- }
93
- function checkIntrinsic(receiver, method) {
94
- if (receiver === null || receiver === undefined)
95
- return;
96
- if (method === '@slot') {
97
- if (typeof receiver !== 'function' || !compiledSlots.has(receiver))
98
- throw new Error('EffectWeb render callbacks must be compiled slots. Declare markup with slot(), or use a checked helper with an explicit purity contract.');
99
- return;
100
- }
101
- let object = Object(receiver);
102
- while (object) {
103
- const descriptor = Object.getOwnPropertyDescriptor(object, method);
104
- if (descriptor) {
105
- const known = operations
106
- .get(method)
107
- ?.some((operation) => Object.hasOwn(descriptor, 'value')
108
- ? typeof descriptor.value === 'function' && descriptor.value === operation.value
109
- : descriptor.get !== undefined &&
110
- descriptor.get === operation.get &&
111
- descriptor.set === undefined);
112
- if (!known)
113
- throw new Error(`EffectWeb cannot prove .${method} is a standard data operation. Use a checked local helper or an imported helper with a purity contract.`);
114
- return;
115
- }
116
- object = Object.getPrototypeOf(object);
117
- }
118
- // The original member call retains its own missing/optional-method behavior.
119
- return;
120
- }
@@ -1 +0,0 @@
1
- export declare const compiledSlots: WeakSet<object>;
@@ -1,2 +0,0 @@
1
- // Shared only by compiler runtime helpers; user functions cannot acquire this proof.
2
- export const compiledSlots = new WeakSet();