@shivaedev/reconciler 0.0.0-stage → 0.1.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.
- package/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +90 -2
- package/dist/PassState.d.ts +9 -0
- package/dist/PassState.d.ts.map +1 -0
- package/dist/PassState.js +4 -0
- package/dist/PassState.js.map +1 -0
- package/dist/Reconciler.d.ts +18 -0
- package/dist/Reconciler.d.ts.map +1 -0
- package/dist/Reconciler.js +2 -0
- package/dist/Reconciler.js.map +1 -0
- package/dist/claimRows.d.ts +7 -0
- package/dist/claimRows.d.ts.map +1 -0
- package/dist/claimRows.js +31 -0
- package/dist/claimRows.js.map +1 -0
- package/dist/each.d.ts +10 -0
- package/dist/each.d.ts.map +1 -0
- package/dist/each.js +14 -0
- package/dist/each.js.map +1 -0
- package/dist/run.d.ts +13 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +6 -0
- package/dist/run.js.map +1 -0
- package/dist/supervise.d.ts +8 -0
- package/dist/supervise.d.ts.map +1 -0
- package/dist/supervise.js +45 -0
- package/dist/supervise.js.map +1 -0
- package/dist/watch.d.ts +13 -0
- package/dist/watch.d.ts.map +1 -0
- package/dist/watch.js +28 -0
- package/dist/watch.js.map +1 -0
- package/package.json +71 -3
- package/src/PassState.ts +9 -0
- package/src/Reconciler.ts +15 -0
- package/src/claimRows.ts +44 -0
- package/src/each.ts +36 -0
- package/src/run.ts +24 -0
- package/src/supervise.ts +66 -0
- package/src/watch.ts +44 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 - 2026-11-03
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Run scoped background reconciliation at startup, on changes keys or at a reported due time, with merged pending wakes and no polling interval.
|
|
8
|
+
- Supervise failures independently with readable state, typed termination, resume and optional application-owned failure persistence.
|
|
9
|
+
- Run keyed actions once while each key remains present, interrupting removed work and isolating child failures.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ShivaeDev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,91 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @shivaedev/reconciler
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Keep background work in agreement with current data without repeatedly scanning an idle database. A reconciler wakes at startup, when its changes keys are invalidated, or when its next reported deadline arrives. Each has its own scope, readable state and resume operation.
|
|
4
|
+
|
|
5
|
+
## Wake when there is work
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
const worker = yield* run({
|
|
9
|
+
keys: [tasks.list],
|
|
10
|
+
read: pendingTasks,
|
|
11
|
+
act: reconcileTasks,
|
|
12
|
+
due: (tasks, now) => nextDeadline(tasks, now),
|
|
13
|
+
});
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`run` comes from `@shivaedev/reconciler/run.ts`. The application supplies `tasks`, its read, action and deadline calculation. Changes arriving during an action merge into one pending pass, which reads current state once.
|
|
17
|
+
|
|
18
|
+
## Using it
|
|
19
|
+
|
|
20
|
+
### Connect writes and reads
|
|
21
|
+
|
|
22
|
+
Use the same `collection` keys for your writes and reconciler. Provide native Reactivity to both; Platform's PostgreSQL layer already supplies it. Writes recorded through `changes` and `transact` wake readers after commit. A rollback never wakes them.
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { Effect, Schema } from "effect";
|
|
26
|
+
import { SqlClient } from "effect/sql";
|
|
27
|
+
import { collection } from "@shivaedev/changes/keys.ts";
|
|
28
|
+
import { run } from "@shivaedev/reconciler/run.ts";
|
|
29
|
+
import { changes } from "@shivaedev/sql/changes.ts";
|
|
30
|
+
import { transact } from "@shivaedev/sql/transact.ts";
|
|
31
|
+
|
|
32
|
+
const tasks = collection("tasks", Schema.Int);
|
|
33
|
+
const application = Effect.gen(function* () {
|
|
34
|
+
const sql = yield* SqlClient.SqlClient;
|
|
35
|
+
const worker = yield* run({
|
|
36
|
+
keys: [tasks.list],
|
|
37
|
+
read: sql<{ id: number; title: string }>`select id, title from tasks order by id`,
|
|
38
|
+
act: (rows) => Effect.logInfo(rows),
|
|
39
|
+
onFailure: (cause) => sql`insert into worker_failures (cause) values (${String(cause)})`,
|
|
40
|
+
});
|
|
41
|
+
yield* Effect.gen(function* () {
|
|
42
|
+
yield* sql`update tasks set title = 'ready' where id = 1`;
|
|
43
|
+
yield* changes({ tasks: [tasks.item(1)] }).record();
|
|
44
|
+
}).pipe(transact({ onSqlError: (error) => error }));
|
|
45
|
+
return worker;
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Run `application` inside the application's lifetime scope with its SQL layer. The application creates its tables and chooses the failure record schema. `onFailure` is optional; no storage service or schema belongs to the engine. The callback runs after the failed generation's scope closes, and a stored record remains after resume.
|
|
50
|
+
|
|
51
|
+
The subscription is registered before the first read. The engine consumes wakes from a single pending slot and reads only when it consumes one; it has no polling interval. A burst during a pass adds one subsequent pass. Reads and actions retain their typed errors and service requirements.
|
|
52
|
+
|
|
53
|
+
### Deadlines
|
|
54
|
+
|
|
55
|
+
`due(value, now)` returns an absolute timestamp in milliseconds or `undefined`. For several deadlines, return the earliest one that needs attention. The calculation runs after the action using that pass's read value and the current clock. Each pass replaces the old timer. A timestamp at or before `now` leaves no timer: the action should already handle that due work. Returning `undefined` removes the timer.
|
|
56
|
+
|
|
57
|
+
### Supervision
|
|
58
|
+
|
|
59
|
+
Types `Reconciler`, `State` and `Due` come from `@shivaedev/reconciler/Reconciler.ts`. `run` returns a `Reconciler<E>` with these native Effect values:
|
|
60
|
+
|
|
61
|
+
| Member | Meaning |
|
|
62
|
+
| --------- | ------------------------------------------------------------------------------------------------- |
|
|
63
|
+
| `state` | Native `SubscriptionRef` with `running`, `waiting` and its `nextDue`, or `failed` and its `cause` |
|
|
64
|
+
| `refresh` | Manually enqueue a pass |
|
|
65
|
+
| `await` | Await termination, retaining typed errors and interruption |
|
|
66
|
+
| `resume` | Start a fresh generation after failure |
|
|
67
|
+
|
|
68
|
+
A defect or typed failure stops that reconciler and closes its scope. Other reconcilers continue. Resume starts with a new subscription, pending slot, timer and startup pass. The original service context is retained. Closing the owning scope interrupts the reconciler and removes its timer; interruption does not invoke `onFailure`. There is no automatic retry. If failure recording also fails, the failed state and `await` retain both causes.
|
|
69
|
+
|
|
70
|
+
### Keyed work
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { each } from "@shivaedev/reconciler/each.ts";
|
|
74
|
+
|
|
75
|
+
const worker = yield* each({
|
|
76
|
+
keys: [tasks.list],
|
|
77
|
+
read: pendingTasks,
|
|
78
|
+
key: (task) => task.id,
|
|
79
|
+
act: processTask,
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`each` starts scoped work for every unclaimed key. A successful action remains claimed while its key stays present, so another wake does not repeat it. Removing a key interrupts its unfinished action and releases its claim; reappearance starts fresh work. A child defect fails the reconciler and closes its sibling actions. The returned handle, optional `due` calculation and `onFailure` callback follow the same lifecycle as `run`. State describes the snapshot pass; keyed actions can remain active while it is waiting.
|
|
84
|
+
|
|
85
|
+
### Install and limits
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
pnpm add @shivaedev/reconciler effect
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Use `@shivaedev/sql` for SQL transaction announcements. Reactivity is process local; cross-process delivery uses the SQL package's existing notification binding. The reconciler provides no durable queue, distributed claim or exactly-once external side effect. Applications choose idempotency and transaction boundaries.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Context, type Effect } from "effect";
|
|
2
|
+
declare const PassState_base: Context.ServiceClass<PassState, "@shivaedev/reconciler/PassState", {
|
|
3
|
+
readonly running: Effect.Effect<void>;
|
|
4
|
+
readonly waiting: (nextDue: number | undefined) => Effect.Effect<void>;
|
|
5
|
+
}>;
|
|
6
|
+
export declare class PassState extends PassState_base {
|
|
7
|
+
}
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=PassState.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PassState.d.ts","sourceRoot":"","sources":["../src/PassState.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;;sBAK1B,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;sBACnB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;;AAJxE,qBAAa,SAAU,SAAQ,cAMO;CAAG"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PassState.js","sourceRoot":"","sources":["../src/PassState.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAe,MAAM,QAAQ,CAAC;AAE9C,MAAM,OAAO,SAAU,SAAQ,OAAO,CAAC,OAAO,EAM3C,CAAC,iCAAiC,CAAC;CAAG"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Cause, Effect, SubscriptionRef } from "effect";
|
|
2
|
+
export type State<E> = {
|
|
3
|
+
readonly status: "running";
|
|
4
|
+
} | {
|
|
5
|
+
readonly status: "waiting";
|
|
6
|
+
readonly nextDue: number | undefined;
|
|
7
|
+
} | {
|
|
8
|
+
readonly status: "failed";
|
|
9
|
+
readonly cause: Cause.Cause<E>;
|
|
10
|
+
};
|
|
11
|
+
export interface Reconciler<E> {
|
|
12
|
+
readonly await: Effect.Effect<void, E>;
|
|
13
|
+
readonly refresh: Effect.Effect<void>;
|
|
14
|
+
readonly resume: Effect.Effect<void>;
|
|
15
|
+
readonly state: SubscriptionRef.SubscriptionRef<State<E>>;
|
|
16
|
+
}
|
|
17
|
+
export type Due<TValue> = (value: TValue, now: number) => number | undefined;
|
|
18
|
+
//# sourceMappingURL=Reconciler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Reconciler.d.ts","sourceRoot":"","sources":["../src/Reconciler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AAE7D,MAAM,MAAM,KAAK,CAAC,CAAC,IAChB;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAA;CAAE,GAC9B;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GACpE;IAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;CAAE,CAAC;AAEjE,MAAM,WAAW,UAAU,CAAC,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACrC,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;CAC1D;AAED,MAAM,MAAM,GAAG,CAAC,MAAM,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Reconciler.js","sourceRoot":"","sources":["../src/Reconciler.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Cause, Effect, Fiber } from "effect";
|
|
2
|
+
type Claims<TKey, E> = Map<TKey, {
|
|
3
|
+
fiber?: Fiber.Fiber<unknown, E>;
|
|
4
|
+
}>;
|
|
5
|
+
export declare const claimRows: <TRow, TKey, E, R>(claims: Claims<TKey, E>, rows: readonly TRow[], keyOf: (row: TRow) => TKey, act: (row: TRow) => Effect.Effect<unknown, E, R>, fail: (cause: Cause.Cause<E>) => Effect.Effect<unknown>) => Effect.Effect<void, never, import("effect/Scope").Scope | Exclude<R, import("effect/Scope").Scope>>;
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=claimRows.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claimRows.d.ts","sourceRoot":"","sources":["../src/claimRows.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAQ,KAAK,EAAE,MAAM,QAAQ,CAAC;AAEpD,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC,IAAI,GAAG,CAAC,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;CAAE,CAAC,CAAC;AActE,eAAO,MAAM,SAAS,GAAgC,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,+DAGxD,IAAI,KAAK,IAAI,aACf,IAAI,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,gBAClC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,wGAsBtD,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Cause, Effect, Exit, Fiber } from "effect";
|
|
2
|
+
const releaseAbsent = Effect.fnUntraced(function* (claims, matching) {
|
|
3
|
+
for (const [key, claim] of claims) {
|
|
4
|
+
if (matching.has(key)) {
|
|
5
|
+
continue;
|
|
6
|
+
}
|
|
7
|
+
claims.delete(key);
|
|
8
|
+
if (claim.fiber !== undefined) {
|
|
9
|
+
yield* Fiber.interrupt(claim.fiber);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
export const claimRows = Effect.fnUntraced(function* (claims, rows, keyOf, act, fail) {
|
|
14
|
+
yield* releaseAbsent(claims, new Set(rows.map(keyOf)));
|
|
15
|
+
for (const row of rows) {
|
|
16
|
+
const key = keyOf(row);
|
|
17
|
+
if (claims.has(key)) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const claim = {};
|
|
21
|
+
claims.set(key, claim);
|
|
22
|
+
claim.fiber = yield* Effect.forkScoped(Effect.scoped(Effect.suspend(() => act(row))).pipe(Effect.onExit((exit) => {
|
|
23
|
+
if (Exit.isSuccess(exit)) {
|
|
24
|
+
return Effect.void;
|
|
25
|
+
}
|
|
26
|
+
claims.delete(key);
|
|
27
|
+
return Cause.hasInterruptsOnly(exit.cause) ? Effect.void : fail(exit.cause);
|
|
28
|
+
})));
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
//# sourceMappingURL=claimRows.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claimRows.js","sourceRoot":"","sources":["../src/claimRows.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAIpD,MAAM,aAAa,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAW,MAAuB,EAAE,QAA2B;IAC/G,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,EAAE,CAAC;QACnC,IAAI,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACvB,SAAS;QACV,CAAC;QACD,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACnB,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC/B,KAAK,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACrC,CAAC;IACF,CAAC;AACF,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,SAAS,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EACnD,MAAuB,EACvB,IAAqB,EACrB,KAA0B,EAC1B,GAAgD,EAChD,IAAuD;IAEvD,KAAK,CAAC,CAAC,aAAa,CAAC,MAAM,EAAE,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACvD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACxB,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;QACvB,IAAI,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACrB,SAAS;QACV,CAAC;QACD,MAAM,KAAK,GAAwC,EAAE,CAAC;QACtD,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACvB,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,UAAU,CACrC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CACjD,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;YACtB,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC1B,OAAO,MAAM,CAAC,IAAI,CAAC;YACpB,CAAC;YACD,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACnB,OAAO,KAAK,CAAC,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC7E,CAAC,CAAC,CACF,CACD,CAAC;IACH,CAAC;AACF,CAAC,CAAC,CAAC"}
|
package/dist/each.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Effect, type Scope } from "effect";
|
|
2
|
+
import type * as Reactivity from "effect/reactivity/Reactivity";
|
|
3
|
+
import type { Reconciler } from "#Reconciler.ts";
|
|
4
|
+
import type { Options } from "#run.ts";
|
|
5
|
+
export interface EachOptions<TRow, TKey, TReadError, TReadServices, TActError, TActServices, TRecordError = never, TRecordServices = never> extends Omit<Options<readonly TRow[], TReadError, TReadServices, TActError, TActServices, TRecordError, TRecordServices>, "act"> {
|
|
6
|
+
readonly act: (row: TRow) => Effect.Effect<unknown, TActError, TActServices>;
|
|
7
|
+
readonly key: (row: TRow) => TKey;
|
|
8
|
+
}
|
|
9
|
+
export declare function each<TRow, TKey, TReadError, TReadServices, TActError, TActServices, TRecordError = never, TRecordServices = never>(options: EachOptions<TRow, TKey, TReadError, TReadServices, TActError, TActServices, TRecordError, TRecordServices>): Effect.Effect<Reconciler<TReadError | TActError | TRecordError>, never, TReadServices | TActServices | TRecordServices | Reactivity.Reactivity | Scope.Scope>;
|
|
10
|
+
//# sourceMappingURL=each.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"each.d.ts","sourceRoot":"","sources":["../src/each.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAc,KAAK,KAAK,EAAE,MAAM,QAAQ,CAAC;AACxD,OAAO,KAAK,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAEhE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAIvC,MAAM,WAAW,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,aAAa,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,GAAG,KAAK,EAAE,eAAe,GAAG,KAAK,CACzI,SAAQ,IAAI,CAAC,OAAO,CAAC,SAAS,IAAI,EAAE,EAAE,UAAU,EAAE,aAAa,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,eAAe,CAAC,EAAE,KAAK,CAAC;IAChI,QAAQ,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC;IAC7E,QAAQ,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,IAAI,CAAC;CAClC;AAED,wBAAgB,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,aAAa,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,GAAG,KAAK,EAAE,eAAe,GAAG,KAAK,EACjI,OAAO,EAAE,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,aAAa,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,eAAe,CAAC,GACjH,MAAM,CAAC,MAAM,CACf,UAAU,CAAC,UAAU,GAAG,SAAS,GAAG,YAAY,CAAC,EACjD,KAAK,EACL,aAAa,GAAG,YAAY,GAAG,eAAe,GAAG,UAAU,CAAC,UAAU,GAAG,KAAK,CAAC,KAAK,CACpF,CAeA"}
|
package/dist/each.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { claimRows } from "#claimRows.ts";
|
|
3
|
+
import { supervise } from "#supervise.ts";
|
|
4
|
+
import { watch } from "#watch.ts";
|
|
5
|
+
export function each(options) {
|
|
6
|
+
return supervise((pending) => Effect.gen(function* () {
|
|
7
|
+
const claims = new Map();
|
|
8
|
+
return yield* watch({
|
|
9
|
+
...options,
|
|
10
|
+
act: (rows, fail) => claimRows(claims, rows, options.key, options.act, fail),
|
|
11
|
+
}, pending);
|
|
12
|
+
}), options);
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=each.js.map
|
package/dist/each.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"each.js","sourceRoot":"","sources":["../src/each.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAA0B,MAAM,QAAQ,CAAC;AAExD,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAG1C,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAQlC,MAAM,UAAU,IAAI,CACnB,OAAmH;IAMnH,OAAO,SAAS,CACf,CAAC,OAAO,EAAE,EAAE,CACX,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACnB,MAAM,MAAM,GAAG,IAAI,GAAG,EAAqD,CAAC;QAC5E,OAAO,KAAK,CAAC,CAAC,KAAK,CAClB;YACC,GAAG,OAAO;YACV,GAAG,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC;SAC5E,EACD,OAAO,CACP,CAAC;IACH,CAAC,CAAC,EACH,OAAO,CACP,CAAC;AACH,CAAC"}
|
package/dist/run.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Effect, Scope } from "effect";
|
|
2
|
+
import type * as Reactivity from "effect/reactivity/Reactivity";
|
|
3
|
+
import type { Key } from "@shivaedev/changes/keys.ts";
|
|
4
|
+
import type { Due, Reconciler } from "#Reconciler.ts";
|
|
5
|
+
import { type FailureHandler } from "#supervise.ts";
|
|
6
|
+
export interface Options<TValue, TReadError, TReadServices, TActError, TActServices, TRecordError = never, TRecordServices = never> extends FailureHandler<TReadError | TActError, TRecordError, TRecordServices> {
|
|
7
|
+
readonly act: (value: TValue) => Effect.Effect<unknown, TActError, TActServices>;
|
|
8
|
+
readonly due?: Due<TValue>;
|
|
9
|
+
readonly keys: readonly Key[];
|
|
10
|
+
readonly read: Effect.Effect<TValue, TReadError, TReadServices>;
|
|
11
|
+
}
|
|
12
|
+
export declare function run<TValue, TReadError, TReadServices, TActError, TActServices, TRecordError = never, TRecordServices = never>(options: Options<TValue, TReadError, TReadServices, TActError, TActServices, TRecordError, TRecordServices>): Effect.Effect<Reconciler<TReadError | TActError | TRecordError>, never, TReadServices | TActServices | TRecordServices | Reactivity.Reactivity | Scope.Scope>;
|
|
13
|
+
//# sourceMappingURL=run.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAO,KAAK,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAChE,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,4BAA4B,CAAC;AACtD,OAAO,KAAK,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,EAAE,KAAK,cAAc,EAAa,MAAM,eAAe,CAAC;AAG/D,MAAM,WAAW,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,GAAG,KAAK,EAAE,eAAe,GAAG,KAAK,CACjI,SAAQ,cAAc,CAAC,UAAU,GAAG,SAAS,EAAE,YAAY,EAAE,eAAe,CAAC;IAC7E,QAAQ,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC;IACjF,QAAQ,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,EAAE,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,aAAa,CAAC,CAAC;CAChE;AAED,wBAAgB,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,GAAG,KAAK,EAAE,eAAe,GAAG,KAAK,EAC5H,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,eAAe,CAAC,GACzG,MAAM,CAAC,MAAM,CACf,UAAU,CAAC,UAAU,GAAG,SAAS,GAAG,YAAY,CAAC,EACjD,KAAK,EACL,aAAa,GAAG,YAAY,GAAG,eAAe,GAAG,UAAU,CAAC,UAAU,GAAG,KAAK,CAAC,KAAK,CACpF,CAEA"}
|
package/dist/run.js
ADDED
package/dist/run.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAIA,OAAO,EAAuB,SAAS,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAUlC,MAAM,UAAU,GAAG,CAClB,OAA2G;IAM3G,OAAO,SAAS,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,KAAK,CAA+D,OAAO,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,CAAC;AAC/H,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Cause, Effect, Queue, type Scope } from "effect";
|
|
2
|
+
import { PassState } from "#PassState.ts";
|
|
3
|
+
import type { Reconciler } from "#Reconciler.ts";
|
|
4
|
+
export interface FailureHandler<E, TRecordError, TRecordServices> {
|
|
5
|
+
readonly onFailure?: (cause: Cause.Cause<E>) => Effect.Effect<unknown, TRecordError, TRecordServices>;
|
|
6
|
+
}
|
|
7
|
+
export declare const supervise: <E, R, TRecordError = never, TRecordServices = never>(open: (pending: Queue.Queue<void>) => Effect.Effect<never, E, R | Scope.Scope | PassState>, options: FailureHandler<E, TRecordError, TRecordServices>) => Effect.Effect<Reconciler<E | TRecordError>, never, R | TRecordServices | Scope.Scope>;
|
|
8
|
+
//# sourceMappingURL=supervise.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supervise.d.ts","sourceRoot":"","sources":["../src/supervise.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAY,MAAM,EAAQ,KAAK,EAAE,KAAK,KAAK,EAA8B,MAAM,QAAQ,CAAC;AACtG,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAS,MAAM,gBAAgB,CAAC;AAExD,MAAM,WAAW,cAAc,CAAC,CAAC,EAAE,YAAY,EAAE,eAAe;IAC/D,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,YAAY,EAAE,eAAe,CAAC,CAAC;CACtG;AAED,eAAO,MAAM,SAAS,GAAgC,CAAC,EAAE,CAAC,EAAE,YAAY,UAAU,eAAe,0BAChF,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,KAAK,GAAG,SAAS,CAAC,qJAwDzF,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Cause, Deferred, Effect, Exit, Queue, Semaphore, SubscriptionRef } from "effect";
|
|
2
|
+
import { PassState } from "#PassState.ts";
|
|
3
|
+
export const supervise = Effect.fnUntraced(function* (open, options) {
|
|
4
|
+
const lifetime = yield* Effect.scope;
|
|
5
|
+
const context = yield* Effect.context();
|
|
6
|
+
const state = yield* SubscriptionRef.make({ status: "running" });
|
|
7
|
+
const lock = yield* Semaphore.make(1);
|
|
8
|
+
let completion = yield* Deferred.make();
|
|
9
|
+
let refresh = Effect.void;
|
|
10
|
+
const launch = Effect.uninterruptible(Effect.gen(function* () {
|
|
11
|
+
const pending = yield* Queue.sliding(1);
|
|
12
|
+
refresh = Queue.offer(pending, undefined).pipe(Effect.asVoid);
|
|
13
|
+
const finished = yield* Deferred.make();
|
|
14
|
+
completion = finished;
|
|
15
|
+
yield* SubscriptionRef.set(state, { status: "running" });
|
|
16
|
+
const work = Effect.gen(function* () {
|
|
17
|
+
const opening = open(pending).pipe(Effect.provideService(PassState, {
|
|
18
|
+
running: SubscriptionRef.set(state, { status: "running" }),
|
|
19
|
+
waiting: (nextDue) => SubscriptionRef.set(state, { nextDue, status: "waiting" }),
|
|
20
|
+
}));
|
|
21
|
+
const exit = yield* Effect.exit(Effect.scoped(opening));
|
|
22
|
+
if (Exit.isSuccess(exit) || Cause.hasInterruptsOnly(exit.cause)) {
|
|
23
|
+
return yield* Deferred.done(finished, exit);
|
|
24
|
+
}
|
|
25
|
+
const recorded = yield* Effect.exit(Effect.suspend(() => options.onFailure?.(exit.cause) ?? Effect.void));
|
|
26
|
+
const cause = Exit.isFailure(recorded) ? Cause.combine(exit.cause, recorded.cause) : exit.cause;
|
|
27
|
+
yield* SubscriptionRef.set(state, { cause, status: "failed" });
|
|
28
|
+
return yield* Deferred.failCause(finished, cause);
|
|
29
|
+
});
|
|
30
|
+
yield* Effect.forkIn(work.pipe(Effect.asVoid, Effect.onExit((exit) => Deferred.done(finished, exit)), Effect.provideContext(context)), lifetime);
|
|
31
|
+
}));
|
|
32
|
+
yield* launch;
|
|
33
|
+
return {
|
|
34
|
+
await: Effect.suspend(() => Deferred.await(completion)),
|
|
35
|
+
refresh: Effect.suspend(() => refresh),
|
|
36
|
+
resume: Semaphore.withPermit(lock)(Effect.gen(function* () {
|
|
37
|
+
const current = yield* SubscriptionRef.get(state);
|
|
38
|
+
if (current.status === "failed") {
|
|
39
|
+
yield* launch;
|
|
40
|
+
}
|
|
41
|
+
})),
|
|
42
|
+
state,
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
//# sourceMappingURL=supervise.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supervise.js","sourceRoot":"","sources":["../src/supervise.ts"],"names":[],"mappings":"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"}
|
package/dist/watch.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Cause, Effect, Queue, type Scope } from "effect";
|
|
2
|
+
import * as Reactivity from "effect/reactivity/Reactivity";
|
|
3
|
+
import { type Key } from "@shivaedev/changes/keys.ts";
|
|
4
|
+
import { PassState } from "#PassState.ts";
|
|
5
|
+
import type { Due } from "#Reconciler.ts";
|
|
6
|
+
export interface Work<TValue, E, R> {
|
|
7
|
+
readonly act: (value: TValue, fail: (cause: Cause.Cause<E>) => Effect.Effect<unknown>) => Effect.Effect<unknown, E, R>;
|
|
8
|
+
readonly due?: Due<TValue>;
|
|
9
|
+
readonly keys: readonly Key[];
|
|
10
|
+
readonly read: Effect.Effect<TValue, E, R>;
|
|
11
|
+
}
|
|
12
|
+
export declare const watch: <TValue, E, R>(work: Work<TValue, E, R>, pending: Queue.Queue<void, never>) => Effect.Effect<never, E, R | PassState | Reactivity.Reactivity | Scope.Scope>;
|
|
13
|
+
//# sourceMappingURL=watch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../src/watch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,KAAK,EAAmB,MAAM,EAAe,KAAK,EAAE,KAAK,KAAK,EAAE,MAAM,QAAQ,CAAC;AAC7F,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,KAAK,GAAG,EAAY,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AAE1C,MAAM,WAAW,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC;IACjC,QAAQ,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IACvH,QAAQ,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,EAAE,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;CAC3C;AAED,eAAO,MAAM,KAAK,GAAgC,MAAM,EAAE,CAAC,EAAE,CAAC,8IA8B5D,CAAC"}
|
package/dist/watch.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Clock, Deferred, Effect, FiberHandle, Queue } from "effect";
|
|
2
|
+
import * as Reactivity from "effect/reactivity/Reactivity";
|
|
3
|
+
import { readKeys } from "@shivaedev/changes/keys.ts";
|
|
4
|
+
import { PassState } from "#PassState.ts";
|
|
5
|
+
export const watch = Effect.fnUntraced(function* (work, pending) {
|
|
6
|
+
const state = yield* PassState;
|
|
7
|
+
const reactivity = yield* Reactivity.Reactivity;
|
|
8
|
+
const failed = yield* Deferred.make();
|
|
9
|
+
const timer = yield* FiberHandle.make();
|
|
10
|
+
yield* Effect.acquireRelease(Effect.sync(() => reactivity.registerUnsafe(readKeys(work.keys), () => Queue.offerUnsafe(pending, undefined))), (release) => Effect.sync(release));
|
|
11
|
+
yield* Queue.offer(pending, undefined);
|
|
12
|
+
const consume = Effect.forever(Effect.gen(function* () {
|
|
13
|
+
yield* Queue.take(pending);
|
|
14
|
+
yield* FiberHandle.clear(timer);
|
|
15
|
+
yield* state.running;
|
|
16
|
+
const value = yield* work.read;
|
|
17
|
+
yield* work.act(value, (cause) => Deferred.failCause(failed, cause));
|
|
18
|
+
const now = yield* Clock.currentTimeMillis;
|
|
19
|
+
const reported = work.due?.(value, now);
|
|
20
|
+
const nextDue = reported !== undefined && reported > now ? reported : undefined;
|
|
21
|
+
if (nextDue !== undefined) {
|
|
22
|
+
yield* FiberHandle.run(timer, Effect.andThen(Effect.sleep(nextDue - now), Queue.offer(pending, undefined)).pipe(Effect.asVoid));
|
|
23
|
+
}
|
|
24
|
+
yield* state.waiting(nextDue);
|
|
25
|
+
}));
|
|
26
|
+
return yield* Effect.raceFirst(consume, Deferred.await(failed));
|
|
27
|
+
});
|
|
28
|
+
//# sourceMappingURL=watch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch.js","sourceRoot":"","sources":["../src/watch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,EAAc,MAAM,QAAQ,CAAC;AAC7F,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAY,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAU1C,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAC/C,IAAwB,EACxB,OAA0B;IAE1B,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,SAAS,CAAC;IAC/B,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC;IAChD,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAY,CAAC;IAChD,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,WAAW,CAAC,IAAI,EAAQ,CAAC;IAC9C,KAAK,CAAC,CAAC,MAAM,CAAC,cAAc,CAC3B,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,cAAc,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,WAAW,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,EAC9G,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CACjC,CAAC;IACF,KAAK,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACvC,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAC7B,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACnB,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC3B,KAAK,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAChC,KAAK,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC;QACrB,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;QAC/B,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;QACrE,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC;QAC3C,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QACxC,MAAM,OAAO,GAAG,QAAQ,KAAK,SAAS,IAAI,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;QAChF,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC3B,KAAK,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,GAAG,GAAG,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;QACjI,CAAC;QACD,KAAK,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/B,CAAC,CAAC,CACF,CAAC;IACF,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;AACjE,CAAC,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,74 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/reconciler",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Change-driven background reconciliation with scoped supervision for Effect applications",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"effect"
|
|
7
|
+
],
|
|
8
|
+
"homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/reconciler#readme",
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/ShivaeDev/platform/issues"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/ShivaeDev/platform.git",
|
|
15
|
+
"directory": "packages/reconciler"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"sideEffects": false,
|
|
19
|
+
"type": "module",
|
|
20
|
+
"imports": {
|
|
21
|
+
"#test/*": "./src/test-support/*",
|
|
22
|
+
"#*.ts": {
|
|
23
|
+
"source": "./src/*.ts",
|
|
24
|
+
"types": "./dist/*.d.ts",
|
|
25
|
+
"default": "./dist/*.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"exports": {
|
|
29
|
+
"./*.ts": {
|
|
30
|
+
"source": "./src/*.ts",
|
|
31
|
+
"types": "./dist/*.d.ts",
|
|
32
|
+
"default": "./dist/*.js"
|
|
33
|
+
},
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"src",
|
|
39
|
+
"!src/**/*.test.ts",
|
|
40
|
+
"!src/**/*.spec.ts",
|
|
41
|
+
"!src/**/test-support",
|
|
42
|
+
"CHANGELOG.md",
|
|
43
|
+
"README.md"
|
|
44
|
+
],
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@shivaedev/changes": "0.9.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@shivaedev/quality": "0.57.1",
|
|
50
|
+
"@shivaedev/sql": "0.35.0",
|
|
51
|
+
"@shivaedev/test-sql": "0.18.0",
|
|
52
|
+
"@shivaedev/test-story": "0.36.0",
|
|
53
|
+
"@types/node": "24.19.2",
|
|
54
|
+
"effect": "4.0.6",
|
|
55
|
+
"typescript": "7.0.2",
|
|
56
|
+
"vitest": "5.0.5"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"effect": "4.0.6"
|
|
60
|
+
},
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=24"
|
|
63
|
+
},
|
|
64
|
+
"publishConfig": {
|
|
65
|
+
"access": "public",
|
|
66
|
+
"provenance": true
|
|
67
|
+
},
|
|
68
|
+
"scripts": {
|
|
69
|
+
"build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" && tsc --project tsconfig.build.json",
|
|
70
|
+
"ready": "pnpm typecheck && pnpm test && pnpm build",
|
|
71
|
+
"test": "vitest run",
|
|
72
|
+
"typecheck": "tsc --noEmit"
|
|
73
|
+
}
|
|
6
74
|
}
|
package/src/PassState.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Context, type Effect } from "effect";
|
|
2
|
+
|
|
3
|
+
export class PassState extends Context.Service<
|
|
4
|
+
PassState,
|
|
5
|
+
{
|
|
6
|
+
readonly running: Effect.Effect<void>;
|
|
7
|
+
readonly waiting: (nextDue: number | undefined) => Effect.Effect<void>;
|
|
8
|
+
}
|
|
9
|
+
>()("@shivaedev/reconciler/PassState") {}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Cause, Effect, SubscriptionRef } from "effect";
|
|
2
|
+
|
|
3
|
+
export type State<E> =
|
|
4
|
+
| { readonly status: "running" }
|
|
5
|
+
| { readonly status: "waiting"; readonly nextDue: number | undefined }
|
|
6
|
+
| { readonly status: "failed"; readonly cause: Cause.Cause<E> };
|
|
7
|
+
|
|
8
|
+
export interface Reconciler<E> {
|
|
9
|
+
readonly await: Effect.Effect<void, E>;
|
|
10
|
+
readonly refresh: Effect.Effect<void>;
|
|
11
|
+
readonly resume: Effect.Effect<void>;
|
|
12
|
+
readonly state: SubscriptionRef.SubscriptionRef<State<E>>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type Due<TValue> = (value: TValue, now: number) => number | undefined;
|
package/src/claimRows.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Cause, Effect, Exit, Fiber } from "effect";
|
|
2
|
+
|
|
3
|
+
type Claims<TKey, E> = Map<TKey, { fiber?: Fiber.Fiber<unknown, E> }>;
|
|
4
|
+
|
|
5
|
+
const releaseAbsent = Effect.fnUntraced(function* <TKey, E>(claims: Claims<TKey, E>, matching: ReadonlySet<TKey>) {
|
|
6
|
+
for (const [key, claim] of claims) {
|
|
7
|
+
if (matching.has(key)) {
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
claims.delete(key);
|
|
11
|
+
if (claim.fiber !== undefined) {
|
|
12
|
+
yield* Fiber.interrupt(claim.fiber);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const claimRows = Effect.fnUntraced(function* <TRow, TKey, E, R>(
|
|
18
|
+
claims: Claims<TKey, E>,
|
|
19
|
+
rows: readonly TRow[],
|
|
20
|
+
keyOf: (row: TRow) => TKey,
|
|
21
|
+
act: (row: TRow) => Effect.Effect<unknown, E, R>,
|
|
22
|
+
fail: (cause: Cause.Cause<E>) => Effect.Effect<unknown>,
|
|
23
|
+
) {
|
|
24
|
+
yield* releaseAbsent(claims, new Set(rows.map(keyOf)));
|
|
25
|
+
for (const row of rows) {
|
|
26
|
+
const key = keyOf(row);
|
|
27
|
+
if (claims.has(key)) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const claim: { fiber?: Fiber.Fiber<unknown, E> } = {};
|
|
31
|
+
claims.set(key, claim);
|
|
32
|
+
claim.fiber = yield* Effect.forkScoped(
|
|
33
|
+
Effect.scoped(Effect.suspend(() => act(row))).pipe(
|
|
34
|
+
Effect.onExit((exit) => {
|
|
35
|
+
if (Exit.isSuccess(exit)) {
|
|
36
|
+
return Effect.void;
|
|
37
|
+
}
|
|
38
|
+
claims.delete(key);
|
|
39
|
+
return Cause.hasInterruptsOnly(exit.cause) ? Effect.void : fail(exit.cause);
|
|
40
|
+
}),
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
});
|
package/src/each.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Effect, type Fiber, type Scope } from "effect";
|
|
2
|
+
import type * as Reactivity from "effect/reactivity/Reactivity";
|
|
3
|
+
import { claimRows } from "#claimRows.ts";
|
|
4
|
+
import type { Reconciler } from "#Reconciler.ts";
|
|
5
|
+
import type { Options } from "#run.ts";
|
|
6
|
+
import { supervise } from "#supervise.ts";
|
|
7
|
+
import { watch } from "#watch.ts";
|
|
8
|
+
|
|
9
|
+
export interface EachOptions<TRow, TKey, TReadError, TReadServices, TActError, TActServices, TRecordError = never, TRecordServices = never>
|
|
10
|
+
extends Omit<Options<readonly TRow[], TReadError, TReadServices, TActError, TActServices, TRecordError, TRecordServices>, "act"> {
|
|
11
|
+
readonly act: (row: TRow) => Effect.Effect<unknown, TActError, TActServices>;
|
|
12
|
+
readonly key: (row: TRow) => TKey;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function each<TRow, TKey, TReadError, TReadServices, TActError, TActServices, TRecordError = never, TRecordServices = never>(
|
|
16
|
+
options: EachOptions<TRow, TKey, TReadError, TReadServices, TActError, TActServices, TRecordError, TRecordServices>,
|
|
17
|
+
): Effect.Effect<
|
|
18
|
+
Reconciler<TReadError | TActError | TRecordError>,
|
|
19
|
+
never,
|
|
20
|
+
TReadServices | TActServices | TRecordServices | Reactivity.Reactivity | Scope.Scope
|
|
21
|
+
> {
|
|
22
|
+
return supervise(
|
|
23
|
+
(pending) =>
|
|
24
|
+
Effect.gen(function* () {
|
|
25
|
+
const claims = new Map<TKey, { fiber?: Fiber.Fiber<unknown, TActError> }>();
|
|
26
|
+
return yield* watch<readonly TRow[], TReadError | TActError, TReadServices | TActServices | Scope.Scope>(
|
|
27
|
+
{
|
|
28
|
+
...options,
|
|
29
|
+
act: (rows, fail) => claimRows(claims, rows, options.key, options.act, fail),
|
|
30
|
+
},
|
|
31
|
+
pending,
|
|
32
|
+
);
|
|
33
|
+
}),
|
|
34
|
+
options,
|
|
35
|
+
);
|
|
36
|
+
}
|
package/src/run.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Effect, Scope } from "effect";
|
|
2
|
+
import type * as Reactivity from "effect/reactivity/Reactivity";
|
|
3
|
+
import type { Key } from "@shivaedev/changes/keys.ts";
|
|
4
|
+
import type { Due, Reconciler } from "#Reconciler.ts";
|
|
5
|
+
import { type FailureHandler, supervise } from "#supervise.ts";
|
|
6
|
+
import { watch } from "#watch.ts";
|
|
7
|
+
|
|
8
|
+
export interface Options<TValue, TReadError, TReadServices, TActError, TActServices, TRecordError = never, TRecordServices = never>
|
|
9
|
+
extends FailureHandler<TReadError | TActError, TRecordError, TRecordServices> {
|
|
10
|
+
readonly act: (value: TValue) => Effect.Effect<unknown, TActError, TActServices>;
|
|
11
|
+
readonly due?: Due<TValue>;
|
|
12
|
+
readonly keys: readonly Key[];
|
|
13
|
+
readonly read: Effect.Effect<TValue, TReadError, TReadServices>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function run<TValue, TReadError, TReadServices, TActError, TActServices, TRecordError = never, TRecordServices = never>(
|
|
17
|
+
options: Options<TValue, TReadError, TReadServices, TActError, TActServices, TRecordError, TRecordServices>,
|
|
18
|
+
): Effect.Effect<
|
|
19
|
+
Reconciler<TReadError | TActError | TRecordError>,
|
|
20
|
+
never,
|
|
21
|
+
TReadServices | TActServices | TRecordServices | Reactivity.Reactivity | Scope.Scope
|
|
22
|
+
> {
|
|
23
|
+
return supervise((pending) => watch<TValue, TReadError | TActError, TReadServices | TActServices>(options, pending), options);
|
|
24
|
+
}
|
package/src/supervise.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Cause, Deferred, Effect, Exit, Queue, type Scope, Semaphore, SubscriptionRef } from "effect";
|
|
2
|
+
import { PassState } from "#PassState.ts";
|
|
3
|
+
import type { Reconciler, State } from "#Reconciler.ts";
|
|
4
|
+
|
|
5
|
+
export interface FailureHandler<E, TRecordError, TRecordServices> {
|
|
6
|
+
readonly onFailure?: (cause: Cause.Cause<E>) => Effect.Effect<unknown, TRecordError, TRecordServices>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const supervise = Effect.fnUntraced(function* <E, R, TRecordError = never, TRecordServices = never>(
|
|
10
|
+
open: (pending: Queue.Queue<void>) => Effect.Effect<never, E, R | Scope.Scope | PassState>,
|
|
11
|
+
options: FailureHandler<E, TRecordError, TRecordServices>,
|
|
12
|
+
): Effect.fn.Return<Reconciler<E | TRecordError>, never, R | TRecordServices | Scope.Scope> {
|
|
13
|
+
const lifetime = yield* Effect.scope;
|
|
14
|
+
const context = yield* Effect.context<R | TRecordServices>();
|
|
15
|
+
const state = yield* SubscriptionRef.make<State<E | TRecordError>>({ status: "running" });
|
|
16
|
+
const lock = yield* Semaphore.make(1);
|
|
17
|
+
let completion = yield* Deferred.make<void, E | TRecordError>();
|
|
18
|
+
let refresh: Effect.Effect<void> = Effect.void;
|
|
19
|
+
const launch = Effect.uninterruptible(
|
|
20
|
+
Effect.gen(function* () {
|
|
21
|
+
const pending = yield* Queue.sliding<void>(1);
|
|
22
|
+
refresh = Queue.offer(pending, undefined).pipe(Effect.asVoid);
|
|
23
|
+
const finished = yield* Deferred.make<void, E | TRecordError>();
|
|
24
|
+
completion = finished;
|
|
25
|
+
yield* SubscriptionRef.set(state, { status: "running" });
|
|
26
|
+
const work = Effect.gen(function* () {
|
|
27
|
+
const opening = open(pending).pipe(
|
|
28
|
+
Effect.provideService(PassState, {
|
|
29
|
+
running: SubscriptionRef.set(state, { status: "running" }),
|
|
30
|
+
waiting: (nextDue) => SubscriptionRef.set(state, { nextDue, status: "waiting" }),
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
const exit = yield* Effect.exit(Effect.scoped(opening));
|
|
34
|
+
if (Exit.isSuccess(exit) || Cause.hasInterruptsOnly(exit.cause)) {
|
|
35
|
+
return yield* Deferred.done(finished, exit);
|
|
36
|
+
}
|
|
37
|
+
const recorded = yield* Effect.exit(Effect.suspend(() => options.onFailure?.(exit.cause) ?? Effect.void));
|
|
38
|
+
const cause = Exit.isFailure(recorded) ? Cause.combine(exit.cause, recorded.cause) : exit.cause;
|
|
39
|
+
yield* SubscriptionRef.set(state, { cause, status: "failed" });
|
|
40
|
+
return yield* Deferred.failCause(finished, cause);
|
|
41
|
+
});
|
|
42
|
+
yield* Effect.forkIn(
|
|
43
|
+
work.pipe(
|
|
44
|
+
Effect.asVoid,
|
|
45
|
+
Effect.onExit((exit) => Deferred.done(finished, exit)),
|
|
46
|
+
Effect.provideContext(context),
|
|
47
|
+
),
|
|
48
|
+
lifetime,
|
|
49
|
+
);
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
yield* launch;
|
|
53
|
+
return {
|
|
54
|
+
await: Effect.suspend(() => Deferred.await(completion)),
|
|
55
|
+
refresh: Effect.suspend(() => refresh),
|
|
56
|
+
resume: Semaphore.withPermit(lock)(
|
|
57
|
+
Effect.gen(function* () {
|
|
58
|
+
const current = yield* SubscriptionRef.get(state);
|
|
59
|
+
if (current.status === "failed") {
|
|
60
|
+
yield* launch;
|
|
61
|
+
}
|
|
62
|
+
}),
|
|
63
|
+
),
|
|
64
|
+
state,
|
|
65
|
+
};
|
|
66
|
+
});
|
package/src/watch.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type Cause, Clock, Deferred, Effect, FiberHandle, Queue, type Scope } from "effect";
|
|
2
|
+
import * as Reactivity from "effect/reactivity/Reactivity";
|
|
3
|
+
import { type Key, readKeys } from "@shivaedev/changes/keys.ts";
|
|
4
|
+
import { PassState } from "#PassState.ts";
|
|
5
|
+
import type { Due } from "#Reconciler.ts";
|
|
6
|
+
|
|
7
|
+
export interface Work<TValue, E, R> {
|
|
8
|
+
readonly act: (value: TValue, fail: (cause: Cause.Cause<E>) => Effect.Effect<unknown>) => Effect.Effect<unknown, E, R>;
|
|
9
|
+
readonly due?: Due<TValue>;
|
|
10
|
+
readonly keys: readonly Key[];
|
|
11
|
+
readonly read: Effect.Effect<TValue, E, R>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const watch = Effect.fnUntraced(function* <TValue, E, R>(
|
|
15
|
+
work: Work<TValue, E, R>,
|
|
16
|
+
pending: Queue.Queue<void>,
|
|
17
|
+
): Effect.fn.Return<never, E, R | Reactivity.Reactivity | Scope.Scope | PassState> {
|
|
18
|
+
const state = yield* PassState;
|
|
19
|
+
const reactivity = yield* Reactivity.Reactivity;
|
|
20
|
+
const failed = yield* Deferred.make<never, E>();
|
|
21
|
+
const timer = yield* FiberHandle.make<void>();
|
|
22
|
+
yield* Effect.acquireRelease(
|
|
23
|
+
Effect.sync(() => reactivity.registerUnsafe(readKeys(work.keys), () => Queue.offerUnsafe(pending, undefined))),
|
|
24
|
+
(release) => Effect.sync(release),
|
|
25
|
+
);
|
|
26
|
+
yield* Queue.offer(pending, undefined);
|
|
27
|
+
const consume = Effect.forever(
|
|
28
|
+
Effect.gen(function* () {
|
|
29
|
+
yield* Queue.take(pending);
|
|
30
|
+
yield* FiberHandle.clear(timer);
|
|
31
|
+
yield* state.running;
|
|
32
|
+
const value = yield* work.read;
|
|
33
|
+
yield* work.act(value, (cause) => Deferred.failCause(failed, cause));
|
|
34
|
+
const now = yield* Clock.currentTimeMillis;
|
|
35
|
+
const reported = work.due?.(value, now);
|
|
36
|
+
const nextDue = reported !== undefined && reported > now ? reported : undefined;
|
|
37
|
+
if (nextDue !== undefined) {
|
|
38
|
+
yield* FiberHandle.run(timer, Effect.andThen(Effect.sleep(nextDue - now), Queue.offer(pending, undefined)).pipe(Effect.asVoid));
|
|
39
|
+
}
|
|
40
|
+
yield* state.waiting(nextDue);
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
return yield* Effect.raceFirst(consume, Deferred.await(failed));
|
|
44
|
+
});
|