effectweb 0.3.0 → 0.3.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/dist/AsyncContent.js +1 -1
- package/dist/boundary.d.ts +11 -0
- package/dist/boundary.js +66 -0
- package/dist/cache-internals.d.ts +4 -0
- package/dist/cache.d.ts +20 -4
- package/dist/cache.js +174 -25
- package/dist/component.d.ts +3 -0
- package/dist/component.js +70 -10
- package/dist/dom.d.ts +12 -4
- package/dist/dom.js +38 -20
- package/dist/effectEvent.d.ts +2 -1
- package/dist/effectEvent.js +4 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +5 -2
- package/dist/infinite-query.d.ts +42 -0
- package/dist/infinite-query.js +150 -0
- package/dist/intrinsic.d.ts +2 -0
- package/dist/intrinsic.js +120 -0
- package/dist/keyed-tasks.d.ts +29 -0
- package/dist/keyed-tasks.js +97 -0
- package/dist/lazy.js +9 -1
- package/dist/mount.d.ts +3 -1
- package/dist/mount.js +29 -11
- package/dist/owner.d.ts +3 -1
- package/dist/owner.js +39 -3
- package/dist/pages.d.ts +15 -14
- package/dist/pages.js +6 -4
- package/dist/program.d.ts +10 -0
- package/dist/program.js +71 -24
- package/dist/query-internals.d.ts +4 -2
- package/dist/query.d.ts +6 -0
- package/dist/query.js +7 -1
- package/dist/session.js +13 -4
- package/dist/settlement.d.ts +7 -0
- package/dist/settlement.js +27 -0
- package/dist/sharing.js +16 -13
- package/dist/slotIdentity.d.ts +1 -0
- package/dist/slotIdentity.js +2 -0
- package/dist/snapshot.js +32 -24
- package/dist/tasks.js +1 -0
- package/package.json +9 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Cause, Effect, Exit } from 'effect';
|
|
2
|
+
import { commandSlot, program } from './program.js';
|
|
3
|
+
import { defaultUiRuntime } from './runtime.js';
|
|
4
|
+
import { reportError, reportSafely } from './errors.js';
|
|
5
|
+
import { protectSnapshot } from './snapshot.js';
|
|
6
|
+
/** Per-entity serialization. Inputs are captured now; run reads services when work starts. */
|
|
7
|
+
export function keyedTasks(owner, definition, ...provided) {
|
|
8
|
+
const runtime = provided[0] ?? defaultUiRuntime;
|
|
9
|
+
const entries = new Map();
|
|
10
|
+
let disposed = false;
|
|
11
|
+
const slot = commandSlot(definition.name);
|
|
12
|
+
const makeSource = () => program({
|
|
13
|
+
initial: null,
|
|
14
|
+
update: (model, message) => 'command' in message ? { model, commands: [message.command] } : { model, cancel: [slot] },
|
|
15
|
+
});
|
|
16
|
+
// Each key owns a program, including its interrupted fibers, so drain never waits on other keys.
|
|
17
|
+
const get = (key) => {
|
|
18
|
+
let entry = entries.get(key);
|
|
19
|
+
if (!entry) {
|
|
20
|
+
entry = { source: makeSource(), version: 0, retiring: false };
|
|
21
|
+
entries.set(key, entry);
|
|
22
|
+
}
|
|
23
|
+
return entry;
|
|
24
|
+
};
|
|
25
|
+
const dispose = () => {
|
|
26
|
+
disposed = true;
|
|
27
|
+
for (const entry of entries.values())
|
|
28
|
+
entry.source.dispose();
|
|
29
|
+
};
|
|
30
|
+
const close = async () => {
|
|
31
|
+
dispose();
|
|
32
|
+
await Promise.all([...entries.values()].map((entry) => entry.source.close()));
|
|
33
|
+
entries.clear();
|
|
34
|
+
};
|
|
35
|
+
owner.own({ dispose, close });
|
|
36
|
+
return {
|
|
37
|
+
submit(key, input) {
|
|
38
|
+
if (disposed || owner.disposed)
|
|
39
|
+
return { outcome: Promise.resolve(Object.freeze({ _tag: 'Cancelled' })) };
|
|
40
|
+
const captured = protectSnapshot(input);
|
|
41
|
+
const entry = get(key);
|
|
42
|
+
entry.version++;
|
|
43
|
+
let settle;
|
|
44
|
+
const outcome = new Promise((resolve) => {
|
|
45
|
+
settle = (result) => resolve(protectSnapshot(result));
|
|
46
|
+
});
|
|
47
|
+
outcome
|
|
48
|
+
.then(async () => {
|
|
49
|
+
if (entry.retiring)
|
|
50
|
+
return;
|
|
51
|
+
entry.retiring = true;
|
|
52
|
+
let version;
|
|
53
|
+
do {
|
|
54
|
+
version = entry.version;
|
|
55
|
+
await entry.source.awaitStopped();
|
|
56
|
+
} while (version !== entry.version);
|
|
57
|
+
if (entries.get(key) === entry) {
|
|
58
|
+
entries.delete(key);
|
|
59
|
+
entry.source.dispose();
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
.catch((error) => reportSafely(reportError, error));
|
|
63
|
+
const action = Effect.uninterruptibleMask((restore) => restore(Effect.suspend(() => definition.run(key, captured)).pipe(Effect.map((value) => protectSnapshot(value)))).pipe(Effect.exit, Effect.flatMap((exit) => Effect.sync(() => settle(Exit.isSuccess(exit)
|
|
64
|
+
? { _tag: 'Success', value: exit.value }
|
|
65
|
+
: Cause.hasInterruptsOnly(exit.cause)
|
|
66
|
+
? { _tag: 'Cancelled' }
|
|
67
|
+
: { _tag: 'Failure', cause: exit.cause })))));
|
|
68
|
+
entry.source.send({
|
|
69
|
+
command: {
|
|
70
|
+
slot,
|
|
71
|
+
policy: definition.policy,
|
|
72
|
+
action: runtime.provide(action),
|
|
73
|
+
onDiscard: (_tag) => settle({ _tag }),
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
return { outcome };
|
|
77
|
+
},
|
|
78
|
+
cancel(key) {
|
|
79
|
+
const entry = entries.get(key);
|
|
80
|
+
if (entry) {
|
|
81
|
+
entry.version++;
|
|
82
|
+
entry.source.send({ cancel: true });
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
async drain(...selected) {
|
|
86
|
+
const pending = selected.length
|
|
87
|
+
? [entries.get(selected[0])].filter((entry) => !!entry)
|
|
88
|
+
: [...entries.values()];
|
|
89
|
+
await Promise.all(pending.map(async (entry) => {
|
|
90
|
+
await entry.source.awaitIdle();
|
|
91
|
+
await entry.source.awaitStopped();
|
|
92
|
+
}));
|
|
93
|
+
},
|
|
94
|
+
close,
|
|
95
|
+
dispose,
|
|
96
|
+
};
|
|
97
|
+
}
|
package/dist/lazy.js
CHANGED
|
@@ -25,7 +25,15 @@ export function lazyView(load, ...provided) {
|
|
|
25
25
|
scope.cleanups.push(interrupt);
|
|
26
26
|
if (scope.disposed)
|
|
27
27
|
return;
|
|
28
|
-
|
|
28
|
+
const finished = scope.settlement.begin();
|
|
29
|
+
try {
|
|
30
|
+
fiber = Effect.runFork(runtime.provide(Effect.suspend(load)));
|
|
31
|
+
fiber.addObserver(finished);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
finished();
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
29
37
|
if (scope.disposed) {
|
|
30
38
|
interrupt();
|
|
31
39
|
return;
|
package/dist/mount.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export declare function domMount<T extends Element, R = never>(start: (element:
|
|
|
17
17
|
/** A stable DOM lifecycle with fresh immutable inputs, for focus, measurements and native events. */
|
|
18
18
|
export declare function domBinding<T extends Element, A, R = never>(data: A | Snapshot<A>, acquire: (element: T, input: () => Snapshot<A>) => Work<R>, ...provided: [R] extends [never] ? [runtime?: UiRuntime<R>] : [runtime: UiRuntime<R>]): DomMount<T>;
|
|
19
19
|
export declare function startMount<T extends Element>(element: T, mount: DomMount<T>, report?: ReportError): {
|
|
20
|
+
closed: Promise<void>;
|
|
21
|
+
close(): Promise<void>;
|
|
20
22
|
update(next: DomMount<T>): boolean;
|
|
21
|
-
dispose()
|
|
23
|
+
dispose: () => void;
|
|
22
24
|
};
|
package/dist/mount.js
CHANGED
|
@@ -31,11 +31,39 @@ export function startMount(element, mount, report = reportError) {
|
|
|
31
31
|
const work = mount.acquire(element, () => data);
|
|
32
32
|
const fiber = Effect.isEffect(work) ? Effect.runFork(work) : undefined;
|
|
33
33
|
let disposed = false;
|
|
34
|
+
let finish;
|
|
35
|
+
const closed = new Promise((resolve) => {
|
|
36
|
+
finish = resolve;
|
|
37
|
+
});
|
|
34
38
|
fiber?.addObserver((exit) => {
|
|
35
39
|
if (exit._tag === 'Failure' && !disposed)
|
|
36
40
|
reportSafely(report, exit.cause);
|
|
41
|
+
finish();
|
|
37
42
|
});
|
|
43
|
+
const dispose = () => {
|
|
44
|
+
if (disposed)
|
|
45
|
+
return;
|
|
46
|
+
disposed = true;
|
|
47
|
+
if (fiber)
|
|
48
|
+
Effect.runFork(Fiber.interrupt(fiber));
|
|
49
|
+
else {
|
|
50
|
+
try {
|
|
51
|
+
if (typeof work === 'function')
|
|
52
|
+
work();
|
|
53
|
+
else if ('dispose' in work)
|
|
54
|
+
work.dispose();
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
finish();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
38
61
|
return {
|
|
62
|
+
closed,
|
|
63
|
+
close() {
|
|
64
|
+
dispose();
|
|
65
|
+
return closed;
|
|
66
|
+
},
|
|
39
67
|
update(next) {
|
|
40
68
|
if (next.identity !== mount.identity)
|
|
41
69
|
return false;
|
|
@@ -44,16 +72,6 @@ export function startMount(element, mount, report = reportError) {
|
|
|
44
72
|
work.update?.();
|
|
45
73
|
return true;
|
|
46
74
|
},
|
|
47
|
-
dispose
|
|
48
|
-
if (disposed)
|
|
49
|
-
return;
|
|
50
|
-
disposed = true;
|
|
51
|
-
if (fiber)
|
|
52
|
-
Effect.runFork(Fiber.interrupt(fiber));
|
|
53
|
-
else if (typeof work === 'function')
|
|
54
|
-
work();
|
|
55
|
-
else if ('dispose' in work)
|
|
56
|
-
work.dispose();
|
|
57
|
-
},
|
|
75
|
+
dispose,
|
|
58
76
|
};
|
|
59
77
|
}
|
package/dist/owner.d.ts
CHANGED
|
@@ -24,7 +24,9 @@ export interface ModelOwner<Model extends object, R = never> extends DisposableO
|
|
|
24
24
|
readonly run: (slot: CommandSlot, effect: Effect.Effect<unknown, unknown, R>, policy: TaskPolicy) => void;
|
|
25
25
|
readonly cancel: (slot: CommandSlot) => void;
|
|
26
26
|
readonly isRunning: (slot: CommandSlot) => boolean;
|
|
27
|
-
readonly awaitIdle: () => Promise<void>;
|
|
27
|
+
readonly awaitIdle: (slot?: CommandSlot) => Promise<void>;
|
|
28
|
+
/** Interrupt and join work before closing owned dependencies in reverse order. */
|
|
29
|
+
readonly close: () => Promise<void>;
|
|
28
30
|
readonly dispose: () => void;
|
|
29
31
|
}
|
|
30
32
|
type Options = {
|
package/dist/owner.js
CHANGED
|
@@ -7,6 +7,7 @@ export function modelOwner(initial, options = {}) {
|
|
|
7
7
|
let disposed = false;
|
|
8
8
|
let staged;
|
|
9
9
|
const cleanups = [];
|
|
10
|
+
let closing;
|
|
10
11
|
const source = program({
|
|
11
12
|
initial,
|
|
12
13
|
...options,
|
|
@@ -56,10 +57,44 @@ export function modelOwner(initial, options = {}) {
|
|
|
56
57
|
return;
|
|
57
58
|
disposed = true;
|
|
58
59
|
source.dispose();
|
|
59
|
-
runAll(cleanups
|
|
60
|
+
runAll(cleanups
|
|
61
|
+
.splice(0)
|
|
62
|
+
.reverse()
|
|
63
|
+
.map((resource) => () => resource.dispose()), options.onDefect ?? reportError);
|
|
64
|
+
};
|
|
65
|
+
const close = () => {
|
|
66
|
+
if (closing)
|
|
67
|
+
return closing;
|
|
68
|
+
disposed = true;
|
|
69
|
+
const resources = cleanups.splice(0).reverse();
|
|
70
|
+
let finish;
|
|
71
|
+
let fail;
|
|
72
|
+
closing = new Promise((resolve, reject) => {
|
|
73
|
+
finish = resolve;
|
|
74
|
+
fail = reject;
|
|
75
|
+
});
|
|
76
|
+
const closeResources = async () => {
|
|
77
|
+
await source.close();
|
|
78
|
+
const errors = [];
|
|
79
|
+
for (const resource of resources) {
|
|
80
|
+
try {
|
|
81
|
+
if (resource.close)
|
|
82
|
+
await resource.close();
|
|
83
|
+
else
|
|
84
|
+
resource.dispose();
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
errors.push(error);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (errors.length)
|
|
91
|
+
throw new AggregateError(errors, 'Owner cleanup failed.');
|
|
92
|
+
};
|
|
93
|
+
closeResources().then(finish, fail);
|
|
94
|
+
return closing;
|
|
60
95
|
};
|
|
61
96
|
return {
|
|
62
|
-
source: { model: source.model, send: source.send, subscribe: source.subscribe, dispose },
|
|
97
|
+
source: { model: source.model, send: source.send, subscribe: source.subscribe, dispose, close },
|
|
63
98
|
read,
|
|
64
99
|
patch,
|
|
65
100
|
fields: (...keys) => {
|
|
@@ -114,11 +149,12 @@ export function modelOwner(initial, options = {}) {
|
|
|
114
149
|
get disposed() {
|
|
115
150
|
return disposed;
|
|
116
151
|
},
|
|
152
|
+
close,
|
|
117
153
|
own(resource) {
|
|
118
154
|
if (disposed)
|
|
119
155
|
resource.dispose();
|
|
120
156
|
else
|
|
121
|
-
cleanups.push(
|
|
157
|
+
cleanups.push(resource);
|
|
122
158
|
return resource;
|
|
123
159
|
},
|
|
124
160
|
dispose,
|
package/dist/pages.d.ts
CHANGED
|
@@ -3,10 +3,11 @@ import { Cause } from 'effect';
|
|
|
3
3
|
import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
|
|
4
4
|
import type { UiLoad, UiPage } from './load.js';
|
|
5
5
|
import { type Transition, type RunningProgram } from './program.js';
|
|
6
|
-
|
|
6
|
+
import { type UiRuntime } from './runtime.js';
|
|
7
|
+
export interface PagesModel<Props, A, Cursor, E = unknown> {
|
|
7
8
|
readonly props: Props;
|
|
8
9
|
readonly key: string | undefined;
|
|
9
|
-
readonly result: AsyncResult.AsyncResult<UiPage<A, Cursor>,
|
|
10
|
+
readonly result: AsyncResult.AsyncResult<UiPage<A, Cursor>, E>;
|
|
10
11
|
readonly append: boolean;
|
|
11
12
|
}
|
|
12
13
|
export type PagesRequest = {
|
|
@@ -17,34 +18,34 @@ export type PagesRequest = {
|
|
|
17
18
|
type: 'Refresh';
|
|
18
19
|
};
|
|
19
20
|
/** Low-level reducer protocol, including completions for explicit program composition. */
|
|
20
|
-
export type PagesMessage<A, Cursor> = PagesRequest | {
|
|
21
|
+
export type PagesMessage<A, Cursor, E = unknown> = PagesRequest | {
|
|
21
22
|
type: 'Loaded';
|
|
22
23
|
page: UiPage<A, Cursor>;
|
|
23
24
|
append: boolean;
|
|
24
25
|
} | {
|
|
25
26
|
type: 'Failed';
|
|
26
|
-
cause: Cause.Cause<
|
|
27
|
+
cause: Cause.Cause<E>;
|
|
27
28
|
};
|
|
28
29
|
/** An owned pagination source with immutable input updates. */
|
|
29
|
-
export interface PagesProgram<Props, A, Cursor> extends RunningProgram<PagesModel<Props, A, Cursor>, PagesRequest> {
|
|
30
|
+
export interface PagesProgram<Props, A, Cursor, E = unknown> extends RunningProgram<PagesModel<Props, A, Cursor, E>, PagesRequest> {
|
|
30
31
|
readonly receive: (props: Props | Snapshot<Props>) => void;
|
|
31
32
|
}
|
|
32
33
|
/** Reusable pagination transitions, or an independently owned source through create. */
|
|
33
|
-
export interface Pagination<Props, A, Cursor> {
|
|
34
|
-
readonly init: (props: Props | Snapshot<Props>) => PagesModel<Props, A, Cursor> | Snapshot<PagesModel<Props, A, Cursor>>;
|
|
35
|
-
readonly receive: (model: PagesModel<Props, A, Cursor> | Snapshot<PagesModel<Props, A, Cursor>>, props: Props | Snapshot<Props>, options?: {
|
|
34
|
+
export interface Pagination<Props, A, Cursor, E = unknown> {
|
|
35
|
+
readonly init: (props: Props | Snapshot<Props>) => PagesModel<Props, A, Cursor, E> | Snapshot<PagesModel<Props, A, Cursor, E>>;
|
|
36
|
+
readonly receive: (model: PagesModel<Props, A, Cursor, E> | Snapshot<PagesModel<Props, A, Cursor, E>>, props: Props | Snapshot<Props>, options?: {
|
|
36
37
|
refresh?: boolean;
|
|
37
|
-
}) => Transition<PagesModel<Props, A, Cursor>, PagesMessage<A, Cursor>>;
|
|
38
|
-
readonly update: (model: PagesModel<Props, A, Cursor> | Snapshot<PagesModel<Props, A, Cursor>>, message: PagesMessage<A, Cursor>) => Transition<PagesModel<Props, A, Cursor>, PagesMessage<A, Cursor>>;
|
|
39
|
-
readonly create: (props: Props | Snapshot<Props>) => PagesProgram<Props, A, Cursor>;
|
|
38
|
+
}) => Transition<PagesModel<Props, A, Cursor, E>, PagesMessage<A, Cursor, E>>;
|
|
39
|
+
readonly update: (model: PagesModel<Props, A, Cursor, E> | Snapshot<PagesModel<Props, A, Cursor, E>>, message: PagesMessage<A, Cursor, E>) => Transition<PagesModel<Props, A, Cursor, E>, PagesMessage<A, Cursor, E>>;
|
|
40
|
+
readonly create: (props: Props | Snapshot<Props>) => PagesProgram<Props, A, Cursor, E>;
|
|
40
41
|
}
|
|
41
42
|
/**
|
|
42
43
|
* Pagination owns the `page` command slot. Key changes clear results and replace the request;
|
|
43
44
|
* same-key refreshes keep the last success, and retries repeat the failed cursor.
|
|
44
45
|
* Feed request inputs through receive; pass { refresh: true } for explicit same-key submissions.
|
|
45
46
|
*/
|
|
46
|
-
export declare function pages<Props, A, Cursor>(definition: {
|
|
47
|
+
export declare function pages<Props, A, Cursor, E = unknown, R = never>(definition: {
|
|
47
48
|
key: (props: Snapshot<Props>) => string | undefined;
|
|
48
|
-
load: (props: Snapshot<Props>, cursor: Snapshot<Cursor> | undefined) => UiLoad<UiPage<A, Cursor
|
|
49
|
+
load: (props: Snapshot<Props>, cursor: Snapshot<Cursor> | undefined) => UiLoad<UiPage<A, Cursor>, E, R>;
|
|
49
50
|
itemKey: (item: Snapshot<A>) => string;
|
|
50
|
-
}): Pagination<Props, A, Cursor>;
|
|
51
|
+
}, ...provided: [R] extends [never] ? [runtime?: UiRuntime<R>] : [runtime: UiRuntime<R>]): Pagination<Props, A, Cursor, E>;
|
package/dist/pages.js
CHANGED
|
@@ -3,14 +3,16 @@ import { shareData } from './sharing.js';
|
|
|
3
3
|
import { Cause, Option } from 'effect';
|
|
4
4
|
import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
|
|
5
5
|
import { effectCommand, program, } from './program.js';
|
|
6
|
+
import { defaultUiRuntime } from './runtime.js';
|
|
6
7
|
import { available } from './resource.js';
|
|
7
|
-
const commandPage = commandSlot('page');
|
|
8
8
|
/**
|
|
9
9
|
* Pagination owns the `page` command slot. Key changes clear results and replace the request;
|
|
10
10
|
* same-key refreshes keep the last success, and retries repeat the failed cursor.
|
|
11
11
|
* Feed request inputs through receive; pass { refresh: true } for explicit same-key submissions.
|
|
12
12
|
*/
|
|
13
|
-
export function pages(definition) {
|
|
13
|
+
export function pages(definition, ...provided) {
|
|
14
|
+
const runtime = provided[0] ?? defaultUiRuntime;
|
|
15
|
+
const commandPage = commandSlot('page');
|
|
14
16
|
const request = (model, append) => {
|
|
15
17
|
const previous = available(model.result);
|
|
16
18
|
if (append && (model.result.waiting || !previous || previous.next === undefined))
|
|
@@ -21,11 +23,11 @@ export function pages(definition) {
|
|
|
21
23
|
return {
|
|
22
24
|
model: { ...model, append, result: AsyncResult.waiting(model.result) },
|
|
23
25
|
commands: [
|
|
24
|
-
effectCommand(commandPage, () => definition.load(model.props, cursor), {
|
|
26
|
+
runtime.command(effectCommand(commandPage, () => definition.load(model.props, cursor), {
|
|
25
27
|
policy: 'replace',
|
|
26
28
|
onSuccess: (page) => ({ type: 'Loaded', page, append }),
|
|
27
29
|
onFailure: (cause) => ({ type: 'Failed', cause }),
|
|
28
|
-
}),
|
|
30
|
+
})),
|
|
29
31
|
],
|
|
30
32
|
};
|
|
31
33
|
};
|
package/dist/program.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export declare const commandSlot: (name: string) => CommandSlot;
|
|
|
13
13
|
export type Command<Message, R = never> = {
|
|
14
14
|
readonly slot: CommandSlot;
|
|
15
15
|
readonly policy: TaskPolicy;
|
|
16
|
+
/** Notification for work that never started. */
|
|
17
|
+
readonly onDiscard?: (reason: 'Cancelled' | 'Superseded' | 'Dropped') => void;
|
|
16
18
|
} & ({
|
|
17
19
|
readonly effect: Effect.Effect<Message, never, R>;
|
|
18
20
|
readonly stream?: never;
|
|
@@ -40,13 +42,21 @@ export interface Transition<Model, Message, R = never> {
|
|
|
40
42
|
readonly commands?: readonly Command<Message, R>[];
|
|
41
43
|
readonly cancel?: readonly CommandSlot[];
|
|
42
44
|
}
|
|
45
|
+
export declare function mapTransition<Model, Message, Parent, ParentMessage, R = never>(transition: Transition<Model, Message, R>, maps: {
|
|
46
|
+
readonly model: (model: Snapshot<Model>) => Parent | Snapshot<Parent>;
|
|
47
|
+
readonly message: (message: Message) => ParentMessage;
|
|
48
|
+
}): Transition<Parent, ParentMessage, R>;
|
|
43
49
|
export interface Program<Model, Message> {
|
|
44
50
|
readonly model: () => Snapshot<Model>;
|
|
45
51
|
readonly send: Send<Message>;
|
|
46
52
|
readonly subscribe: (listener: (model: Snapshot<Model>) => void) => () => void;
|
|
47
53
|
readonly dispose: () => void;
|
|
54
|
+
readonly close?: () => Promise<void>;
|
|
48
55
|
}
|
|
49
56
|
export interface RunningProgram<Model, Message> extends Program<Model, Message> {
|
|
57
|
+
/** Wait for interrupted work and its finalizers as well as admitted work. */
|
|
58
|
+
readonly awaitStopped: () => Promise<void>;
|
|
59
|
+
readonly close: () => Promise<void>;
|
|
50
60
|
readonly activeSlots: () => readonly CommandSlot[];
|
|
51
61
|
readonly awaitIdle: (slot?: CommandSlot) => Promise<void>;
|
|
52
62
|
}
|
package/dist/program.js
CHANGED
|
@@ -23,6 +23,15 @@ export function mapCommand(command, map) {
|
|
|
23
23
|
? { ...command, stream: command.stream.pipe(Stream.map(map)) }
|
|
24
24
|
: { ...command, effect: command.effect.pipe(Effect.map(map)) };
|
|
25
25
|
}
|
|
26
|
+
export function mapTransition(transition, maps) {
|
|
27
|
+
return {
|
|
28
|
+
model: maps.model(transition.model),
|
|
29
|
+
...(transition.cancel ? { cancel: transition.cancel } : {}),
|
|
30
|
+
...(transition.commands
|
|
31
|
+
? { commands: transition.commands.map((command) => mapCommand(command, maps.message)) }
|
|
32
|
+
: {}),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
26
35
|
export function program(options) {
|
|
27
36
|
const id = nextProgramId();
|
|
28
37
|
const trace = (kind, slot, message) => hasProgramObservers() &&
|
|
@@ -40,6 +49,7 @@ export function program(options) {
|
|
|
40
49
|
});
|
|
41
50
|
const waiters = new Set();
|
|
42
51
|
const notify = () => {
|
|
52
|
+
notifyStopped();
|
|
43
53
|
for (const waiter of waiters) {
|
|
44
54
|
if (disposed ||
|
|
45
55
|
(!draining && (waiter.slot ? !running.has(waiter.slot) : running.size === 0))) {
|
|
@@ -53,11 +63,33 @@ export function program(options) {
|
|
|
53
63
|
let current = protectSnapshot(options.initial);
|
|
54
64
|
const listeners = new Set();
|
|
55
65
|
const running = new Map();
|
|
66
|
+
const live = new Set();
|
|
67
|
+
const stopped = new Set();
|
|
68
|
+
const notifyStopped = () => {
|
|
69
|
+
if (live.size || running.size || draining)
|
|
70
|
+
return;
|
|
71
|
+
for (const done of stopped)
|
|
72
|
+
done();
|
|
73
|
+
stopped.clear();
|
|
74
|
+
};
|
|
75
|
+
const awaitStopped = () => live.size === 0 && running.size === 0 && !draining
|
|
76
|
+
? Promise.resolve()
|
|
77
|
+
: new Promise((done) => stopped.add(done));
|
|
56
78
|
let disposed = false;
|
|
57
79
|
let draining = false;
|
|
58
80
|
const queue = [];
|
|
59
81
|
const deferred = [];
|
|
60
|
-
const
|
|
82
|
+
const discard = (command, reason) => {
|
|
83
|
+
if (command.onDiscard) {
|
|
84
|
+
try {
|
|
85
|
+
command.onDiscard(reason);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
reportSafely(options.onDefect ?? reportError, error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const cancel = (slot, reason = 'Cancelled') => {
|
|
61
93
|
// A synchronous Effect can enqueue completion behind a reset/replacement message.
|
|
62
94
|
// Its fiber is already done, but cancellation still owns that unpublished completion.
|
|
63
95
|
for (let index = queue.length - 1; index >= 0; index--)
|
|
@@ -67,10 +99,15 @@ export function program(options) {
|
|
|
67
99
|
running.delete(slot);
|
|
68
100
|
if (previous) {
|
|
69
101
|
trace('cancel', slot);
|
|
70
|
-
previous.pending.
|
|
71
|
-
for (const
|
|
102
|
+
const pending = previous.pending.splice(0);
|
|
103
|
+
for (const command of pending)
|
|
104
|
+
discard(command, reason);
|
|
105
|
+
for (const task of previous.active) {
|
|
72
106
|
if (task.fiber)
|
|
73
107
|
Effect.runFork(Fiber.interrupt(task.fiber));
|
|
108
|
+
else if (task.command && !live.has(task))
|
|
109
|
+
discard(task.command, reason);
|
|
110
|
+
}
|
|
74
111
|
previous.active.clear();
|
|
75
112
|
}
|
|
76
113
|
};
|
|
@@ -104,13 +141,12 @@ export function program(options) {
|
|
|
104
141
|
: command.action
|
|
105
142
|
? command.action.pipe(Effect.as(Option.none()))
|
|
106
143
|
: command.effect.pipe(Effect.map(Option.some));
|
|
144
|
+
live.add(task);
|
|
107
145
|
const fiber = Effect.runFork(effect);
|
|
108
146
|
task.fiber = fiber;
|
|
109
|
-
if (!valid()) {
|
|
110
|
-
Effect.runFork(Fiber.interrupt(fiber));
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
147
|
fiber.addObserver((exit) => {
|
|
148
|
+
live.delete(task);
|
|
149
|
+
notifyStopped();
|
|
114
150
|
if (!valid())
|
|
115
151
|
return;
|
|
116
152
|
group.active.delete(task);
|
|
@@ -133,6 +169,8 @@ export function program(options) {
|
|
|
133
169
|
}
|
|
134
170
|
notify();
|
|
135
171
|
});
|
|
172
|
+
if (!valid())
|
|
173
|
+
Effect.runFork(Fiber.interrupt(fiber));
|
|
136
174
|
};
|
|
137
175
|
const advance = (slot, group) => {
|
|
138
176
|
if (disposed || running.get(slot) !== group || group.active.size)
|
|
@@ -140,7 +178,7 @@ export function program(options) {
|
|
|
140
178
|
const next = group.pending.shift();
|
|
141
179
|
if (!next)
|
|
142
180
|
return;
|
|
143
|
-
const task = {};
|
|
181
|
+
const task = { command: next };
|
|
144
182
|
group.active.add(task);
|
|
145
183
|
launch(next, group, task);
|
|
146
184
|
};
|
|
@@ -181,15 +219,18 @@ export function program(options) {
|
|
|
181
219
|
break;
|
|
182
220
|
const policy = command.policy;
|
|
183
221
|
let group = running.get(command.slot);
|
|
184
|
-
if (policy === 'drop' && group)
|
|
222
|
+
if (policy === 'drop' && group) {
|
|
223
|
+
discard(command, 'Dropped');
|
|
185
224
|
continue;
|
|
225
|
+
}
|
|
186
226
|
if (policy === 'replace') {
|
|
187
|
-
cancel(command.slot);
|
|
227
|
+
cancel(command.slot, 'Superseded');
|
|
188
228
|
group = undefined;
|
|
189
229
|
}
|
|
190
230
|
if (group && (policy === 'queue' || policy === 'latest-queued')) {
|
|
191
231
|
if (policy === 'latest-queued')
|
|
192
|
-
group.pending.
|
|
232
|
+
for (const pending of group.pending.splice(0))
|
|
233
|
+
discard(pending, 'Superseded');
|
|
193
234
|
group.pending.push(command);
|
|
194
235
|
continue;
|
|
195
236
|
}
|
|
@@ -197,7 +238,7 @@ export function program(options) {
|
|
|
197
238
|
group = { active: new Set(), pending: [] };
|
|
198
239
|
running.set(command.slot, group);
|
|
199
240
|
}
|
|
200
|
-
const task = {};
|
|
241
|
+
const task = { command };
|
|
201
242
|
group.active.add(task);
|
|
202
243
|
const admitted = group;
|
|
203
244
|
starts.push(() => launch(command, admitted, task));
|
|
@@ -221,7 +262,24 @@ export function program(options) {
|
|
|
221
262
|
}
|
|
222
263
|
};
|
|
223
264
|
const send = (message) => enqueue(message);
|
|
265
|
+
const dispose = () => {
|
|
266
|
+
if (disposed)
|
|
267
|
+
return;
|
|
268
|
+
disposed = true;
|
|
269
|
+
queue.length = 0;
|
|
270
|
+
deferred.length = 0;
|
|
271
|
+
for (const slot of running.keys())
|
|
272
|
+
cancel(slot);
|
|
273
|
+
listeners.clear();
|
|
274
|
+
trace('dispose');
|
|
275
|
+
notify();
|
|
276
|
+
};
|
|
224
277
|
return {
|
|
278
|
+
awaitStopped,
|
|
279
|
+
close: () => {
|
|
280
|
+
dispose();
|
|
281
|
+
return awaitStopped();
|
|
282
|
+
},
|
|
225
283
|
model: () => current,
|
|
226
284
|
activeSlots: () => [...running.keys()],
|
|
227
285
|
awaitIdle: (slot) => disposed || (!draining && (slot ? !running.has(slot) : running.size === 0))
|
|
@@ -246,17 +304,6 @@ export function program(options) {
|
|
|
246
304
|
listeners.delete(receive);
|
|
247
305
|
};
|
|
248
306
|
},
|
|
249
|
-
dispose
|
|
250
|
-
if (disposed)
|
|
251
|
-
return;
|
|
252
|
-
disposed = true;
|
|
253
|
-
queue.length = 0;
|
|
254
|
-
deferred.length = 0;
|
|
255
|
-
for (const slot of running.keys())
|
|
256
|
-
cancel(slot);
|
|
257
|
-
listeners.clear();
|
|
258
|
-
trace('dispose');
|
|
259
|
-
notify();
|
|
260
|
-
},
|
|
307
|
+
dispose,
|
|
261
308
|
};
|
|
262
309
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { Effect } from 'effect';
|
|
2
2
|
import type { Snapshot } from './snapshot.js';
|
|
3
|
-
import type { Query } from './query.js';
|
|
3
|
+
import type { Query, QueryGroup } from './query.js';
|
|
4
4
|
export interface QueryDefinition<Args, A, E, R> {
|
|
5
5
|
readonly name: string;
|
|
6
|
-
readonly load: (args: Snapshot<Args>) => Effect.Effect<A, E, R>;
|
|
6
|
+
readonly load: (args: Snapshot<Args>, previous?: Snapshot<A>) => Effect.Effect<A, E, R>;
|
|
7
|
+
readonly groups?: readonly QueryGroup[];
|
|
8
|
+
readonly unused?: 'retain' | 'cancel';
|
|
7
9
|
readonly staleTime: number;
|
|
8
10
|
readonly share?: (previous: Snapshot<A>, next: A | Snapshot<A>) => A | Snapshot<A>;
|
|
9
11
|
}
|
package/dist/query.d.ts
CHANGED
|
@@ -27,4 +27,10 @@ export declare function query<Args, A, E = never, R = never>(definition: Definit
|
|
|
27
27
|
export declare function query<A, E = never, R = never>(definition: Definition<true, A, E, R> & {
|
|
28
28
|
readonly load: () => Effect.Effect<A, E, R>;
|
|
29
29
|
}): Query<true, A, E, R>;
|
|
30
|
+
declare const groupType: unique symbol;
|
|
31
|
+
export type QueryGroup = symbol & {
|
|
32
|
+
readonly [groupType]: true;
|
|
33
|
+
};
|
|
34
|
+
/** Invalidation identity; equal diagnostic names do not share membership. */
|
|
35
|
+
export declare const queryGroup: (name: string) => QueryGroup;
|
|
30
36
|
export {};
|
package/dist/query.js
CHANGED
|
@@ -60,6 +60,12 @@ export function query(definition) {
|
|
|
60
60
|
if (Number.isNaN(staleTime) || staleTime < 0)
|
|
61
61
|
throw new RangeError('Query staleTime must be nonnegative.');
|
|
62
62
|
const result = Object.freeze({ name: definition.name });
|
|
63
|
-
registerQuery(result, Object.freeze({
|
|
63
|
+
registerQuery(result, Object.freeze({
|
|
64
|
+
...definition,
|
|
65
|
+
...(definition.groups ? { groups: Object.freeze([...definition.groups]) } : {}),
|
|
66
|
+
staleTime,
|
|
67
|
+
}));
|
|
64
68
|
return result;
|
|
65
69
|
}
|
|
70
|
+
/** Invalidation identity; equal diagnostic names do not share membership. */
|
|
71
|
+
export const queryGroup = (name) => Symbol(name);
|
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 &&
|
|
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
|
|
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.
|
|
153
|
+
internal.refresh(atom);
|
|
145
154
|
},
|
|
146
155
|
dispose: () => {
|
|
147
156
|
if (disposed)
|