foldkit 0.102.0 → 0.103.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/managedResource/public.d.ts +2 -2
- package/dist/managedResource/public.d.ts.map +1 -1
- package/dist/managedResource/public.js +1 -1
- package/dist/runtime/managedResource.d.ts +104 -43
- package/dist/runtime/managedResource.d.ts.map +1 -1
- package/dist/runtime/managedResource.js +68 -30
- package/dist/runtime/runtime.d.ts +7 -2
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +36 -26
- package/dist/runtime/subscription.js +1 -1
- package/dist/test/apps/crashOnRender.d.ts +64 -0
- package/dist/test/apps/crashOnRender.d.ts.map +1 -0
- package/dist/test/apps/crashOnRender.js +72 -0
- package/dist/vdom.d.ts +3 -2
- package/dist/vdom.d.ts.map +1 -1
- package/dist/vdom.js +44 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { tag, ResourceNotAvailable } from './index.js';
|
|
2
2
|
export type { ManagedResource, ManagedResourceService, Value, ServiceOf, } from './index.js';
|
|
3
|
-
export {
|
|
4
|
-
export type {
|
|
3
|
+
export { make, lift, aggregate } from '../runtime/managedResource.js';
|
|
4
|
+
export type { Entry, ManagedResources, ManagedResourceConfig, ServicesOf, } from '../runtime/managedResource.js';
|
|
5
5
|
//# sourceMappingURL=public.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/managedResource/public.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAEtD,YAAY,EACV,eAAe,EACf,sBAAsB,EACtB,KAAK,EACL,SAAS,GACV,MAAM,YAAY,CAAA;AAEnB,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/managedResource/public.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAEtD,YAAY,EACV,eAAe,EACf,sBAAsB,EACtB,KAAK,EACL,SAAS,GACV,MAAM,YAAY,CAAA;AAEnB,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAA;AAErE,YAAY,EACV,KAAK,EACL,gBAAgB,EAChB,qBAAqB,EACrB,UAAU,GACX,MAAM,+BAA+B,CAAA"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { tag, ResourceNotAvailable } from './index.js';
|
|
2
|
-
export {
|
|
2
|
+
export { make, lift, aggregate } from '../runtime/managedResource.js';
|
|
@@ -1,33 +1,88 @@
|
|
|
1
|
-
import { type Effect,
|
|
1
|
+
import { type Effect, Option, type Schema } from 'effect';
|
|
2
2
|
import type { ManagedResource } from '../managedResource/index.js';
|
|
3
|
-
/** Internal configuration for a single
|
|
3
|
+
/** Internal configuration for a single Managed Resource, used by the runtime. */
|
|
4
4
|
export type ManagedResourceConfig<Model, Message> = {
|
|
5
5
|
readonly schema: Schema.Schema<any>;
|
|
6
6
|
readonly resource: ManagedResource<any>;
|
|
7
|
-
readonly modelToMaybeRequirements: (model: Model) =>
|
|
8
|
-
readonly acquire: (params:
|
|
9
|
-
readonly release: (value:
|
|
10
|
-
readonly onAcquired: (value:
|
|
7
|
+
readonly modelToMaybeRequirements: (model: Model) => any;
|
|
8
|
+
readonly acquire: (params: any) => Effect.Effect<any, unknown>;
|
|
9
|
+
readonly release: (value: any) => Effect.Effect<void>;
|
|
10
|
+
readonly onAcquired: (value: any) => Message;
|
|
11
11
|
readonly onReleased: () => Message;
|
|
12
12
|
readonly onAcquireError: (error: unknown) => Message;
|
|
13
13
|
};
|
|
14
|
-
|
|
15
|
-
/** A record of named managed resource configurations, keyed by dependency field name. */
|
|
14
|
+
/** A record of named Managed Resource configurations, keyed by resource name. */
|
|
16
15
|
export type ManagedResources<Model, Message, Services = never> = Record<string, ManagedResourceConfig<Model, Message>> & {
|
|
17
|
-
readonly
|
|
16
|
+
readonly __managedResourceServices?: Services;
|
|
18
17
|
};
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
type EntryBrand = {
|
|
19
|
+
readonly __managedResourceEntry: never;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The requirements value the runtime hands to `acquire`. When the requirements
|
|
23
|
+
* schema is wrapped in `S.Option`, the runtime unwraps the `Some` before
|
|
24
|
+
* calling `acquire`, so the parameter is the inner type.
|
|
25
|
+
*/
|
|
26
|
+
type AcquireParams<Requirements> = Requirements extends Option.Option<infer Params> ? Params : Requirements;
|
|
27
|
+
/**
|
|
28
|
+
* A single Managed Resource entry produced by `ManagedResource.make`,
|
|
29
|
+
* `ManagedResource.lift`, or `ManagedResource.aggregate`. The brand field is
|
|
30
|
+
* `never`, so application code cannot manually construct one: it must go
|
|
31
|
+
* through a constructor.
|
|
32
|
+
*
|
|
33
|
+
* The `Service` parameter carries the resource tag's identity so `make`,
|
|
34
|
+
* `lift`, and `aggregate` can union the services a record requires. Read the
|
|
35
|
+
* union off a finished record with `ManagedResource.ServicesOf`.
|
|
36
|
+
*/
|
|
37
|
+
export type Entry<Model, Message, Requirements, Service = unknown> = {
|
|
38
|
+
readonly schema: Schema.Schema<Requirements>;
|
|
39
|
+
readonly resource: ManagedResource<any, Service>;
|
|
40
|
+
readonly modelToMaybeRequirements: (model: Model) => Requirements;
|
|
41
|
+
readonly acquire: (params: AcquireParams<Requirements>) => Effect.Effect<any, unknown>;
|
|
42
|
+
readonly release: (value: any) => Effect.Effect<void>;
|
|
43
|
+
readonly onAcquired: (value: any) => Message;
|
|
44
|
+
readonly onReleased: () => Message;
|
|
45
|
+
readonly onAcquireError: (error: unknown) => Message;
|
|
46
|
+
} & EntryBrand;
|
|
47
|
+
/** Type-level utility to extract the service union from a Managed Resources record. */
|
|
48
|
+
export type ServicesOf<Resources> = {
|
|
49
|
+
[Key in keyof Resources]: Resources[Key] extends {
|
|
50
|
+
readonly resource: ManagedResource<any, infer Service>;
|
|
51
|
+
} ? Service : never;
|
|
52
|
+
}[keyof Resources];
|
|
53
|
+
/**
|
|
54
|
+
* Builds a single Managed Resource entry from a requirements schema and a
|
|
55
|
+
* config. Reading the schema as a positional argument (rather than a property
|
|
56
|
+
* on the config literal) lets TypeScript fully resolve the requirements type
|
|
57
|
+
* before contextually typing `modelToMaybeRequirements` and `acquire`, so
|
|
58
|
+
* destructuring patterns are inferred correctly even when the schema uses
|
|
59
|
+
* transforms like `S.Option`.
|
|
60
|
+
*/
|
|
61
|
+
export type EntryBuilder<Model, Message> = <RequirementsSchema extends Schema.Schema<any>, Service>(schema: RequirementsSchema, config: {
|
|
62
|
+
readonly resource: ManagedResource<any, Service>;
|
|
63
|
+
readonly modelToMaybeRequirements: (model: Model) => Schema.Schema.Type<RequirementsSchema>;
|
|
64
|
+
readonly acquire: (params: AcquireParams<Schema.Schema.Type<RequirementsSchema>>) => Effect.Effect<any, unknown>;
|
|
65
|
+
readonly release: (value: any) => Effect.Effect<void>;
|
|
66
|
+
readonly onAcquired: (value: any) => Message;
|
|
67
|
+
readonly onReleased: () => Message;
|
|
68
|
+
readonly onAcquireError: (error: unknown) => Message;
|
|
69
|
+
}) => Entry<Model, Message, Schema.Schema.Type<RequirementsSchema>, Service>;
|
|
21
70
|
/**
|
|
22
|
-
*
|
|
71
|
+
* Declares a Managed Resources record. The Model and Message generics are
|
|
72
|
+
* provided up front; the entries record follows, built from calls to the
|
|
73
|
+
* `entry` builder passed into the inner function.
|
|
23
74
|
*
|
|
24
75
|
* Use this when a resource is expensive or stateful and should only exist while
|
|
25
|
-
* the model is in a particular state
|
|
76
|
+
* the model is in a particular state: a camera stream during a video call, a
|
|
26
77
|
* WebSocket connection while on a chat page, or a Web Worker pool during a
|
|
27
78
|
* computation. For resources that live for the entire application lifetime, use
|
|
28
79
|
* the static `resources` config instead.
|
|
29
80
|
*
|
|
30
|
-
*
|
|
81
|
+
* Reach for `ManagedResource.aggregate` to combine multiple records, and
|
|
82
|
+
* `ManagedResource.lift` to translate a child Submodel's record into a parent
|
|
83
|
+
* context.
|
|
84
|
+
*
|
|
85
|
+
* **Lifecycle** — The runtime watches each entry's `modelToMaybeRequirements`
|
|
31
86
|
* after every model update, structurally comparing the result against the
|
|
32
87
|
* previous value:
|
|
33
88
|
*
|
|
@@ -39,8 +94,8 @@ export type ManagedResourceServicesOf<MR> = MR extends ManagedResources<any, any
|
|
|
39
94
|
* dispatches `onReleased()`. No re-acquisition occurs.
|
|
40
95
|
*
|
|
41
96
|
* If `acquire` fails, `onAcquireError` is dispatched and the resource daemon
|
|
42
|
-
* continues watching for the next
|
|
43
|
-
* crash the application.
|
|
97
|
+
* continues watching for the next requirements change: a failed acquisition
|
|
98
|
+
* does not crash the application.
|
|
44
99
|
*
|
|
45
100
|
* **Config fields:**
|
|
46
101
|
*
|
|
@@ -49,13 +104,12 @@ export type ManagedResourceServicesOf<MR> = MR extends ManagedResources<any, any
|
|
|
49
104
|
* - `modelToMaybeRequirements` — Extracts requirements from the model.
|
|
50
105
|
* `Option.none()` means "release", `Option.some(params)` means
|
|
51
106
|
* "acquire/re-acquire if params changed". For resources with no
|
|
52
|
-
* parameters, use `S.Option(S.Null)` and return `Option.some(null)
|
|
53
|
-
* not `S.Struct({})`, which has no fields for equivalence comparison.
|
|
107
|
+
* parameters, use `S.Option(S.Null)` and return `Option.some(null)`.
|
|
54
108
|
* - `acquire` — Creates the resource from the unwrapped params. The returned
|
|
55
|
-
* Effect should fail when acquisition fails
|
|
109
|
+
* Effect should fail when acquisition fails: errors in the error channel
|
|
56
110
|
* flow to `onAcquireError` as a message instead of crashing the runtime.
|
|
57
111
|
* - `release` — Tears down the resource. Errors thrown here are silently
|
|
58
|
-
* swallowed
|
|
112
|
+
* swallowed: release must not block cleanup.
|
|
59
113
|
* - `onAcquired` — Message dispatched when `acquire` succeeds.
|
|
60
114
|
* - `onAcquireError` — Message dispatched when `acquire` fails.
|
|
61
115
|
* - `onReleased` — Message dispatched after `release` completes.
|
|
@@ -64,14 +118,8 @@ export type ManagedResourceServicesOf<MR> = MR extends ManagedResources<any, any
|
|
|
64
118
|
* ```ts
|
|
65
119
|
* const CameraStream = ManagedResource.tag<MediaStream>()('CameraStream')
|
|
66
120
|
*
|
|
67
|
-
* const
|
|
68
|
-
* camera: S.Option(S.Struct({ facingMode: S.String })),
|
|
69
|
-
* })
|
|
70
|
-
*
|
|
71
|
-
* const managedResources = ManagedResource.makeManagedResources(
|
|
72
|
-
* ManagedResourceDeps,
|
|
73
|
-
* )<Model, Message>({
|
|
74
|
-
* camera: {
|
|
121
|
+
* const managedResources = ManagedResource.make<Model, Message>()(entry => ({
|
|
122
|
+
* camera: entry(S.Option(S.Struct({ facingMode: S.String })), {
|
|
75
123
|
* resource: CameraStream,
|
|
76
124
|
* modelToMaybeRequirements: model =>
|
|
77
125
|
* pipe(
|
|
@@ -91,24 +139,37 @@ export type ManagedResourceServicesOf<MR> = MR extends ManagedResources<any, any
|
|
|
91
139
|
* onAcquired: () => AcquiredCamera(),
|
|
92
140
|
* onAcquireError: error => FailedAcquireCamera({ error: String(error) }),
|
|
93
141
|
* onReleased: () => ReleasedCamera(),
|
|
94
|
-
* },
|
|
95
|
-
* })
|
|
142
|
+
* }),
|
|
143
|
+
* }))
|
|
96
144
|
* ```
|
|
97
145
|
*
|
|
98
|
-
* @param ManagedResourceDeps - An Effect Schema struct where each field's type
|
|
99
|
-
* drives the requirements for one managed resource. Wrap in `S.Option(...)` for
|
|
100
|
-
* resources that can be released (most cases).
|
|
101
|
-
*
|
|
102
146
|
* @see {@link ManagedResource.tag} for creating the resource identity.
|
|
103
147
|
*/
|
|
104
|
-
export declare const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
148
|
+
export declare const make: <Model, Message>() => <Entries extends Record<string, Entry<Model, Message, any, any>>>(build: (entry: EntryBuilder<Model, Message>) => Entries) => Entries;
|
|
149
|
+
type ChildModelOf<Resources> = Resources[keyof Resources] extends Entry<infer ChildModel, any, any, any> ? ChildModel : never;
|
|
150
|
+
type ChildMessageOf<Resources> = Resources[keyof Resources] extends Entry<any, infer ChildMessage, any, any> ? ChildMessage : never;
|
|
151
|
+
/**
|
|
152
|
+
* Lifts a record of child Managed Resources into a parent's Model and Message
|
|
153
|
+
* context, applying a Model accessor and a Message wrapper uniformly to every
|
|
154
|
+
* entry. Per-entry requirements schemas and resource services are preserved.
|
|
155
|
+
*
|
|
156
|
+
* Unlike `Subscription.lift`, `toChildModel` returns an `Option`: a managed
|
|
157
|
+
* resource already speaks in `Option` (`modelToMaybeRequirements` returns
|
|
158
|
+
* `Option.none()` to release), and a child Submodel that owns a managed
|
|
159
|
+
* resource is itself something that mounts and unmounts. A missing child is
|
|
160
|
+
* just another `None` and flows through the same acquire/release channel, so
|
|
161
|
+
* each lifted entry's requirements must be `S.Option`-wrapped.
|
|
162
|
+
*/
|
|
163
|
+
export declare const lift: <Resources extends Record<string, Entry<any, any, Option.Option<any>, any>>>(resources: Resources) => <ParentModel, ParentMessage>(config: {
|
|
164
|
+
readonly toChildModel: (parentModel: ParentModel) => Option.Option<ChildModelOf<Resources>>;
|
|
165
|
+
readonly toParentMessage: (message: ChildMessageOf<Resources>) => ParentMessage;
|
|
166
|
+
}) => { readonly [Key in keyof Resources]: Resources[Key] extends Entry<any, any, infer Requirements, infer Service> ? Entry<ParentModel, ParentMessage, Requirements, Service> : never; };
|
|
167
|
+
type MergeRecords<Records extends ReadonlyArray<unknown>> = Records extends readonly [infer Head, ...infer Rest] ? Head & (Rest extends ReadonlyArray<unknown> ? MergeRecords<Rest> : {}) : {};
|
|
168
|
+
/**
|
|
169
|
+
* Combines multiple Managed Resources records into one. Throws on duplicate
|
|
170
|
+
* keys so a misconfigured aggregate fails loudly at startup rather than
|
|
171
|
+
* silently overriding.
|
|
172
|
+
*/
|
|
173
|
+
export declare const aggregate: <Model, Message>() => <Records extends ReadonlyArray<Record<string, Entry<Model, Message, any, any>>>>(...records: Records) => MergeRecords<Records>;
|
|
113
174
|
export {};
|
|
114
175
|
//# sourceMappingURL=managedResource.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"managedResource.d.ts","sourceRoot":"","sources":["../../src/runtime/managedResource.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"managedResource.d.ts","sourceRoot":"","sources":["../../src/runtime/managedResource.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
import { Record } from 'effect';
|
|
1
|
+
import { Option, Record } from 'effect';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Declares a Managed Resources record. The Model and Message generics are
|
|
4
|
+
* provided up front; the entries record follows, built from calls to the
|
|
5
|
+
* `entry` builder passed into the inner function.
|
|
4
6
|
*
|
|
5
7
|
* Use this when a resource is expensive or stateful and should only exist while
|
|
6
|
-
* the model is in a particular state
|
|
8
|
+
* the model is in a particular state: a camera stream during a video call, a
|
|
7
9
|
* WebSocket connection while on a chat page, or a Web Worker pool during a
|
|
8
10
|
* computation. For resources that live for the entire application lifetime, use
|
|
9
11
|
* the static `resources` config instead.
|
|
10
12
|
*
|
|
11
|
-
*
|
|
13
|
+
* Reach for `ManagedResource.aggregate` to combine multiple records, and
|
|
14
|
+
* `ManagedResource.lift` to translate a child Submodel's record into a parent
|
|
15
|
+
* context.
|
|
16
|
+
*
|
|
17
|
+
* **Lifecycle** — The runtime watches each entry's `modelToMaybeRequirements`
|
|
12
18
|
* after every model update, structurally comparing the result against the
|
|
13
19
|
* previous value:
|
|
14
20
|
*
|
|
@@ -20,8 +26,8 @@ import { Record } from 'effect';
|
|
|
20
26
|
* dispatches `onReleased()`. No re-acquisition occurs.
|
|
21
27
|
*
|
|
22
28
|
* If `acquire` fails, `onAcquireError` is dispatched and the resource daemon
|
|
23
|
-
* continues watching for the next
|
|
24
|
-
* crash the application.
|
|
29
|
+
* continues watching for the next requirements change: a failed acquisition
|
|
30
|
+
* does not crash the application.
|
|
25
31
|
*
|
|
26
32
|
* **Config fields:**
|
|
27
33
|
*
|
|
@@ -30,13 +36,12 @@ import { Record } from 'effect';
|
|
|
30
36
|
* - `modelToMaybeRequirements` — Extracts requirements from the model.
|
|
31
37
|
* `Option.none()` means "release", `Option.some(params)` means
|
|
32
38
|
* "acquire/re-acquire if params changed". For resources with no
|
|
33
|
-
* parameters, use `S.Option(S.Null)` and return `Option.some(null)
|
|
34
|
-
* not `S.Struct({})`, which has no fields for equivalence comparison.
|
|
39
|
+
* parameters, use `S.Option(S.Null)` and return `Option.some(null)`.
|
|
35
40
|
* - `acquire` — Creates the resource from the unwrapped params. The returned
|
|
36
|
-
* Effect should fail when acquisition fails
|
|
41
|
+
* Effect should fail when acquisition fails: errors in the error channel
|
|
37
42
|
* flow to `onAcquireError` as a message instead of crashing the runtime.
|
|
38
43
|
* - `release` — Tears down the resource. Errors thrown here are silently
|
|
39
|
-
* swallowed
|
|
44
|
+
* swallowed: release must not block cleanup.
|
|
40
45
|
* - `onAcquired` — Message dispatched when `acquire` succeeds.
|
|
41
46
|
* - `onAcquireError` — Message dispatched when `acquire` fails.
|
|
42
47
|
* - `onReleased` — Message dispatched after `release` completes.
|
|
@@ -45,14 +50,8 @@ import { Record } from 'effect';
|
|
|
45
50
|
* ```ts
|
|
46
51
|
* const CameraStream = ManagedResource.tag<MediaStream>()('CameraStream')
|
|
47
52
|
*
|
|
48
|
-
* const
|
|
49
|
-
* camera: S.Option(S.Struct({ facingMode: S.String })),
|
|
50
|
-
* })
|
|
51
|
-
*
|
|
52
|
-
* const managedResources = ManagedResource.makeManagedResources(
|
|
53
|
-
* ManagedResourceDeps,
|
|
54
|
-
* )<Model, Message>({
|
|
55
|
-
* camera: {
|
|
53
|
+
* const managedResources = ManagedResource.make<Model, Message>()(entry => ({
|
|
54
|
+
* camera: entry(S.Option(S.Struct({ facingMode: S.String })), {
|
|
56
55
|
* resource: CameraStream,
|
|
57
56
|
* modelToMaybeRequirements: model =>
|
|
58
57
|
* pipe(
|
|
@@ -72,21 +71,60 @@ import { Record } from 'effect';
|
|
|
72
71
|
* onAcquired: () => AcquiredCamera(),
|
|
73
72
|
* onAcquireError: error => FailedAcquireCamera({ error: String(error) }),
|
|
74
73
|
* onReleased: () => ReleasedCamera(),
|
|
75
|
-
* },
|
|
76
|
-
* })
|
|
74
|
+
* }),
|
|
75
|
+
* }))
|
|
77
76
|
* ```
|
|
78
77
|
*
|
|
79
|
-
* @param ManagedResourceDeps - An Effect Schema struct where each field's type
|
|
80
|
-
* drives the requirements for one managed resource. Wrap in `S.Option(...)` for
|
|
81
|
-
* resources that can be released (most cases).
|
|
82
|
-
*
|
|
83
78
|
* @see {@link ManagedResource.tag} for creating the resource identity.
|
|
84
79
|
*/
|
|
85
|
-
export const
|
|
86
|
-
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
87
|
-
|
|
80
|
+
export const make = () => (build) => {
|
|
81
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
82
|
+
const entry = ((schema, config) => ({
|
|
83
|
+
schema,
|
|
84
|
+
...config,
|
|
85
|
+
}));
|
|
86
|
+
return build(entry);
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Lifts a record of child Managed Resources into a parent's Model and Message
|
|
90
|
+
* context, applying a Model accessor and a Message wrapper uniformly to every
|
|
91
|
+
* entry. Per-entry requirements schemas and resource services are preserved.
|
|
92
|
+
*
|
|
93
|
+
* Unlike `Subscription.lift`, `toChildModel` returns an `Option`: a managed
|
|
94
|
+
* resource already speaks in `Option` (`modelToMaybeRequirements` returns
|
|
95
|
+
* `Option.none()` to release), and a child Submodel that owns a managed
|
|
96
|
+
* resource is itself something that mounts and unmounts. A missing child is
|
|
97
|
+
* just another `None` and flows through the same acquire/release channel, so
|
|
98
|
+
* each lifted entry's requirements must be `S.Option`-wrapped.
|
|
99
|
+
*/
|
|
100
|
+
export const lift = (resources) => (config) =>
|
|
88
101
|
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
89
|
-
({
|
|
90
|
-
schema:
|
|
91
|
-
|
|
102
|
+
Record.map(resources, resource => ({
|
|
103
|
+
schema: resource.schema,
|
|
104
|
+
resource: resource.resource,
|
|
105
|
+
modelToMaybeRequirements: (parentModel) => Option.flatMap(config.toChildModel(parentModel), resource.modelToMaybeRequirements),
|
|
106
|
+
acquire: resource.acquire,
|
|
107
|
+
release: resource.release,
|
|
108
|
+
onAcquired: (value) => config.toParentMessage(resource.onAcquired(value)),
|
|
109
|
+
onReleased: () => config.toParentMessage(resource.onReleased()),
|
|
110
|
+
onAcquireError: (error) => config.toParentMessage(resource.onAcquireError(error)),
|
|
92
111
|
}));
|
|
112
|
+
/**
|
|
113
|
+
* Combines multiple Managed Resources records into one. Throws on duplicate
|
|
114
|
+
* keys so a misconfigured aggregate fails loudly at startup rather than
|
|
115
|
+
* silently overriding.
|
|
116
|
+
*/
|
|
117
|
+
export const aggregate = () => (...records) => {
|
|
118
|
+
const result = {};
|
|
119
|
+
for (const record of records) {
|
|
120
|
+
for (const key of Object.keys(record)) {
|
|
121
|
+
if (Object.hasOwn(result, key)) {
|
|
122
|
+
throw new Error(`ManagedResource.aggregate: duplicate key "${key}" across records`);
|
|
123
|
+
}
|
|
124
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
125
|
+
result[key] = record[key];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
129
|
+
return result;
|
|
130
|
+
};
|
|
@@ -3,6 +3,7 @@ import type { Command } from '../command/index.js';
|
|
|
3
3
|
import { Document } from '../html/index.js';
|
|
4
4
|
import { UrlRequest } from '../navigation/urlRequest.js';
|
|
5
5
|
import { Url } from '../url/index.js';
|
|
6
|
+
import { VNode } from '../vdom.js';
|
|
6
7
|
import type { ManagedResources } from './managedResource.js';
|
|
7
8
|
import type { Subscriptions } from './subscription.js';
|
|
8
9
|
/** Position of the DevTools badge and panel on screen. */
|
|
@@ -88,11 +89,14 @@ export type RoutingConfig<Message> = Readonly<{
|
|
|
88
89
|
onUrlRequest: (request: UrlRequest) => Message;
|
|
89
90
|
onUrlChange: (url: Url) => Message;
|
|
90
91
|
}>;
|
|
91
|
-
/** Context provided to crash.view and crash.report when the runtime encounters
|
|
92
|
+
/** Context provided to crash.view and crash.report when the runtime encounters
|
|
93
|
+
* an unrecoverable error. `message` is the Message being processed when the
|
|
94
|
+
* crash occurred, present as an `Option` because a crash during the initial
|
|
95
|
+
* render has no triggering Message. */
|
|
92
96
|
export type CrashContext<Model, Message> = Readonly<{
|
|
93
97
|
error: Error;
|
|
94
98
|
model: Model;
|
|
95
|
-
message: Message
|
|
99
|
+
message: Option.Option<Message>;
|
|
96
100
|
}>;
|
|
97
101
|
/** Configuration for crash handling, with custom crash UI and/or crash reporting. */
|
|
98
102
|
export type CrashConfig<Model, Message> = Readonly<{
|
|
@@ -170,6 +174,7 @@ export type MakeRuntimeReturn = Readonly<{
|
|
|
170
174
|
runtimeId: string;
|
|
171
175
|
start: (hmrModel?: unknown) => Effect.Effect<void>;
|
|
172
176
|
}>;
|
|
177
|
+
export declare const patchVNode: (maybeCurrentVNode: Option.Option<VNode>, nextVNode: VNode | null, container: HTMLElement) => VNode;
|
|
173
178
|
/** Creates a Foldkit program and returns a runtime that can be passed to `run`. Add a `routing` config for URL routing. */
|
|
174
179
|
export declare function makeProgram<Model, Message extends {
|
|
175
180
|
_tag: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/runtime/runtime.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,OAAO,EAEP,MAAM,EAGN,KAAK,EAEL,MAAM,EAON,MAAM,EAIP,MAAM,QAAQ,CAAA;AAGf,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AASlD,OAAO,EAEL,QAAQ,EAKT,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAA;AACxD,OAAO,EAAE,GAAG,EAA+B,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/runtime/runtime.ts"],"names":[],"mappings":"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"}
|
package/dist/runtime/runtime.js
CHANGED
|
@@ -7,7 +7,7 @@ import { startWebSocketBridge } from '../devTools/webSocketBridge.js';
|
|
|
7
7
|
import { __beginRender as beginHtmlRender, __clearRuntime as clearHtmlRuntime, __createBoundaryRegistry as createHtmlBoundaryRegistry, __setRuntime as setHtmlRuntime, } from '../html/index.js';
|
|
8
8
|
import { MountTracker } from '../mount/index.js';
|
|
9
9
|
import { fromString as urlFromString } from '../url/index.js';
|
|
10
|
-
import { patch, toVNode } from '../vdom.js';
|
|
10
|
+
import { dedupeSharedVNodes, patch, toVNode } from '../vdom.js';
|
|
11
11
|
import { addBfcacheRestoreListener, addNavigationEventListeners, } from './browserListeners.js';
|
|
12
12
|
import { defaultCrashView, noOpDispatch } from './crashUI.js';
|
|
13
13
|
import { deepFreeze } from './deepFreeze.js';
|
|
@@ -208,6 +208,16 @@ const makeRuntime = ({ Model, flags: resolveFlags, init, update, view, subscript
|
|
|
208
208
|
}
|
|
209
209
|
const modelRef = yield* Ref.make(initModel);
|
|
210
210
|
const maybeCurrentVNodeRef = yield* Ref.make(Option.none());
|
|
211
|
+
// NOTE: shared by every perpetual fiber's crash path (init render,
|
|
212
|
+
// render loop, message drain). Each fiber catches its own cause so a
|
|
213
|
+
// failure surfaces as the crash view instead of dying silently and
|
|
214
|
+
// leaving the DOM frozen at the last successful render.
|
|
215
|
+
const crashWith = (cause, maybeMessage) => Effect.gen(function* () {
|
|
216
|
+
const model = yield* Ref.get(modelRef);
|
|
217
|
+
const squashed = Cause.squash(cause);
|
|
218
|
+
const error = squashed instanceof Error ? squashed : new Error(String(squashed));
|
|
219
|
+
renderCrashView({ error, model, message: maybeMessage }, crash, container, maybeCurrentVNodeRef);
|
|
220
|
+
});
|
|
211
221
|
// NOTE: queue-drain-fiber-local state. Kept as plain closure
|
|
212
222
|
// variables instead of `Ref`s because nothing else reads or writes
|
|
213
223
|
// them concurrently, and JS's single-threaded model already orders
|
|
@@ -230,7 +240,7 @@ const makeRuntime = ({ Model, flags: resolveFlags, init, update, view, subscript
|
|
|
230
240
|
enqueueHigh(message);
|
|
231
241
|
const dispatch = { dispatchAsync, dispatchSync };
|
|
232
242
|
const isRenderPendingRef = yield* SubscriptionRef.make(false);
|
|
233
|
-
const
|
|
243
|
+
const maybeLastDirtyMessageRef = yield* Ref.make(Option.none());
|
|
234
244
|
const isPausedEffect = Effect.suspend(() => Option.match(maybeDevToolsStore, {
|
|
235
245
|
onNone: () => Effect.succeed(false),
|
|
236
246
|
onSome: ({ stateRef }) => SubscriptionRef.get(stateRef).pipe(Effect.map(({ isPaused }) => isPaused)),
|
|
@@ -259,7 +269,7 @@ const makeRuntime = ({ Model, flags: resolveFlags, init, update, view, subscript
|
|
|
259
269
|
if (currentModel !== nextModel) {
|
|
260
270
|
yield* Ref.set(modelRef, nextModel);
|
|
261
271
|
yield* SubscriptionRef.set(isRenderPendingRef, true);
|
|
262
|
-
yield* Ref.set(
|
|
272
|
+
yield* Ref.set(maybeLastDirtyMessageRef, Option.some(message));
|
|
263
273
|
PubSub.publishUnsafe(modelPubSub, nextModel);
|
|
264
274
|
yield* schedulePreserveModel(nextModel);
|
|
265
275
|
}
|
|
@@ -395,25 +405,28 @@ const makeRuntime = ({ Model, flags: resolveFlags, init, update, view, subscript
|
|
|
395
405
|
maybeMessageSchema);
|
|
396
406
|
}
|
|
397
407
|
}
|
|
398
|
-
yield* render(initModel, Option.none());
|
|
408
|
+
const initRenderExit = yield* Effect.exit(render(initModel, Option.none()));
|
|
409
|
+
if (Exit.isFailure(initRenderExit)) {
|
|
410
|
+
return yield* crashWith(initRenderExit.cause, Option.none());
|
|
411
|
+
}
|
|
399
412
|
const initMountEvents = drainMountEvents();
|
|
400
413
|
yield* Option.match(maybeDevToolsStore, {
|
|
401
414
|
onNone: () => Effect.void,
|
|
402
415
|
onSome: store => store.recordInit(initModel, Array.map(initCommands, toCommandRecord), initMountEvents.starts),
|
|
403
416
|
});
|
|
404
|
-
// NOTE:
|
|
405
|
-
// slow-view callbacks during high-rate bursts attribute
|
|
406
|
-
// Message in the frame batch, not the specific one that
|
|
407
|
-
// view past threshold. Acceptable for a debug callback;
|
|
408
|
-
// attribution would require correlating each message with its
|
|
409
|
-
// contribution, which isn't worth the complexity.
|
|
417
|
+
// NOTE: maybeLastDirtyMessageRef holds the most recent dirtying
|
|
418
|
+
// Message, so slow-view callbacks during high-rate bursts attribute
|
|
419
|
+
// to the last Message in the frame batch, not the specific one that
|
|
420
|
+
// pushed the view past threshold. Acceptable for a debug callback;
|
|
421
|
+
// full attribution would require correlating each message with its
|
|
422
|
+
// render contribution, which isn't worth the complexity.
|
|
410
423
|
const renderLoop = makeRenderLoop({
|
|
411
424
|
pendingRef: isRenderPendingRef,
|
|
412
425
|
awaitNextFrame,
|
|
413
426
|
isPaused: isPausedEffect,
|
|
414
427
|
render: Effect.gen(function* () {
|
|
415
428
|
const model = yield* Ref.get(modelRef);
|
|
416
|
-
const maybeMessage = yield* Ref.get(
|
|
429
|
+
const maybeMessage = yield* Ref.get(maybeLastDirtyMessageRef);
|
|
417
430
|
yield* render(model, maybeMessage);
|
|
418
431
|
const mountEvents = drainMountEvents();
|
|
419
432
|
yield* Option.match(maybeDevToolsStore, {
|
|
@@ -422,7 +435,10 @@ const makeRuntime = ({ Model, flags: resolveFlags, init, update, view, subscript
|
|
|
422
435
|
});
|
|
423
436
|
}),
|
|
424
437
|
});
|
|
425
|
-
yield* Effect.forkDetach(renderLoop)
|
|
438
|
+
yield* Effect.forkDetach(renderLoop.pipe(Effect.catchCause(cause => Effect.gen(function* () {
|
|
439
|
+
const maybeMessage = yield* Ref.get(maybeLastDirtyMessageRef);
|
|
440
|
+
yield* crashWith(cause, maybeMessage);
|
|
441
|
+
}))));
|
|
426
442
|
addBfcacheRestoreListener();
|
|
427
443
|
if (subscriptions) {
|
|
428
444
|
yield* pipe(subscriptions, Record.toEntries, Effect.forEach(([_key, { dependenciesSchema, modelToDependencies, keepAliveEquivalence, dependenciesToStream, },]) => Effect.gen(function* () {
|
|
@@ -534,25 +550,19 @@ const makeRuntime = ({ Model, flags: resolveFlags, init, update, view, subscript
|
|
|
534
550
|
const rest = yield* pollAvailable;
|
|
535
551
|
yield* processBatch(Array.prepend(rest, first));
|
|
536
552
|
yield* drainQueue;
|
|
537
|
-
})), Effect.catchCause(cause =>
|
|
538
|
-
const squashed = Cause.squash(cause);
|
|
539
|
-
const appError = squashed instanceof Error
|
|
540
|
-
? squashed
|
|
541
|
-
: new Error(String(squashed));
|
|
542
|
-
const model = Effect.runSync(Ref.get(modelRef));
|
|
543
|
-
const message = Option.getOrThrow(currentMessage);
|
|
544
|
-
renderCrashView({ error: appError, model, message }, crash, container, maybeCurrentVNodeRef);
|
|
545
|
-
})));
|
|
553
|
+
})), Effect.catchCause(cause => crashWith(cause, currentMessage)));
|
|
546
554
|
}));
|
|
547
555
|
return { runtimeId, start };
|
|
548
556
|
};
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
557
|
+
// NOTE: exported for `patchVNode.test.ts` to assert the dedupeSharedVNodes
|
|
558
|
+
// wiring; not part of the public surface (`runtime/public.ts` is curated).
|
|
559
|
+
export const patchVNode = (maybeCurrentVNode, nextVNode, container) => {
|
|
560
|
+
const dedupedVNode = Predicate.isNotNull(nextVNode)
|
|
561
|
+
? dedupeSharedVNodes(nextVNode)
|
|
552
562
|
: h('!');
|
|
553
563
|
return Option.match(maybeCurrentVNode, {
|
|
554
|
-
onNone: () => patch(toVNode(container),
|
|
555
|
-
onSome: currentVNode => patch(currentVNode,
|
|
564
|
+
onNone: () => patch(toVNode(container), dedupedVNode),
|
|
565
|
+
onSome: currentVNode => patch(currentVNode, dedupedVNode),
|
|
556
566
|
});
|
|
557
567
|
};
|
|
558
568
|
const currentLocationUrl = () => {
|
|
@@ -40,7 +40,7 @@ export const aggregate = () => (...records) => {
|
|
|
40
40
|
const result = {};
|
|
41
41
|
for (const record of records) {
|
|
42
42
|
for (const key of Object.keys(record)) {
|
|
43
|
-
if (key
|
|
43
|
+
if (Object.hasOwn(result, key)) {
|
|
44
44
|
throw new Error(`Subscription.aggregate: duplicate key "${key}" across records`);
|
|
45
45
|
}
|
|
46
46
|
result[key] = record[key];
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Effect, Schema as S } from 'effect';
|
|
2
|
+
import * as Command from '../../command/index.js';
|
|
3
|
+
import { type Document } from '../../html/index.js';
|
|
4
|
+
export declare const RawSource: S.Struct<{
|
|
5
|
+
readonly kind: S.Literal<"Book">;
|
|
6
|
+
readonly id: S.String;
|
|
7
|
+
}>;
|
|
8
|
+
export type RawSource = typeof RawSource.Type;
|
|
9
|
+
export declare const Source: S.Struct<{
|
|
10
|
+
readonly kind: S.Literal<"Book">;
|
|
11
|
+
readonly id: S.String;
|
|
12
|
+
}>;
|
|
13
|
+
export type Source = typeof Source.Type;
|
|
14
|
+
export declare const Model: S.Struct<{
|
|
15
|
+
readonly sources: S.$Array<S.Struct<{
|
|
16
|
+
readonly kind: S.Literal<"Book">;
|
|
17
|
+
readonly id: S.String;
|
|
18
|
+
}>>;
|
|
19
|
+
}>;
|
|
20
|
+
export type Model = typeof Model.Type;
|
|
21
|
+
export declare const ClickedReload: import("../../schema/index.js").CallableTaggedStruct<"ClickedReload", {}>;
|
|
22
|
+
export declare const LoadedSources: import("../../schema/index.js").CallableTaggedStruct<"LoadedSources", {
|
|
23
|
+
sources: S.$Array<S.Struct<{
|
|
24
|
+
readonly kind: S.Literal<"Book">;
|
|
25
|
+
readonly id: S.String;
|
|
26
|
+
}>>;
|
|
27
|
+
}>;
|
|
28
|
+
export declare const SelectedSource: import("../../schema/index.js").CallableTaggedStruct<"SelectedSource", {
|
|
29
|
+
source: S.Struct<{
|
|
30
|
+
readonly kind: S.Literal<"Book">;
|
|
31
|
+
readonly id: S.String;
|
|
32
|
+
}>;
|
|
33
|
+
}>;
|
|
34
|
+
export declare const SubmittedNewSourceId: import("../../schema/index.js").CallableTaggedStruct<"SubmittedNewSourceId", {
|
|
35
|
+
id: S.String;
|
|
36
|
+
}>;
|
|
37
|
+
export declare const Message: S.Union<readonly [import("../../schema/index.js").CallableTaggedStruct<"ClickedReload", {}>, import("../../schema/index.js").CallableTaggedStruct<"LoadedSources", {
|
|
38
|
+
sources: S.$Array<S.Struct<{
|
|
39
|
+
readonly kind: S.Literal<"Book">;
|
|
40
|
+
readonly id: S.String;
|
|
41
|
+
}>>;
|
|
42
|
+
}>, import("../../schema/index.js").CallableTaggedStruct<"SelectedSource", {
|
|
43
|
+
source: S.Struct<{
|
|
44
|
+
readonly kind: S.Literal<"Book">;
|
|
45
|
+
readonly id: S.String;
|
|
46
|
+
}>;
|
|
47
|
+
}>, import("../../schema/index.js").CallableTaggedStruct<"SubmittedNewSourceId", {
|
|
48
|
+
id: S.String;
|
|
49
|
+
}>]>;
|
|
50
|
+
export type Message = typeof Message.Type;
|
|
51
|
+
export declare const reloadedSources: ReadonlyArray<RawSource>;
|
|
52
|
+
export declare const ReloadSources: Command.CommandDefinitionNoArgs<"ReloadSources", Effect.Effect<{
|
|
53
|
+
readonly _tag: "LoadedSources";
|
|
54
|
+
readonly sources: readonly {
|
|
55
|
+
readonly kind: "Book";
|
|
56
|
+
readonly id: string;
|
|
57
|
+
}[];
|
|
58
|
+
}, never, never>>;
|
|
59
|
+
export declare const validSources: ReadonlyArray<RawSource>;
|
|
60
|
+
export declare const malformedSources: ReadonlyArray<RawSource>;
|
|
61
|
+
export declare const initialModel: Model;
|
|
62
|
+
export declare const update: (model: Model, message: Message) => readonly [Model, ReadonlyArray<Command.Command<Message>>];
|
|
63
|
+
export declare const view: (model: Model) => Document;
|
|
64
|
+
//# sourceMappingURL=crashOnRender.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crashOnRender.d.ts","sourceRoot":"","sources":["../../../src/test/apps/crashOnRender.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAc,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAE/D,OAAO,KAAK,OAAO,MAAM,wBAAwB,CAAA;AACjD,OAAO,EAAE,KAAK,QAAQ,EAAQ,MAAM,qBAAqB,CAAA;AAMzD,eAAO,MAAM,SAAS;;;EAGpB,CAAA;AACF,MAAM,MAAM,SAAS,GAAG,OAAO,SAAS,CAAC,IAAI,CAAA;AAK7C,eAAO,MAAM,MAAM;;;EAGjB,CAAA;AACF,MAAM,MAAM,MAAM,GAAG,OAAO,MAAM,CAAC,IAAI,CAAA;AAEvC,eAAO,MAAM,KAAK;;;;;EAEhB,CAAA;AACF,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAA;AAIrC,eAAO,MAAM,aAAa,2EAAqB,CAAA;AAC/C,eAAO,MAAM,aAAa;;;;;EAAsD,CAAA;AAChF,eAAO,MAAM,cAAc;;;;;EAA0C,CAAA;AACrE,eAAO,MAAM,oBAAoB;;EAA8C,CAAA;AAE/E,eAAO,MAAM,OAAO;;;;;;;;;;;;IAKlB,CAAA;AACF,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAMzC,eAAO,MAAM,eAAe,EAAE,aAAa,CAAC,SAAS,CAGpD,CAAA;AAED,eAAO,MAAM,aAAa;;;;;;iBAGoC,CAAA;AAI9D,eAAO,MAAM,YAAY,EAAE,aAAa,CAAC,SAAS,CAEjD,CAAA;AAED,eAAO,MAAM,gBAAgB,EAAE,aAAa,CAAC,SAAS,CAErD,CAAA;AAED,eAAO,MAAM,YAAY,EAAE,KAAiC,CAAA;AAI5D,eAAO,MAAM,MAAM,GACjB,OAAO,KAAK,EACZ,SAAS,OAAO,KACf,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAiBxD,CAAA;AAIH,eAAO,MAAM,IAAI,GAAI,OAAO,KAAK,KAAG,QA8BnC,CAAA"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Array, Effect, Match as M, Schema as S } from 'effect';
|
|
2
|
+
import * as Command from '../../command/index.js';
|
|
3
|
+
import { html } from '../../html/index.js';
|
|
4
|
+
import { m } from '../../message/index.js';
|
|
5
|
+
import { evo } from '../../struct/index.js';
|
|
6
|
+
// MODEL
|
|
7
|
+
export const RawSource = S.Struct({
|
|
8
|
+
kind: S.Literal('Book'),
|
|
9
|
+
id: S.String,
|
|
10
|
+
});
|
|
11
|
+
// NOTE: the strict counterpart of RawSource, requiring a non-empty id.
|
|
12
|
+
// Constructing one (or a SelectedSource Message carrying one) from an empty id
|
|
13
|
+
// throws at construction time, which is what drives the render and update crash.
|
|
14
|
+
export const Source = S.Struct({
|
|
15
|
+
kind: S.Literal('Book'),
|
|
16
|
+
id: S.String.check(S.isNonEmpty()),
|
|
17
|
+
});
|
|
18
|
+
export const Model = S.Struct({
|
|
19
|
+
sources: S.Array(RawSource),
|
|
20
|
+
});
|
|
21
|
+
// MESSAGE
|
|
22
|
+
export const ClickedReload = m('ClickedReload');
|
|
23
|
+
export const LoadedSources = m('LoadedSources', { sources: S.Array(RawSource) });
|
|
24
|
+
export const SelectedSource = m('SelectedSource', { source: Source });
|
|
25
|
+
export const SubmittedNewSourceId = m('SubmittedNewSourceId', { id: S.String });
|
|
26
|
+
export const Message = S.Union([
|
|
27
|
+
ClickedReload,
|
|
28
|
+
LoadedSources,
|
|
29
|
+
SelectedSource,
|
|
30
|
+
SubmittedNewSourceId,
|
|
31
|
+
]);
|
|
32
|
+
// COMMAND
|
|
33
|
+
// NOTE: the malformed entry (empty id) sits inertly as a RawSource until the
|
|
34
|
+
// view rebuilds its SelectedSource handler, which constructs a Source and throws.
|
|
35
|
+
export const reloadedSources = [
|
|
36
|
+
{ kind: 'Book', id: '1' },
|
|
37
|
+
{ kind: 'Book', id: '' },
|
|
38
|
+
];
|
|
39
|
+
export const ReloadSources = Command.define('ReloadSources', LoadedSources)(Effect.succeed(LoadedSources({ sources: reloadedSources })));
|
|
40
|
+
// INIT
|
|
41
|
+
export const validSources = [
|
|
42
|
+
{ kind: 'Book', id: '1' },
|
|
43
|
+
];
|
|
44
|
+
export const malformedSources = [
|
|
45
|
+
{ kind: 'Book', id: '' },
|
|
46
|
+
];
|
|
47
|
+
export const initialModel = { sources: validSources };
|
|
48
|
+
// UPDATE
|
|
49
|
+
export const update = (model, message) => M.value(message).pipe(M.withReturnType(), M.tagsExhaustive({
|
|
50
|
+
ClickedReload: () => [model, [ReloadSources()]],
|
|
51
|
+
LoadedSources: ({ sources }) => [
|
|
52
|
+
evo(model, { sources: () => sources }),
|
|
53
|
+
[],
|
|
54
|
+
],
|
|
55
|
+
SelectedSource: () => [model, []],
|
|
56
|
+
SubmittedNewSourceId: ({ id }) => {
|
|
57
|
+
const source = Source.make({ kind: 'Book', id });
|
|
58
|
+
return [evo(model, { sources: Array.append(source) }), []];
|
|
59
|
+
},
|
|
60
|
+
}));
|
|
61
|
+
// VIEW
|
|
62
|
+
export const view = (model) => {
|
|
63
|
+
const h = html();
|
|
64
|
+
const body = h.div([], [
|
|
65
|
+
h.button([h.OnClick(ClickedReload()), h.Role('button')], ['Reload']),
|
|
66
|
+
h.button([h.OnClick(SubmittedNewSourceId({ id: '' })), h.Role('button')], ['Add source']),
|
|
67
|
+
h.ul([h.Role('list')], Array.map(model.sources, source => h.keyed('li')(source.id, [], [
|
|
68
|
+
h.button([h.OnClick(SelectedSource({ source })), h.Role('button')], [`Select ${source.id}`]),
|
|
69
|
+
]))),
|
|
70
|
+
]);
|
|
71
|
+
return { title: 'Sources', body };
|
|
72
|
+
};
|
package/dist/vdom.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { toVNode } from 'snabbdom';
|
|
1
|
+
import { type VNode, toVNode } from 'snabbdom';
|
|
2
2
|
export type { VNode } from 'snabbdom';
|
|
3
3
|
export { toVNode };
|
|
4
|
-
export declare const patch: (oldVnode:
|
|
4
|
+
export declare const patch: (oldVnode: VNode | Element | DocumentFragment, vnode: VNode) => VNode;
|
|
5
|
+
export declare const dedupeSharedVNodes: (root: VNode) => VNode;
|
|
5
6
|
//# sourceMappingURL=vdom.d.ts.map
|
package/dist/vdom.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vdom.d.ts","sourceRoot":"","sources":["../src/vdom.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"vdom.d.ts","sourceRoot":"","sources":["../src/vdom.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,KAAK,EAOV,OAAO,EACR,MAAM,UAAU,CAAA;AAIjB,YAAY,EAAE,KAAK,EAAE,MAAM,UAAU,CAAA;AACrC,OAAO,EAAE,OAAO,EAAE,CAAA;AAElB,eAAO,MAAM,KAAK,uEAOhB,CAAA;AAeF,eAAO,MAAM,kBAAkB,GAAI,MAAM,KAAK,KAAG,KA8BhD,CAAA"}
|
package/dist/vdom.js
CHANGED
|
@@ -9,3 +9,47 @@ export const patch = init([
|
|
|
9
9
|
propsModule,
|
|
10
10
|
styleModule,
|
|
11
11
|
]);
|
|
12
|
+
// NOTE: snabbdom records each element's live DOM node on `vnode.elm` by
|
|
13
|
+
// mutating the vnode object during patch. A vnode object placed in more than
|
|
14
|
+
// one tree position would share a single `.elm`, so removals and text updates
|
|
15
|
+
// land on the wrong DOM node. View code legitimately reuses vnode values, e.g.
|
|
16
|
+
// `const checkmark = h.span(...)` dropped into several slots, so before each
|
|
17
|
+
// patch we clone any vnode object reached a second time, giving every position
|
|
18
|
+
// its own object.
|
|
19
|
+
// Detection is keyed off a per-patch Set, so a vnode reused across renders (a
|
|
20
|
+
// memoized `createLazy` subtree, the identical object each render) is reached
|
|
21
|
+
// only once per patch and passes through untouched, leaving snabbdom's
|
|
22
|
+
// same-object subtree short-circuit intact. Allocation happens only along
|
|
23
|
+
// paths where a duplicate is actually found; a tree with no reuse returns
|
|
24
|
+
// unchanged.
|
|
25
|
+
export const dedupeSharedVNodes = (root) => {
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
const visit = (node) => {
|
|
28
|
+
const base = seen.has(node) ? { ...node, elm: undefined } : node;
|
|
29
|
+
seen.add(base);
|
|
30
|
+
const children = base.children;
|
|
31
|
+
if (children === undefined) {
|
|
32
|
+
return base;
|
|
33
|
+
}
|
|
34
|
+
let nextChildren;
|
|
35
|
+
for (let index = 0; index < children.length; index++) {
|
|
36
|
+
const child = children[index];
|
|
37
|
+
const deduped = typeof child === 'string' ? child : visit(child);
|
|
38
|
+
if (deduped !== child) {
|
|
39
|
+
if (nextChildren === undefined) {
|
|
40
|
+
nextChildren = children.slice();
|
|
41
|
+
}
|
|
42
|
+
nextChildren[index] = deduped;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (nextChildren === undefined) {
|
|
46
|
+
return base;
|
|
47
|
+
}
|
|
48
|
+
if (base === node) {
|
|
49
|
+
return { ...node, children: nextChildren };
|
|
50
|
+
}
|
|
51
|
+
base.children = nextChildren;
|
|
52
|
+
return base;
|
|
53
|
+
};
|
|
54
|
+
return visit(root);
|
|
55
|
+
};
|