@smthrs/plan 1.0.0-rc.6 → 1.0.0-rc.8
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/README.md +16 -14
- package/dist/cjs/PlanDiff.d.ts +19 -4
- package/dist/cjs/PlanDiff.d.ts.map +1 -1
- package/dist/cjs/PlanDiff.js +29 -4
- package/dist/cjs/PlanDiff.js.map +2 -2
- package/dist/cjs/StepKey.d.ts.map +1 -1
- package/dist/cjs/StepKey.js +24 -22
- package/dist/cjs/StepKey.js.map +2 -2
- package/dist/cjs/internal/node.d.ts.map +1 -1
- package/dist/esm/PlanDiff.d.ts +19 -4
- package/dist/esm/PlanDiff.d.ts.map +1 -1
- package/dist/esm/PlanDiff.js +55 -8
- package/dist/esm/PlanDiff.js.map +1 -1
- package/dist/esm/StepKey.d.ts.map +1 -1
- package/dist/esm/StepKey.js +8 -5
- package/dist/esm/StepKey.js.map +1 -1
- package/dist/esm/internal/node.d.ts.map +1 -1
- package/docs/README.md +3 -1
- package/docs/api.md +1 -1
- package/docs/guides/diff-two-plans.md +153 -21
- package/docs/installation.md +5 -1
- package/docs/quickstart.md +1 -1
- package/package.json +4 -4
- package/src/PlanDiff.ts +49 -8
- package/src/StepKey.ts +28 -23
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Diff two plans"
|
|
3
|
-
description: "Compare a re-plan against the plan before it: what the verdict is, how a re-key is attributed to the field that caused it, and why the report is in no digest."
|
|
3
|
+
description: "Compare a re-plan against the plan before it: what the verdict is, how a re-key is attributed to the field that caused it, how a step whose positional id moved is paired by its key, and why the report is in no digest."
|
|
4
4
|
sidebar:
|
|
5
5
|
order: 5
|
|
6
6
|
---
|
|
@@ -8,28 +8,81 @@ sidebar:
|
|
|
8
8
|
"What is different about this run" is a set comparison, not archaeology.
|
|
9
9
|
`PlanDiff.diff` is that comparison as a value.
|
|
10
10
|
|
|
11
|
+
## Before you start
|
|
12
|
+
|
|
13
|
+
The examples run with `node <file>.ts` on Node.js 26.4.0 or later and need no
|
|
14
|
+
engine, database, or jj. Compiling a plan needs Effect's `Crypto` service, which
|
|
15
|
+
`@effect/platform-node` supplies. In a project that installed `@smthrs/flows`,
|
|
16
|
+
add this package and `@smthrs/flow` at the version of `@smthrs/flows` you have,
|
|
17
|
+
plus the platform package:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @smthrs/plan@1.0.0-rc.8 @smthrs/flow@1.0.0-rc.8 @effect/platform-node
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The outputs below are real output from the release that install line names.
|
|
24
|
+
|
|
25
|
+
## Compare a re-plan
|
|
26
|
+
|
|
27
|
+
Create `diff.ts`. It compiles the two drafts from
|
|
28
|
+
[Compile drafts into a plan](./compile-a-plan.md), edits one field of one
|
|
29
|
+
draft's body, compiles again, and compares:
|
|
30
|
+
|
|
11
31
|
```ts
|
|
12
32
|
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
|
|
33
|
+
import * as KeyMaterial from "@smthrs/plan/KeyMaterial"
|
|
13
34
|
import * as Plan from "@smthrs/plan/Plan"
|
|
14
35
|
import * as PlanDiff from "@smthrs/plan/PlanDiff"
|
|
15
|
-
import type * as Crypto from "effect/Crypto"
|
|
16
36
|
import * as Effect from "effect/Effect"
|
|
17
37
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
38
|
+
const readPr: Plan.NodeDraft = {
|
|
39
|
+
id: "read-pr",
|
|
40
|
+
material: {
|
|
41
|
+
version: KeyMaterial.version,
|
|
42
|
+
kind: "sealed",
|
|
43
|
+
body: { action: "read-pr", pr: 4821 },
|
|
44
|
+
inputs: [],
|
|
45
|
+
layers: [],
|
|
46
|
+
capabilities: ["net:get"]
|
|
47
|
+
},
|
|
48
|
+
effects: { reads: [], writes: ["pr.json"], boundaryMode: "hard" }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const runTests: Plan.NodeDraft = {
|
|
52
|
+
id: "run-tests",
|
|
53
|
+
material: {
|
|
54
|
+
version: KeyMaterial.version,
|
|
55
|
+
kind: "sealed",
|
|
56
|
+
body: { action: "run-tests" },
|
|
57
|
+
inputs: [{ _tag: "Ref", from: "read-pr", path: [] }],
|
|
58
|
+
layers: [],
|
|
59
|
+
capabilities: []
|
|
60
|
+
},
|
|
61
|
+
effects: { reads: ["pr.json"], writes: ["report.json"], boundaryMode: "hard" }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const compile = (nodes: ReadonlyArray<Plan.NodeDraft>) =>
|
|
65
|
+
Plan.compile({ planId: "review-4821", flow: "example/Review", nodes })
|
|
22
66
|
|
|
23
67
|
export const compare = Effect.gen(function*() {
|
|
24
|
-
const before = yield*
|
|
25
|
-
const after = yield*
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
})
|
|
68
|
+
const before = yield* compile([readPr, runTests])
|
|
69
|
+
const after = yield* compile([
|
|
70
|
+
readPr,
|
|
71
|
+
{ ...runTests, material: { ...runTests.material, body: { action: "run-tests", jobs: 4 } } }
|
|
72
|
+
])
|
|
30
73
|
const changes: PlanDiff.PlanDiff = PlanDiff.diff(before, after)
|
|
31
74
|
return changes.rekeyed.map((entry) => `${entry.id}: ${entry.changed.join(", ")}`)
|
|
32
75
|
}).pipe(Effect.provide(NodeCrypto.layer))
|
|
76
|
+
|
|
77
|
+
console.log(await Effect.runPromise(compare))
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
node diff.ts
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
[ 'run-tests: body' ]
|
|
33
86
|
```
|
|
34
87
|
|
|
35
88
|
`compare` answers `["run-tests: body"]`. The edit moved one field of one node's
|
|
@@ -37,15 +90,16 @@ body, so that node re-keyed and the report says which field did it.
|
|
|
37
90
|
|
|
38
91
|
## The verdict is the key
|
|
39
92
|
|
|
40
|
-
Two nodes with the same
|
|
41
|
-
needs saying, and `diff` says nothing else. Four lists come
|
|
93
|
+
Two nodes with the same key are the same step, under the same id or a moved
|
|
94
|
+
one. Nothing else needs saying, and `diff` says nothing else. Four lists come
|
|
95
|
+
back:
|
|
42
96
|
|
|
43
|
-
| Field | Contents
|
|
44
|
-
| ----------- |
|
|
45
|
-
| `added` | Node ids
|
|
46
|
-
| `removed` | Node ids
|
|
47
|
-
| `rekeyed` | Nodes
|
|
48
|
-
| `unchanged` | Node ids
|
|
97
|
+
| Field | Contents |
|
|
98
|
+
| ----------- | --------------------------------------------------------------------- |
|
|
99
|
+
| `added` | Node ids in `next` that pair with no node in `previous`. |
|
|
100
|
+
| `removed` | Node ids in `previous` that pair with no node in `next`. |
|
|
101
|
+
| `rekeyed` | Nodes paired by id whose key moved, with `from`, `to`, and `changed`. |
|
|
102
|
+
| `unchanged` | Node ids in `next` paired with a node of the same key. |
|
|
49
103
|
|
|
50
104
|
## The attribution is for a human
|
|
51
105
|
|
|
@@ -70,6 +124,80 @@ reported as nothing changed.
|
|
|
70
124
|
key moved for a reason the comparison does not model. Treat an empty `changed`
|
|
71
125
|
on a re-keyed node as a bug report, not as noise.
|
|
72
126
|
|
|
127
|
+
## Node ids are positional
|
|
128
|
+
|
|
129
|
+
A node id is the node's structural address in the body, such as
|
|
130
|
+
`root.flow.andThen`, not the name of its step. Adding a step to a flow can move
|
|
131
|
+
the steps around it to new addresses while their keys stay the same. `diff`
|
|
132
|
+
therefore pairs nodes in three passes, each over the nodes no earlier pass
|
|
133
|
+
paired:
|
|
134
|
+
|
|
135
|
+
1. The same id with the same key: unchanged.
|
|
136
|
+
2. The same key under a different id: the same step at a new address, reported
|
|
137
|
+
as unchanged under its id in `next`.
|
|
138
|
+
3. The same id with a different key: re-keyed.
|
|
139
|
+
|
|
140
|
+
Whatever is left in `next` is `added`, and whatever is left in `previous` is
|
|
141
|
+
`removed`. `added`, `rekeyed`, and `unchanged` name nodes by their id in
|
|
142
|
+
`next`; `removed` names nodes by their id in `previous`.
|
|
143
|
+
|
|
144
|
+
Create `inserted.ts`. It compiles two steps, then the same two steps with a
|
|
145
|
+
third inserted before them, so every id shifts by one:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
|
|
149
|
+
import * as KeyMaterial from "@smthrs/plan/KeyMaterial"
|
|
150
|
+
import * as Plan from "@smthrs/plan/Plan"
|
|
151
|
+
import * as PlanDiff from "@smthrs/plan/PlanDiff"
|
|
152
|
+
import * as Effect from "effect/Effect"
|
|
153
|
+
|
|
154
|
+
/** A step whose id is its position, as a flow's structural ids are. */
|
|
155
|
+
const step = (position: number, action: string): Plan.NodeDraft => ({
|
|
156
|
+
id: `step-${position}`,
|
|
157
|
+
material: {
|
|
158
|
+
version: KeyMaterial.version,
|
|
159
|
+
kind: "sealed",
|
|
160
|
+
body: { action },
|
|
161
|
+
inputs: [],
|
|
162
|
+
layers: [],
|
|
163
|
+
capabilities: []
|
|
164
|
+
},
|
|
165
|
+
effects: { reads: [], writes: [], boundaryMode: "hard" }
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
const compile = (actions: ReadonlyArray<string>) =>
|
|
169
|
+
Plan.compile({
|
|
170
|
+
planId: "checks-1",
|
|
171
|
+
flow: "example/Checks",
|
|
172
|
+
nodes: actions.map((action, index) => step(index, action))
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const program = Effect.gen(function*() {
|
|
176
|
+
const previous = yield* compile(["lint", "test"])
|
|
177
|
+
const next = yield* compile(["format", "lint", "test"])
|
|
178
|
+
console.log(PlanDiff.diff(previous, next))
|
|
179
|
+
}).pipe(Effect.provide(NodeCrypto.layer))
|
|
180
|
+
|
|
181
|
+
await Effect.runPromise(program)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
node inserted.ts
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
```text
|
|
189
|
+
{
|
|
190
|
+
added: [ 'step-0' ],
|
|
191
|
+
removed: [],
|
|
192
|
+
rekeyed: [],
|
|
193
|
+
unchanged: [ 'step-1', 'step-2' ]
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Only the inserted `format` step is new. `lint` and `test` moved from `step-0`
|
|
198
|
+
and `step-1` to `step-1` and `step-2` with the keys they had, so they come back
|
|
199
|
+
as `unchanged`.
|
|
200
|
+
|
|
73
201
|
## Comparison uses the key's own JSON semantics
|
|
74
202
|
|
|
75
203
|
Each compared field is projected through the same JSON mirror the keys are
|
|
@@ -88,7 +216,9 @@ The pairing this is built for is a re-plan of the same flow: compile the flow
|
|
|
88
216
|
again against the current source and compare the result with the plan an
|
|
89
217
|
operator last saw. `added` and `removed` are the shape change, `rekeyed` is the
|
|
90
218
|
work that will not come from cache, and `unchanged` is everything a cache hit
|
|
91
|
-
will skip.
|
|
219
|
+
will skip. That reading holds when a step is added or removed and ids move,
|
|
220
|
+
because `diff` pairs a moved step by its key, as
|
|
221
|
+
[Node ids are positional](#node-ids-are-positional) shows.
|
|
92
222
|
|
|
93
223
|
The command line prints the same comparison through
|
|
94
224
|
[`smthrs flow plan`](/cli/flow), and the plan card it renders is what
|
|
@@ -96,6 +226,8 @@ The command line prints the same comparison through
|
|
|
96
226
|
|
|
97
227
|
## Next
|
|
98
228
|
|
|
229
|
+
- [Inspect the plan a body builds](../../../flow/docs/guides/inspect-the-plan.md):
|
|
230
|
+
build a flow's graph and read its nodes before anything runs.
|
|
99
231
|
- [Step keys](../concepts/step-keys.md): why an upstream edit re-keys a
|
|
100
232
|
downstream node whose own declaration did not change.
|
|
101
233
|
- [The plan value](../concepts/plan-value.md): the digests a diff sits beside.
|
package/docs/installation.md
CHANGED
|
@@ -13,7 +13,11 @@ sidebar:
|
|
|
13
13
|
|
|
14
14
|
## Install
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
```bash
|
|
17
|
+
npm install @smthrs/plan@next effect
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The `next` tag carries the 1.0 release candidates.
|
|
17
21
|
|
|
18
22
|
That is enough to build node graphs, compile plans, and diff them. Compiling
|
|
19
23
|
asks for Effect's `Crypto` service, which a platform package supplies:
|
package/docs/quickstart.md
CHANGED
|
@@ -17,7 +17,7 @@ Nothing here executes a node or writes a row. A plan is inert: persisting one is
|
|
|
17
17
|
## Prerequisites
|
|
18
18
|
|
|
19
19
|
- Node.js 26.4.0 or later.
|
|
20
|
-
- A
|
|
20
|
+
- A project with the packages installed: `npm install @smthrs/plan@next @effect/platform-node effect`.
|
|
21
21
|
|
|
22
22
|
`@effect/platform-node` supplies Effect's `Crypto` service, which is the only
|
|
23
23
|
thing compiling a plan asks for.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smthrs/plan",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.0-rc.
|
|
4
|
+
"version": "1.0.0-rc.8",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"smthrs": {
|
|
7
7
|
"group": "engine"
|
|
@@ -218,9 +218,9 @@
|
|
|
218
218
|
"tag": "next"
|
|
219
219
|
},
|
|
220
220
|
"dependencies": {
|
|
221
|
-
"@smthrs/crypto": "1.0.0-rc.
|
|
222
|
-
"@smthrs/keys": "1.0.0-rc.
|
|
223
|
-
"@smthrs/canonical": "1.0.0-rc.
|
|
221
|
+
"@smthrs/crypto": "1.0.0-rc.8",
|
|
222
|
+
"@smthrs/keys": "1.0.0-rc.8",
|
|
223
|
+
"@smthrs/canonical": "1.0.0-rc.8"
|
|
224
224
|
},
|
|
225
225
|
"devDependencies": {
|
|
226
226
|
"@effect/language-service": "0.87.2",
|
package/src/PlanDiff.ts
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
* 'what is different about this run' is a set comparison, not archaeology."
|
|
7
7
|
* This module is the set comparison.
|
|
8
8
|
*
|
|
9
|
-
* The **verdict** is the key: two nodes with the same
|
|
10
|
-
* the same
|
|
11
|
-
* `changed: ["input[1]"]` — is a report for a human, derived
|
|
12
|
-
* declarations field by field. It is deliberately not part of any digest.
|
|
9
|
+
* The **verdict** is the key: two nodes with the same key are the same step,
|
|
10
|
+
* under the same id or a moved one, and nothing else needs saying. The
|
|
11
|
+
* **attribution** — `changed: ["input[1]"]` — is a report for a human, derived
|
|
12
|
+
* by comparing the declarations field by field. It is deliberately not part of any digest.
|
|
13
13
|
*
|
|
14
14
|
* @since 0.1.0
|
|
15
15
|
*/
|
|
@@ -157,24 +157,65 @@ const changedFields = (
|
|
|
157
157
|
/**
|
|
158
158
|
* Compares a plan against the last plan for the same flow.
|
|
159
159
|
*
|
|
160
|
+
* Nodes pair in three passes, each over the nodes no earlier pass paired:
|
|
161
|
+
* the same id with the same key, then the same key under any id, then the
|
|
162
|
+
* same id with a different key. The second pass exists because flow node ids
|
|
163
|
+
* can be positional: inserting a step shifts the ids after it while their
|
|
164
|
+
* keys stay equal, and that is one added step, not a re-key of every later
|
|
165
|
+
* one (#3905).
|
|
166
|
+
*
|
|
167
|
+
* `added`, `rekeyed`, and `unchanged` name nodes by their id in `next`;
|
|
168
|
+
* `removed` names nodes by their id in `previous`. A moved id can therefore
|
|
169
|
+
* appear once in `removed` or `added` and once in `unchanged`.
|
|
170
|
+
*
|
|
171
|
+
* @param previous - The last plan recorded for the flow.
|
|
172
|
+
* @param next - The plan to compare against it.
|
|
173
|
+
* @returns The added, removed, re-keyed, and unchanged node ids, each in its
|
|
174
|
+
* plan's node order.
|
|
160
175
|
* @since 0.1.0
|
|
161
176
|
* @category constructors
|
|
162
177
|
* @slop
|
|
163
178
|
*/
|
|
164
179
|
export const diff = (previous: Plan.Plan, next: Plan.Plan): PlanDiff => {
|
|
180
|
+
// pairs maps a next node to the previous node it is the same step as.
|
|
181
|
+
const pairs = new Map<Plan.PlanNode, Plan.PlanNode>()
|
|
182
|
+
const paired = new Set<Plan.PlanNode>()
|
|
183
|
+
const pair = (node: Plan.PlanNode, original: Plan.PlanNode) => {
|
|
184
|
+
pairs.set(node, original)
|
|
185
|
+
paired.add(original)
|
|
186
|
+
}
|
|
165
187
|
const before = new Map(previous.nodes.map((node) => [node.id, node]))
|
|
166
|
-
const
|
|
188
|
+
for (const node of next.nodes) {
|
|
189
|
+
const original = before.get(node.id)
|
|
190
|
+
if (original !== undefined && original.key === node.key) pair(node, original)
|
|
191
|
+
}
|
|
192
|
+
const byKey = new Map<string, Array<Plan.PlanNode>>()
|
|
193
|
+
for (const node of previous.nodes) {
|
|
194
|
+
if (paired.has(node)) continue
|
|
195
|
+
const same = byKey.get(node.key)
|
|
196
|
+
if (same === undefined) byKey.set(node.key, [node])
|
|
197
|
+
else same.push(node)
|
|
198
|
+
}
|
|
199
|
+
for (const node of next.nodes) {
|
|
200
|
+
if (pairs.has(node)) continue
|
|
201
|
+
const original = byKey.get(node.key)?.shift()
|
|
202
|
+
if (original !== undefined) pair(node, original)
|
|
203
|
+
}
|
|
167
204
|
const rekeyedIds = new Set<string>()
|
|
168
205
|
for (const node of next.nodes) {
|
|
206
|
+
if (pairs.has(node)) continue
|
|
169
207
|
const original = before.get(node.id)
|
|
170
|
-
if (original !== undefined && original
|
|
208
|
+
if (original !== undefined && !paired.has(original)) {
|
|
209
|
+
pair(node, original)
|
|
210
|
+
rekeyedIds.add(node.id)
|
|
211
|
+
}
|
|
171
212
|
}
|
|
172
213
|
const added: Array<string> = []
|
|
173
214
|
const rekeyed: Array<Rekeyed> = []
|
|
174
215
|
const unchanged: Array<string> = []
|
|
175
216
|
const sameField = fieldComparison()
|
|
176
217
|
for (const node of next.nodes) {
|
|
177
|
-
const original =
|
|
218
|
+
const original = pairs.get(node)
|
|
178
219
|
if (original === undefined) {
|
|
179
220
|
added.push(node.id)
|
|
180
221
|
continue
|
|
@@ -192,7 +233,7 @@ export const diff = (previous: Plan.Plan, next: Plan.Plan): PlanDiff => {
|
|
|
192
233
|
}
|
|
193
234
|
return {
|
|
194
235
|
added,
|
|
195
|
-
removed: previous.nodes.filter((node) => !
|
|
236
|
+
removed: previous.nodes.filter((node) => !paired.has(node)).map((node) => node.id),
|
|
196
237
|
rekeyed,
|
|
197
238
|
unchanged
|
|
198
239
|
}
|
package/src/StepKey.ts
CHANGED
|
@@ -202,31 +202,36 @@ export interface DigestMemo {
|
|
|
202
202
|
*/
|
|
203
203
|
export const makeDigestMemo = (): DigestMemo => {
|
|
204
204
|
const entries = new Map<string, Deferred.Deferred<StepKey, Schema.SchemaError>>()
|
|
205
|
+
// Registering the entry and installing its exit handler run uninterruptibly,
|
|
206
|
+
// so an interrupt can never land between them and strand a Deferred that no
|
|
207
|
+
// fiber will complete. The wait and the computation stay interruptible.
|
|
205
208
|
const digest: DigestMemo["digest"] = (from, path, compute) =>
|
|
206
|
-
Effect.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
Effect.
|
|
222
|
-
|
|
223
|
-
entries.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
209
|
+
Effect.uninterruptibleMask((restore) =>
|
|
210
|
+
Effect.suspend(() => {
|
|
211
|
+
const address = JSON.stringify([from, path])
|
|
212
|
+
const existing = entries.get(address)
|
|
213
|
+
if (existing !== undefined) {
|
|
214
|
+
return restore(Effect.flatMap(Effect.exit(Deferred.await(existing)), (exit) => {
|
|
215
|
+
if (Exit.isSuccess(exit)) return Effect.succeed(exit.value)
|
|
216
|
+
// A waiter did not request the leader's cancellation. It competes to
|
|
217
|
+
// become the replacement leader instead of inheriting that interrupt.
|
|
218
|
+
return Cause.hasInterrupts(exit.cause) ? digest(from, path, compute) : Effect.failCause(exit.cause)
|
|
219
|
+
}))
|
|
220
|
+
}
|
|
221
|
+
const pending = Deferred.makeUnsafe<StepKey, Schema.SchemaError>()
|
|
222
|
+
entries.set(address, pending)
|
|
223
|
+
return restore(compute).pipe(
|
|
224
|
+
Effect.onExit((exit) =>
|
|
225
|
+
Effect.gen(function*() {
|
|
226
|
+
if (Exit.isFailure(exit) && entries.get(address) === pending) {
|
|
227
|
+
entries.delete(address)
|
|
228
|
+
}
|
|
229
|
+
yield* Deferred.done(pending, exit)
|
|
230
|
+
})
|
|
231
|
+
)
|
|
227
232
|
)
|
|
228
|
-
)
|
|
229
|
-
|
|
233
|
+
})
|
|
234
|
+
)
|
|
230
235
|
return { digest }
|
|
231
236
|
}
|
|
232
237
|
|