@playfast/reform-proof 1.2.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assert.d.ts +15 -0
- package/dist/assert.d.ts.map +1 -0
- package/dist/assert.js +39 -0
- package/dist/assert.js.map +1 -0
- package/dist/engine.d.ts +16 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +123 -0
- package/dist/engine.js.map +1 -0
- package/dist/engineMounted.d.ts +18 -0
- package/dist/engineMounted.d.ts.map +1 -0
- package/dist/engineMounted.js +96 -0
- package/dist/engineMounted.js.map +1 -0
- package/dist/engineMounts.d.ts +13 -0
- package/dist/engineMounts.d.ts.map +1 -0
- package/dist/engineMounts.js +116 -0
- package/dist/engineMounts.js.map +1 -0
- package/dist/engineRender.d.ts +10 -0
- package/dist/engineRender.d.ts.map +1 -0
- package/dist/engineRender.js +64 -0
- package/dist/engineRender.js.map +1 -0
- package/dist/engineSink.d.ts +71 -0
- package/dist/engineSink.d.ts.map +1 -0
- package/dist/engineSink.js +73 -0
- package/dist/engineSink.js.map +1 -0
- package/dist/engineTree.d.ts +63 -0
- package/dist/engineTree.d.ts.map +1 -0
- package/dist/engineTree.js +137 -0
- package/dist/engineTree.js.map +1 -0
- package/dist/engineTreeDriver.d.ts +5 -0
- package/dist/engineTreeDriver.d.ts.map +1 -0
- package/dist/engineTreeDriver.js +187 -0
- package/dist/engineTreeDriver.js.map +1 -0
- package/dist/engineTreeFacade.d.ts +5 -0
- package/dist/engineTreeFacade.d.ts.map +1 -0
- package/dist/engineTreeFacade.js +124 -0
- package/dist/engineTreeFacade.js.map +1 -0
- package/dist/engineTreeTypes.d.ts +80 -0
- package/dist/engineTreeTypes.d.ts.map +1 -0
- package/dist/engineTreeTypes.js +11 -0
- package/dist/engineTreeTypes.js.map +1 -0
- package/dist/errors.d.ts +39 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +30 -0
- package/dist/errors.js.map +1 -0
- package/dist/facade.d.ts +54 -0
- package/dist/facade.d.ts.map +1 -0
- package/dist/facade.js +2 -0
- package/dist/facade.js.map +1 -0
- package/dist/fingerprint.d.ts +2 -0
- package/dist/fingerprint.d.ts.map +1 -0
- package/dist/fingerprint.js +78 -0
- package/dist/fingerprint.js.map +1 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +73 -0
- package/dist/index.js.map +1 -0
- package/dist/product.d.ts +56 -0
- package/dist/product.d.ts.map +1 -0
- package/dist/product.js +35 -0
- package/dist/product.js.map +1 -0
- package/dist/proofCase.d.ts +24 -0
- package/dist/proofCase.d.ts.map +1 -0
- package/dist/proofCase.js +2 -0
- package/dist/proofCase.js.map +1 -0
- package/dist/runner.d.ts +15 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +13 -0
- package/dist/runner.js.map +1 -0
- package/package.json +17 -5
- package/src/engine.ts +17 -13
- package/src/engineMounted.ts +10 -12
- package/src/engineSink.ts +15 -1
- package/src/engineTreeFacade.ts +48 -13
- package/src/fingerprint.ts +45 -10
- package/src/index.ts +1 -1
- package/src/proof-attribution.test.ts +210 -0
- package/src/test-clock.test.ts +16 -6
package/dist/index.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Effect, Layer, TestContext } from 'effect';
|
|
2
|
+
import { ProofRunner, proofRunnerLayer, withProofRunner } from './runner.js';
|
|
3
|
+
import { isRecord } from './assert.js';
|
|
4
|
+
export { ProofRunner, proofRunnerLayer, withProofRunner };
|
|
5
|
+
export { makeFacade, makeProofFacade, makeRuntimeHandleFacade, makeRuntimeTreeFacade, makeSink, proofLayer, } from './engine.js';
|
|
6
|
+
export { Product, ProductRequirement } from './product.js';
|
|
7
|
+
export { expect, matchProps } from './assert.js';
|
|
8
|
+
const implement = (requirement, scene, body) => scene.capture((exactScene) => {
|
|
9
|
+
const proofCase = {
|
|
10
|
+
requirement,
|
|
11
|
+
scene: exactScene,
|
|
12
|
+
placement: 'Root',
|
|
13
|
+
body,
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
requirement,
|
|
17
|
+
scene: exactScene,
|
|
18
|
+
capture: (visit) => visit(proofCase),
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
const implementVia = (requirement, scene, body) => scene.capture((exactScene) => {
|
|
22
|
+
const proofCase = {
|
|
23
|
+
requirement,
|
|
24
|
+
scene: exactScene,
|
|
25
|
+
placement: 'Slot',
|
|
26
|
+
body,
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
requirement,
|
|
30
|
+
scene: exactScene,
|
|
31
|
+
capture: (visit) => visit(proofCase),
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
const suite = (product, config) => ({
|
|
35
|
+
kind: 'ProofSuite',
|
|
36
|
+
product,
|
|
37
|
+
proofs: config.proofs,
|
|
38
|
+
});
|
|
39
|
+
export const isProofSuite = (candidate) => isRecord(candidate) && candidate['kind'] === 'ProofSuite' && Array.isArray(candidate['proofs']);
|
|
40
|
+
const run = (proofSuite) => Effect.runPromise(runEffect(proofSuite));
|
|
41
|
+
const runEffect = (proofSuite) => ProofRunner.pipe(Effect.flatMap((runner) => Effect.forEach(proofSuite.proofs, (proof) => runner.executeProofEffect(proof))), Effect.map((reports) => ({
|
|
42
|
+
product: proofSuite.product.manifest.name,
|
|
43
|
+
results: reports,
|
|
44
|
+
ok: reports.every((report) => report.ok),
|
|
45
|
+
})), Effect.provide(proofRunnerLayer));
|
|
46
|
+
const driver = (proofSuite) => proofSuite.proofs.map((proof) => ({
|
|
47
|
+
requirement: proof.requirement.manifest.statement,
|
|
48
|
+
run: () => withProofRunner((runner) => runner.driveProof(proof)),
|
|
49
|
+
}));
|
|
50
|
+
/**
|
|
51
|
+
* Wrap a scene's layers in a virtual clock, so every `Effect.sleep`, `Schedule` and
|
|
52
|
+
* `Clock` read its procedures make elapses only when the proof says so via
|
|
53
|
+
* `app.advance(...)`:
|
|
54
|
+
*
|
|
55
|
+
* ```ts
|
|
56
|
+
* const MyScene = scene(MyComposition, { provide: [Proof.withTestClock(App)] })
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* It has to WRAP the scene's layers, never sit beside them in `provide`. The engine
|
|
60
|
+
* forks its loop while its layer is being built, and a forked fiber inherits the
|
|
61
|
+
* clock in scope at that moment — so a clock merged as a sibling reaches the proof
|
|
62
|
+
* body but not the procedures whose timers it is meant to control. That mis-wiring
|
|
63
|
+
* is silent (`app.advance(...)` finds a clock, adjusts it, and no product timer
|
|
64
|
+
* moves), which is why the clock is exposed as this combinator, not a bare layer.
|
|
65
|
+
*
|
|
66
|
+
* Opt-in per scene rather than on by default: a scene without it keeps the real
|
|
67
|
+
* clock, so proofs that observe product timers by waiting stay correct. Once a scene
|
|
68
|
+
* has it, a product timer NEVER fires on its own — an unadvanced proof sees the
|
|
69
|
+
* pre-timer state, which is the point.
|
|
70
|
+
*/
|
|
71
|
+
const withTestClock = (layer) => layer.pipe(Layer.provideMerge(TestContext.TestContext));
|
|
72
|
+
export const Proof = { implement, implementVia, suite, run, runEffect, driver, withTestClock };
|
|
73
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Option } from 'effect';
|
|
2
|
+
import type { AnyComposition } from '@playfast/reform/internal';
|
|
3
|
+
import type { ContractOf, EventNamesOf } from './facade.js';
|
|
4
|
+
export type ProductComposition = AnyComposition & {
|
|
5
|
+
readonly identity: symbol;
|
|
6
|
+
};
|
|
7
|
+
declare const RequirementCompositionTypeId: unique symbol;
|
|
8
|
+
export interface AnyRequirementClass {
|
|
9
|
+
new (): {};
|
|
10
|
+
readonly manifest: {
|
|
11
|
+
readonly kind: 'ProductRequirement';
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly statement: string;
|
|
14
|
+
readonly composition: ProductComposition;
|
|
15
|
+
readonly events: Option.Option<ReadonlyArray<PropertyKey>>;
|
|
16
|
+
};
|
|
17
|
+
readonly capture: <Result>(visit: RequirementCapture<Result>) => Result;
|
|
18
|
+
}
|
|
19
|
+
export type RequirementCapture<Result> = <Comp extends ProductComposition, Statement extends string>(requirement: RequirementClass<Comp, Statement>) => Result;
|
|
20
|
+
export interface RequirementManifest<Comp extends ProductComposition, Statement extends string> {
|
|
21
|
+
readonly kind: 'ProductRequirement';
|
|
22
|
+
readonly name: Statement;
|
|
23
|
+
readonly statement: Statement;
|
|
24
|
+
readonly composition: Comp;
|
|
25
|
+
readonly events: Option.Option<ReadonlyArray<EventNamesOf<ContractOf<Comp>>>>;
|
|
26
|
+
}
|
|
27
|
+
export interface RequirementClass<Comp extends ProductComposition, Statement extends string> extends AnyRequirementClass {
|
|
28
|
+
readonly manifest: RequirementManifest<Comp, Statement>;
|
|
29
|
+
readonly [RequirementCompositionTypeId]: (composition: Comp) => Comp;
|
|
30
|
+
}
|
|
31
|
+
export interface ProductManifest<Comp extends ProductComposition> {
|
|
32
|
+
readonly kind: 'Product';
|
|
33
|
+
readonly name: string;
|
|
34
|
+
readonly composition: Comp;
|
|
35
|
+
readonly requirements: ReadonlyArray<AnyRequirementClass>;
|
|
36
|
+
}
|
|
37
|
+
export interface ProductClass<Comp extends ProductComposition = ProductComposition> {
|
|
38
|
+
new (): {};
|
|
39
|
+
readonly manifest: ProductManifest<Comp>;
|
|
40
|
+
}
|
|
41
|
+
interface MakeRequirementOptionsExternalApi<Comp extends ProductComposition> {
|
|
42
|
+
readonly events?: ReadonlyArray<EventNamesOf<ContractOf<Comp>>>;
|
|
43
|
+
}
|
|
44
|
+
declare const makeRequirement: <Comp extends ProductComposition, const Statement extends string>(composition: Comp, statement: Statement, options?: MakeRequirementOptionsExternalApi<Comp>) => RequirementClass<Comp, Statement>;
|
|
45
|
+
export declare const ProductRequirement: {
|
|
46
|
+
readonly make: typeof makeRequirement;
|
|
47
|
+
};
|
|
48
|
+
interface MakeProductConfig<Comp extends ProductComposition> {
|
|
49
|
+
readonly requirements: ReadonlyArray<RequirementClass<Comp, string>>;
|
|
50
|
+
}
|
|
51
|
+
declare const makeProduct: <Comp extends ProductComposition>(composition: Comp, config: MakeProductConfig<Comp>) => ProductClass<Comp>;
|
|
52
|
+
export declare const Product: {
|
|
53
|
+
readonly make: typeof makeProduct;
|
|
54
|
+
};
|
|
55
|
+
export {};
|
|
56
|
+
//# sourceMappingURL=product.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"product.d.ts","sourceRoot":"","sources":["../src/product.ts"],"names":[],"mappings":"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"}
|
package/dist/product.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Option } from 'effect';
|
|
2
|
+
const RequirementCompositionTypeId = Symbol.for('reform-proof/RequirementComposition');
|
|
3
|
+
const makeRequirement = (composition, statement, options) => {
|
|
4
|
+
const manifest = {
|
|
5
|
+
kind: 'ProductRequirement',
|
|
6
|
+
name: statement,
|
|
7
|
+
statement,
|
|
8
|
+
composition,
|
|
9
|
+
events: Option.fromNullable(options?.events),
|
|
10
|
+
};
|
|
11
|
+
class Requirement {
|
|
12
|
+
static manifest = manifest;
|
|
13
|
+
static [RequirementCompositionTypeId] = (exact) => exact;
|
|
14
|
+
static capture = (visit) => visit(Requirement);
|
|
15
|
+
}
|
|
16
|
+
return Requirement;
|
|
17
|
+
};
|
|
18
|
+
export const ProductRequirement = {
|
|
19
|
+
make: makeRequirement,
|
|
20
|
+
};
|
|
21
|
+
const makeProduct = (composition, config) => {
|
|
22
|
+
const manifest = {
|
|
23
|
+
kind: 'Product',
|
|
24
|
+
name: composition.manifest.name,
|
|
25
|
+
composition,
|
|
26
|
+
requirements: config.requirements,
|
|
27
|
+
};
|
|
28
|
+
return class {
|
|
29
|
+
static manifest = manifest;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
export const Product = {
|
|
33
|
+
make: makeProduct,
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=product.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"product.js","sourceRoot":"","sources":["../src/product.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAM/B,MAAM,4BAA4B,GAAkB,MAAM,CAAC,GAAG,CAC5D,qCAAqC,CACtC,CAAA;AAqDD,MAAM,eAAe,GAAG,CACtB,WAAiB,EACjB,SAAoB,EACpB,OAAiD,EACd,EAAE;IACrC,MAAM,QAAQ,GAAyC;QACrD,IAAI,EAAE,oBAAoB;QAC1B,IAAI,EAAE,SAAS;QACf,SAAS;QACT,WAAW;QACX,MAAM,EAAE,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC;KAC7C,CAAA;IACD,MAAM,WAAW;QACf,MAAM,CAAU,QAAQ,GAAG,QAAQ,CAAA;QACnC,MAAM,CAAU,CAAC,4BAA4B,CAAC,GAAG,CAAC,KAAW,EAAQ,EAAE,CAAC,KAAK,CAAA;QAC7E,MAAM,CAAU,OAAO,GAAG,CAAS,KAAiC,EAAU,EAAE,CAC9E,KAAK,CAAC,WAAW,CAAC,CAAA;;IAEtB,OAAO,WAAW,CAAA;AACpB,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,kBAAkB,GAA8C;IAC3E,IAAI,EAAE,eAAe;CACtB,CAAA;AAMD,MAAM,WAAW,GAAG,CAClB,WAAiB,EACjB,MAA+B,EACX,EAAE;IACtB,MAAM,QAAQ,GAA0B;QACtC,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,WAAW,CAAC,QAAQ,CAAC,IAAI;QAC/B,WAAW;QACX,YAAY,EAAE,MAAM,CAAC,YAAY;KAClC,CAAA;IACD,OAAO;QACL,MAAM,CAAU,QAAQ,GAAG,QAAQ,CAAA;KACpC,CAAA;AACH,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,OAAO,GAA0C;IAC5D,IAAI,EAAE,WAAW;CAClB,CAAA"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Effect } from 'effect';
|
|
2
|
+
import type { YieldWrap } from 'effect/Utils';
|
|
3
|
+
import type { CapturedScene, UiContract } from '@playfast/reform';
|
|
4
|
+
import type { Facade } from './facade.js';
|
|
5
|
+
import type { ProductComposition, RequirementClass } from './product.js';
|
|
6
|
+
export type ProofGenerator = Generator<YieldWrap<Effect.Effect<unknown, unknown, never>>, void, unknown>;
|
|
7
|
+
export interface ProofCase<Comp extends ProductComposition, C extends UiContract, S extends ReadonlyArray<unknown>, Services, P, N extends string, Identity> {
|
|
8
|
+
readonly requirement: RequirementClass<Comp, string>;
|
|
9
|
+
readonly scene: CapturedScene<C, S, Services, P, N, Identity>;
|
|
10
|
+
readonly placement: 'Root' | 'Slot';
|
|
11
|
+
readonly body: (app: Facade<C, never>) => ProofGenerator;
|
|
12
|
+
}
|
|
13
|
+
export type ProofCapture<Result> = <Comp extends ProductComposition, C extends UiContract, S extends ReadonlyArray<unknown>, Services, P, N extends string, Identity>(proof: ProofCase<Comp, C, S, Services, P, N, Identity>) => Result;
|
|
14
|
+
export interface ProofResult {
|
|
15
|
+
readonly requirement: string;
|
|
16
|
+
readonly ok: boolean;
|
|
17
|
+
readonly error: string | undefined;
|
|
18
|
+
}
|
|
19
|
+
export interface SuiteResult {
|
|
20
|
+
readonly product: string;
|
|
21
|
+
readonly results: ReadonlyArray<ProofResult>;
|
|
22
|
+
readonly ok: boolean;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=proofCase.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proofCase.d.ts","sourceRoot":"","sources":["../src/proofCase.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAC7C,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AACjE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACtC,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AAErE,MAAM,MAAM,cAAc,GAAG,SAAS,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,CAAA;AAExG,MAAM,WAAW,SAAS,CACxB,IAAI,SAAS,kBAAkB,EAC/B,CAAC,SAAS,UAAU,EACpB,CAAC,SAAS,aAAa,CAAC,OAAO,CAAC,EAChC,QAAQ,EACR,CAAC,EACD,CAAC,SAAS,MAAM,EAChB,QAAQ;IAER,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IACpD,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAA;IAC7D,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAA;IACnC,QAAQ,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,cAAc,CAAA;CACzD;AAED,MAAM,MAAM,YAAY,CAAC,MAAM,IAAI,CACjC,IAAI,SAAS,kBAAkB,EAC/B,CAAC,SAAS,UAAU,EACpB,CAAC,SAAS,aAAa,CAAC,OAAO,CAAC,EAChC,QAAQ,EACR,CAAC,EACD,CAAC,SAAS,MAAM,EAChB,QAAQ,EAER,KAAK,EAAE,SAAS,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC,KACnD,MAAM,CAAA;AAEX,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAA;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAA;CACnC;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,WAAW,CAAC,CAAA;IAC5C,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAA;CACrB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proofCase.js","sourceRoot":"","sources":["../src/proofCase.ts"],"names":[],"mappings":""}
|
package/dist/runner.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from 'effect';
|
|
2
|
+
import type { DriveResult, Proof, ProofResult } from './index.js';
|
|
3
|
+
export interface ProofRunnerApi {
|
|
4
|
+
readonly executeProofEffect: (proof: Proof) => Effect.Effect<ProofResult, never, never>;
|
|
5
|
+
readonly driveProofEffect: (proof: Proof) => Effect.Effect<DriveResult, never, never>;
|
|
6
|
+
readonly executeProof: (proof: Proof) => Promise<ProofResult>;
|
|
7
|
+
readonly driveProof: (proof: Proof) => Promise<DriveResult>;
|
|
8
|
+
}
|
|
9
|
+
declare const ProofRunnerBase: Context.TagClass<ProofRunner, 'reform-proof/ProofRunner', ProofRunnerApi>;
|
|
10
|
+
export declare class ProofRunner extends ProofRunnerBase {
|
|
11
|
+
}
|
|
12
|
+
export declare const proofRunnerLayer: Layer.Layer<ProofRunner>;
|
|
13
|
+
export declare const withProofRunner: <A>(use: (runner: ProofRunnerApi) => Promise<A>) => Promise<A>;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AAE/C,OAAO,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAE9D,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,kBAAkB,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,KAAK,EAAE,KAAK,CAAC,CAAA;IACvF,QAAQ,CAAC,gBAAgB,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,KAAK,EAAE,KAAK,CAAC,CAAA;IACrF,QAAQ,CAAC,YAAY,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,WAAW,CAAC,CAAA;IAC7D,QAAQ,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,WAAW,CAAC,CAAA;CAC5D;AAED,QAAA,MAAM,eAAe,EAAE,OAAO,CAAC,QAAQ,CAAC,WAAW,EAAE,0BAA0B,EAAE,cAAc,CACvB,CAAA;AAExE,qBAAa,WAAY,SAAQ,eAAe;CAAG;AAEnD,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,KAAK,CAAC,WAAW,CAKpD,CAAA;AAEF,eAAO,MAAM,eAAe,GAAI,CAAC,EAAE,KAAK,CAAC,MAAM,EAAE,cAAc,KAAK,OAAO,CAAC,CAAC,CAAC,KAAG,OAAO,CAAC,CAAC,CAMvF,CAAA"}
|
package/dist/runner.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from 'effect';
|
|
2
|
+
import { driveProof, driveProofEffect, executeProof, executeProofEffect } from './engine.js';
|
|
3
|
+
const ProofRunnerBase = Context.Tag('reform-proof/ProofRunner')();
|
|
4
|
+
export class ProofRunner extends ProofRunnerBase {
|
|
5
|
+
}
|
|
6
|
+
export const proofRunnerLayer = Layer.succeed(ProofRunner, {
|
|
7
|
+
executeProofEffect,
|
|
8
|
+
driveProofEffect,
|
|
9
|
+
executeProof,
|
|
10
|
+
driveProof,
|
|
11
|
+
});
|
|
12
|
+
export const withProofRunner = (use) => Effect.runPromise(ProofRunner.pipe(Effect.flatMap((runner) => Effect.promise(() => use(runner))), Effect.provide(proofRunnerLayer)));
|
|
13
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AAC/C,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAUzF,MAAM,eAAe,GACnB,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC,EAA+B,CAAA;AAExE,MAAM,OAAO,WAAY,SAAQ,eAAe;CAAG;AAEnD,MAAM,CAAC,MAAM,gBAAgB,GAA6B,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE;IACnF,kBAAkB;IAClB,gBAAgB;IAChB,YAAY;IACZ,UAAU;CACX,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG,CAAI,GAA2C,EAAc,EAAE,CAC5F,MAAM,CAAC,UAAU,CACf,WAAW,CAAC,IAAI,CACd,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAC7D,MAAM,CAAC,OAAO,CAAC,gBAAgB,CAAC,CACjC,CACF,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playfast/reform-proof",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Headless testing toolkit for reform — drive a scene's compositions and assert on the props they compute, with no DOM, timers, or text matching.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"effect",
|
|
@@ -19,15 +19,26 @@
|
|
|
19
19
|
"directory": "packages/proof"
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
|
+
"dist",
|
|
22
23
|
"src",
|
|
23
24
|
"README.md"
|
|
24
25
|
],
|
|
25
26
|
"type": "module",
|
|
26
27
|
"sideEffects": false,
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
27
30
|
"exports": {
|
|
28
31
|
"./package.json": "./package.json",
|
|
29
|
-
".":
|
|
30
|
-
|
|
32
|
+
".": {
|
|
33
|
+
"playfast-src": "./src/index.ts",
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"default": "./dist/index.js"
|
|
36
|
+
},
|
|
37
|
+
"./*": {
|
|
38
|
+
"playfast-src": "./src/*.ts",
|
|
39
|
+
"types": "./dist/*.d.ts",
|
|
40
|
+
"default": "./dist/*.js"
|
|
41
|
+
}
|
|
31
42
|
},
|
|
32
43
|
"publishConfig": {
|
|
33
44
|
"access": "public"
|
|
@@ -35,12 +46,13 @@
|
|
|
35
46
|
"scripts": {
|
|
36
47
|
"clean": "rm -rf dist .tsbuildinfo",
|
|
37
48
|
"check": "tsc --noEmit",
|
|
38
|
-
"build": "tsc -p tsconfig.build.json",
|
|
49
|
+
"build": "rm -rf dist .tsbuildinfo && tsc -p tsconfig.build.json && bun ../../scripts/fix-esm-extensions.ts dist",
|
|
39
50
|
"test": "vitest run",
|
|
40
51
|
"test:watch": "vitest",
|
|
41
52
|
"coverage": "vitest run --coverage",
|
|
42
53
|
"lint": "oxlint src",
|
|
43
|
-
"lint:fix": "oxlint --fix src"
|
|
54
|
+
"lint:fix": "oxlint --fix src",
|
|
55
|
+
"prepack": "bun run build"
|
|
44
56
|
},
|
|
45
57
|
"peerDependencies": {
|
|
46
58
|
"@playfast/reform": "*",
|
package/src/engine.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Cause, Effect, Option, Record as Rec, Ref } from 'effect'
|
|
1
|
+
import { Array as Arr, Cause, Effect, Option, Record as Rec, Ref } from 'effect'
|
|
2
2
|
import { yieldWrapGet } from 'effect/Utils'
|
|
3
3
|
import {
|
|
4
4
|
type CapturedScene,
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from '@playfast/reform'
|
|
10
10
|
import { type AnyComposition, type AnyScene } from '@playfast/reform/internal'
|
|
11
11
|
import {
|
|
12
|
+
type DispatchedAction,
|
|
12
13
|
invalidCompositionProps,
|
|
13
14
|
type HostRuntime,
|
|
14
15
|
makeSink,
|
|
@@ -26,14 +27,10 @@ import type { ProductComposition } from './product'
|
|
|
26
27
|
import type { Proof } from './index'
|
|
27
28
|
|
|
28
29
|
export { makeSink } from './engineSink'
|
|
29
|
-
export type { Sink } from './engineSink'
|
|
30
|
+
export type { DispatchedAction, Sink } from './engineSink'
|
|
30
31
|
export { proofLayer } from './engineTree'
|
|
31
32
|
export { makeFacade, makeProofFacade, makeRuntimeHandleFacade } from './engineMounted'
|
|
32
|
-
export type {
|
|
33
|
-
MountedFacade,
|
|
34
|
-
MountedSlotFacade,
|
|
35
|
-
MountedSlotFacadesOf,
|
|
36
|
-
} from './engineSink'
|
|
33
|
+
export type { MountedFacade, MountedSlotFacade, MountedSlotFacadesOf } from './engineSink'
|
|
37
34
|
export { makeRuntimeTreeFacade } from './engineTreeFacade'
|
|
38
35
|
export type { RuntimeTreeFacade } from './engineTreeTypes'
|
|
39
36
|
|
|
@@ -63,7 +60,7 @@ const withProofRuntimeTree = <
|
|
|
63
60
|
>(
|
|
64
61
|
scene: CapturedScene<C, S, Services, P, N, Identity>,
|
|
65
62
|
sink: Sink,
|
|
66
|
-
dispatched: Set<
|
|
63
|
+
dispatched: Set<DispatchedAction>,
|
|
67
64
|
use: (runtime: HostRuntime, tree: RuntimeTreeFacade<C>) => Effect.Effect<A, E, never>,
|
|
68
65
|
): Effect.Effect<A, E, never> =>
|
|
69
66
|
Effect.acquireUseRelease(
|
|
@@ -83,11 +80,19 @@ const withProofRuntimeTree = <
|
|
|
83
80
|
|
|
84
81
|
const missingCoverage = (
|
|
85
82
|
proof: Pick<Proof, 'requirement'>,
|
|
86
|
-
dispatched: Set<
|
|
83
|
+
dispatched: Set<DispatchedAction>,
|
|
87
84
|
): ReadonlyArray<string> =>
|
|
88
85
|
Option.match(proof.requirement.manifest.events, {
|
|
89
86
|
onNone: () => [],
|
|
90
|
-
onSome: (declared) =>
|
|
87
|
+
onSome: (declared) => {
|
|
88
|
+
const owner = compositionIdentity(proof.requirement.manifest.composition)
|
|
89
|
+
const covered = new Set(
|
|
90
|
+
Arr.filterMap(Arr.fromIterable(dispatched), (action) =>
|
|
91
|
+
Object.is(action.identity, owner) ? Option.some(action.event) : Option.none(),
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
return declared.map(String).filter((event) => !covered.has(event))
|
|
95
|
+
},
|
|
91
96
|
})
|
|
92
97
|
|
|
93
98
|
interface ProofPlacement {
|
|
@@ -144,7 +149,7 @@ const executeProofWithSink = Effect.fn('executeProofWithSink')(function* <
|
|
|
144
149
|
scene: CapturedScene<C, S, Services, P, N, Identity>,
|
|
145
150
|
sink: Sink,
|
|
146
151
|
): Effect.fn.Return<ProofResult, never, never> {
|
|
147
|
-
const dispatched = new Set<
|
|
152
|
+
const dispatched = new Set<DispatchedAction>()
|
|
148
153
|
const statement = proof.requirement.manifest.statement
|
|
149
154
|
const runGenerator = Effect.fn('runProofGenerator')(function* (
|
|
150
155
|
generator: ReturnType<typeof proof.body>,
|
|
@@ -218,7 +223,7 @@ const driveProofWithSink = Effect.fn('driveProofWithSink')(function* <
|
|
|
218
223
|
const frames = yield* Ref.make<ReadonlyArray<StepFrame>>([])
|
|
219
224
|
const statement = proof.requirement.manifest.statement
|
|
220
225
|
// Driving tracks dispatches for the facade without enforcing coverage.
|
|
221
|
-
const dispatched = new Set<
|
|
226
|
+
const dispatched = new Set<DispatchedAction>()
|
|
222
227
|
const record = (index: number): Effect.Effect<void> =>
|
|
223
228
|
Ref.update(frames, (current) => [...current, { index, captures: [...sink.captures] }])
|
|
224
229
|
const pump = Effect.fn('pump')(function* (
|
|
@@ -282,4 +287,3 @@ export const driveProofEffect = (proof: Proof): Effect.Effect<DriveResult, never
|
|
|
282
287
|
|
|
283
288
|
export const driveProof = (proof: Proof): Promise<DriveResult> =>
|
|
284
289
|
Effect.runPromise(driveProofEffect(proof))
|
|
285
|
-
|
package/src/engineMounted.ts
CHANGED
|
@@ -10,7 +10,9 @@ import {
|
|
|
10
10
|
import { type AnyComposition, CaptureSink } from '@playfast/reform/internal'
|
|
11
11
|
import type { Facade } from './facade'
|
|
12
12
|
import {
|
|
13
|
+
compositionIdentityOf,
|
|
13
14
|
directSettle,
|
|
15
|
+
type DispatchedAction,
|
|
14
16
|
expectMatchingProps,
|
|
15
17
|
keyed,
|
|
16
18
|
type MountedFacade,
|
|
@@ -32,11 +34,7 @@ import {
|
|
|
32
34
|
slotCapturesFor,
|
|
33
35
|
slotsOf,
|
|
34
36
|
} from './engineTree'
|
|
35
|
-
import {
|
|
36
|
-
collectRuntimeBindings,
|
|
37
|
-
renderTreeRuntime,
|
|
38
|
-
settleTreeRuntime,
|
|
39
|
-
} from './engineRender'
|
|
37
|
+
import { collectRuntimeBindings, renderTreeRuntime, settleTreeRuntime } from './engineRender'
|
|
40
38
|
|
|
41
39
|
export function makeFacade<
|
|
42
40
|
C extends UiContract,
|
|
@@ -49,7 +47,7 @@ export function makeFacade<
|
|
|
49
47
|
runtime: ManagedRuntime.ManagedRuntime<Services | CaptureSink, never>,
|
|
50
48
|
scene: CapturedScene<C, S, Services, P, N, Identity>,
|
|
51
49
|
sink: Sink,
|
|
52
|
-
dispatched: Set<
|
|
50
|
+
dispatched: Set<DispatchedAction>,
|
|
53
51
|
): {
|
|
54
52
|
readonly facade: Facade<C, never>
|
|
55
53
|
readonly settle: Effect.Effect<void, never, never>
|
|
@@ -74,7 +72,7 @@ function makeMountedFacade<
|
|
|
74
72
|
runtime: RenderRuntime,
|
|
75
73
|
root: CompositionClass<P, C, S, N, Identity>,
|
|
76
74
|
sink: Sink,
|
|
77
|
-
dispatched: Set<
|
|
75
|
+
dispatched: Set<DispatchedAction>,
|
|
78
76
|
settleBoundary: SettleBoundary,
|
|
79
77
|
): {
|
|
80
78
|
readonly facade: MountedFacade<C>
|
|
@@ -84,14 +82,14 @@ function makeMountedFacade(
|
|
|
84
82
|
runtime: RenderRuntime,
|
|
85
83
|
root: AnyComposition,
|
|
86
84
|
sink: Sink,
|
|
87
|
-
dispatched: Set<
|
|
85
|
+
dispatched: Set<DispatchedAction>,
|
|
88
86
|
settleBoundary: SettleBoundary,
|
|
89
87
|
): MountedFacadeHandleErasure
|
|
90
88
|
function makeMountedFacade(
|
|
91
89
|
runtime: RenderRuntime,
|
|
92
90
|
root: AnyComposition,
|
|
93
91
|
sink: Sink,
|
|
94
|
-
dispatched: Set<
|
|
92
|
+
dispatched: Set<DispatchedAction>,
|
|
95
93
|
settleBoundary: SettleBoundary,
|
|
96
94
|
): MountedFacadeHandleErasure {
|
|
97
95
|
const bindings = collectRuntimeBindings(root, runtime)
|
|
@@ -146,7 +144,7 @@ function makeMountedFacade(
|
|
|
146
144
|
Effect.flatMap(() => triggerOf(ref, event)),
|
|
147
145
|
Effect.flatMap((trigger) =>
|
|
148
146
|
Effect.sync(() => {
|
|
149
|
-
dispatched.add(event)
|
|
147
|
+
dispatched.add({ identity: compositionIdentityOf(comp), event })
|
|
150
148
|
trigger(payload)
|
|
151
149
|
}),
|
|
152
150
|
),
|
|
@@ -244,7 +242,7 @@ export const makeProofFacade = <
|
|
|
244
242
|
runtime: ManagedRuntime.ManagedRuntime<Services | CaptureSink, never>,
|
|
245
243
|
scene: CapturedScene<C, S, Services, P, N, Identity>,
|
|
246
244
|
sink: Sink,
|
|
247
|
-
dispatched: Set<
|
|
245
|
+
dispatched: Set<DispatchedAction>,
|
|
248
246
|
): {
|
|
249
247
|
readonly facade: Facade<C, never>
|
|
250
248
|
readonly settle: Effect.Effect<void, never, never>
|
|
@@ -264,7 +262,7 @@ export const makeRuntimeHandleFacade = <
|
|
|
264
262
|
runtime: RuntimeHandle<Services>,
|
|
265
263
|
root: RuntimeRootComposition<RootIdentifier, P, C, S, N, Identity>,
|
|
266
264
|
sink: Sink,
|
|
267
|
-
dispatched: Set<
|
|
265
|
+
dispatched: Set<DispatchedAction>,
|
|
268
266
|
): {
|
|
269
267
|
readonly facade: MountedFacade<C>
|
|
270
268
|
readonly settle: Effect.Effect<void, never, never>
|
package/src/engineSink.ts
CHANGED
|
@@ -91,7 +91,9 @@ export const settleDrain: Effect.Effect<void> = Effect.yieldNow().pipe(Effect.re
|
|
|
91
91
|
// settle loop would never make progress. Pinning it to a real clock is what lets the
|
|
92
92
|
// two clocks coexist: the harness keeps advancing while the product's timers hold
|
|
93
93
|
// still until `app.advance(...)` moves them.
|
|
94
|
-
export const settleStep: Effect.Effect<void> = Effect.sleep(SETTLE_STEP).pipe(
|
|
94
|
+
export const settleStep: Effect.Effect<void> = Effect.sleep(SETTLE_STEP).pipe(
|
|
95
|
+
Effect.withClock(Clock.make()),
|
|
96
|
+
)
|
|
95
97
|
|
|
96
98
|
export const keyed = <V>(get: (key: string) => V): Record<string, V> => {
|
|
97
99
|
const target: Record<string, V> = Object.create(null)
|
|
@@ -101,6 +103,18 @@ export const keyed = <V>(get: (key: string) => V): Record<string, V> => {
|
|
|
101
103
|
export const messageOf = (error: unknown): string =>
|
|
102
104
|
error instanceof Error ? error.message : String(error)
|
|
103
105
|
|
|
106
|
+
// An action credited to the composition that actually raised it. A bare event name
|
|
107
|
+
// is not enough: two contracts in one tree can expose the same action name, and a
|
|
108
|
+
// requirement's coverage must be answered by *its* composition's action, not by a
|
|
109
|
+
// child that happens to spell its own action the same way.
|
|
110
|
+
export interface DispatchedAction {
|
|
111
|
+
readonly identity: symbol
|
|
112
|
+
readonly event: string
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const compositionIdentityOf = (composition: AnyComposition): symbol =>
|
|
116
|
+
composition.capture<symbol>((exact) => exact.identity)
|
|
117
|
+
|
|
104
118
|
export const uiNameOf = (comp: AnyComposition): string => comp.manifest.ui.manifest.name
|
|
105
119
|
|
|
106
120
|
// UiContract erases event values to never, so this boundary needs no assertion.
|
package/src/engineTreeFacade.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { Array as Arr, type Duration, Effect, Option, TestClock } from 'effect'
|
|
1
|
+
import { Array as Arr, type Duration, Effect, MutableRef, Option, TestClock } from 'effect'
|
|
2
2
|
import { AssertionFailed, NoTestClock, UnknownAction, UnknownSlot } from './errors'
|
|
3
3
|
import { matchProps } from './assert'
|
|
4
4
|
import { type RuntimeHandle, type UiContract } from '@playfast/reform'
|
|
5
5
|
import type { AnyComposition } from '@playfast/reform/internal'
|
|
6
6
|
import {
|
|
7
|
+
compositionIdentityOf,
|
|
8
|
+
type DispatchedAction,
|
|
7
9
|
type HostRuntime,
|
|
8
10
|
keyed,
|
|
9
11
|
type MountedFacadeErasure,
|
|
@@ -28,14 +30,14 @@ export function makeRuntimeTreeFacade<
|
|
|
28
30
|
root: RuntimeRootComposition<RootIdentifier, P, C, S, N, Identity>,
|
|
29
31
|
rootProps: P,
|
|
30
32
|
sink: Sink,
|
|
31
|
-
dispatched: Set<
|
|
33
|
+
dispatched: Set<DispatchedAction>,
|
|
32
34
|
): RuntimeTreeFacade<C>
|
|
33
35
|
export function makeRuntimeTreeFacade(
|
|
34
36
|
runtime: HostRuntime,
|
|
35
37
|
root: AnyComposition,
|
|
36
38
|
rootProps: unknown,
|
|
37
39
|
sink: Sink,
|
|
38
|
-
dispatched: Set<
|
|
40
|
+
dispatched: Set<DispatchedAction>,
|
|
39
41
|
): RuntimeTreeFacadeErasure {
|
|
40
42
|
const driver = makeTreeDriver(runtime, root, rootProps, sink)
|
|
41
43
|
const currentRoot = (): TreeNode => driver.render().root
|
|
@@ -49,10 +51,12 @@ export function makeRuntimeTreeFacade(
|
|
|
49
51
|
})
|
|
50
52
|
|
|
51
53
|
// `peek` is the non-dying half of `resolve`: None once the node has left the
|
|
52
|
-
// tree, so an action that removed its own node can still report a result.
|
|
54
|
+
// tree, so an action that removed its own node can still report a result. It is
|
|
55
|
+
// given the node as it stood before the dispatch, because "is this still the node
|
|
56
|
+
// I acted on" cannot be answered from a bare index.
|
|
53
57
|
const nodeFacade = (
|
|
54
58
|
resolve: () => TreeNode,
|
|
55
|
-
peek: () => Option.Option<TreeNode>,
|
|
59
|
+
peek: (before: TreeNode) => Option.Option<TreeNode>,
|
|
56
60
|
): MountedFacadeErasure => ({
|
|
57
61
|
props: Effect.sync(() => resolve().props),
|
|
58
62
|
expectProps: (partial) =>
|
|
@@ -81,12 +85,12 @@ export function makeRuntimeTreeFacade(
|
|
|
81
85
|
}).message,
|
|
82
86
|
)
|
|
83
87
|
}
|
|
84
|
-
dispatched.add(event)
|
|
88
|
+
dispatched.add({ identity: compositionIdentityOf(node.comp), event })
|
|
85
89
|
trigger(payload)
|
|
86
90
|
yield* driver.settle
|
|
87
91
|
// A self-removing action leaves nothing to re-read; report what the
|
|
88
92
|
// node last rendered instead of dying on the vanished child.
|
|
89
|
-
return Option.match(peek(), {
|
|
93
|
+
return Option.match(peek(node), {
|
|
90
94
|
onNone: () => node.props,
|
|
91
95
|
onSome: (settled) => settled.props,
|
|
92
96
|
})
|
|
@@ -113,15 +117,46 @@ export function makeRuntimeTreeFacade(
|
|
|
113
117
|
}
|
|
114
118
|
return resolved
|
|
115
119
|
}
|
|
116
|
-
const peekAt = (index: number): Option.Option<TreeNode> =>
|
|
117
|
-
Option.fromNullable(children()[index])
|
|
118
120
|
const peekByKey = (key: string): Option.Option<TreeNode> =>
|
|
119
121
|
Arr.findFirst(children(), (candidate) => Option.contains(candidate.key, key))
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
|
|
123
|
+
// Which node an index still refers to, after the slot may have changed under it.
|
|
124
|
+
// Three outcomes, in order: the same node (it survived, wherever it moved to); a
|
|
125
|
+
// node that took its place (a slot of unchanged length swapped one child for
|
|
126
|
+
// another — a placeholder giving way to the real thing); or nothing, because the
|
|
127
|
+
// slot shrank and whatever sits at the index now is a *different row* that slid
|
|
128
|
+
// down. Reporting that neighbour's props is the bug this ordering exists to stop.
|
|
129
|
+
interface PeekAt {
|
|
130
|
+
readonly index: number
|
|
131
|
+
readonly before: TreeNode
|
|
132
|
+
readonly siblingsBefore: number
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const peekAt = ({ index, before, siblingsBefore }: PeekAt): Option.Option<TreeNode> => {
|
|
136
|
+
const now = children()
|
|
137
|
+
const survived = Arr.findFirst(now, (candidate) =>
|
|
138
|
+
Option.match(before.key, {
|
|
139
|
+
onNone: () => candidate.path === before.path,
|
|
140
|
+
onSome: (key) => Option.contains(candidate.key, key),
|
|
141
|
+
}),
|
|
124
142
|
)
|
|
143
|
+
if (Option.isSome(survived)) {
|
|
144
|
+
return survived
|
|
145
|
+
}
|
|
146
|
+
return now.length === siblingsBefore ? Option.fromNullable(now[index]) : Option.none()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const childAt = (index: number): MountedFacadeErasure => {
|
|
150
|
+
const siblings = MutableRef.make(0)
|
|
151
|
+
return nodeFacade(
|
|
152
|
+
() => {
|
|
153
|
+
const now = children()
|
|
154
|
+
MutableRef.set(siblings, now.length)
|
|
155
|
+
return now[index] ?? missingChild(index)
|
|
156
|
+
},
|
|
157
|
+
(before) => peekAt({ index, before, siblingsBefore: MutableRef.get(siblings) }),
|
|
158
|
+
)
|
|
159
|
+
}
|
|
125
160
|
const childByKey = (key: string): MountedFacadeErasure =>
|
|
126
161
|
nodeFacade(
|
|
127
162
|
() => Option.getOrElse(peekByKey(key), () => missingKey(key)),
|