foldkit 0.99.0 → 0.100.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -35
- package/dist/devTools/overlay.d.ts.map +1 -1
- package/dist/devTools/overlay.js +6 -1
- package/dist/devTools/protocol.d.ts +94 -2
- package/dist/devTools/protocol.d.ts.map +1 -1
- package/dist/devTools/protocol.js +32 -0
- package/dist/devTools/public.d.ts +1 -1
- package/dist/devTools/public.d.ts.map +1 -1
- package/dist/devTools/public.js +1 -1
- package/dist/devTools/schemaSummarize.d.ts +82 -0
- package/dist/devTools/schemaSummarize.d.ts.map +1 -0
- package/dist/devTools/schemaSummarize.js +264 -0
- package/dist/devTools/store.d.ts.map +1 -1
- package/dist/devTools/store.js +50 -49
- package/dist/devTools/webSocketBridge.d.ts +9 -0
- package/dist/devTools/webSocketBridge.d.ts.map +1 -1
- package/dist/devTools/webSocketBridge.js +64 -3
- package/dist/runtime/browserListeners.d.ts +1 -0
- package/dist/runtime/browserListeners.d.ts.map +1 -1
- package/dist/runtime/browserListeners.js +17 -5
- package/dist/test/apps/bubbling.d.ts.map +1 -1
- package/dist/test/apps/bubbling.js +7 -5
- package/dist/test/apps/counter.d.ts.map +1 -1
- package/dist/test/apps/counter.js +8 -6
- package/dist/test/apps/disabledButton.d.ts.map +1 -1
- package/dist/test/apps/disabledButton.js +32 -21
- package/dist/test/apps/fileUpload.d.ts.map +1 -1
- package/dist/test/apps/fileUpload.js +18 -16
- package/dist/test/apps/interactions.d.ts.map +1 -1
- package/dist/test/apps/interactions.js +21 -19
- package/dist/test/apps/keypress.d.ts.map +1 -1
- package/dist/test/apps/keypress.js +12 -10
- package/dist/test/apps/login.d.ts.map +1 -1
- package/dist/test/apps/login.js +38 -36
- package/dist/test/apps/logoutButton.d.ts.map +1 -1
- package/dist/test/apps/logoutButton.js +4 -2
- package/dist/test/apps/mountPanel.d.ts.map +1 -1
- package/dist/test/apps/mountPanel.js +29 -21
- package/dist/test/apps/multiRole.d.ts.map +1 -1
- package/dist/test/apps/multiRole.js +6 -4
- package/dist/test/apps/pointer.d.ts.map +1 -1
- package/dist/test/apps/pointer.js +15 -13
- package/dist/test/apps/resumeUpload.d.ts.map +1 -1
- package/dist/test/apps/resumeUpload.js +20 -15
- package/package.json +1 -1
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { Array as Array_, Option, Predicate, Record, String as String_, flow, pipe, } from 'effect';
|
|
2
|
+
const PATH_SEPARATOR = '.';
|
|
3
|
+
const isRecord = (value) => Predicate.isObject(value) && !Array_.isArray(value);
|
|
4
|
+
const isReadonlyArray = (value) => Array_.isArray(value);
|
|
5
|
+
const isStringArray = (values) => Array_.every(values, Predicate.isString);
|
|
6
|
+
const stringEnumOf = (schema) => {
|
|
7
|
+
if (!isRecord(schema)) {
|
|
8
|
+
return Option.none();
|
|
9
|
+
}
|
|
10
|
+
const candidate = schema['enum'];
|
|
11
|
+
if (!Array_.isArray(candidate)) {
|
|
12
|
+
return Option.none();
|
|
13
|
+
}
|
|
14
|
+
return Option.liftPredicate(candidate, isStringArray);
|
|
15
|
+
};
|
|
16
|
+
const variantTagOf = (schema) => {
|
|
17
|
+
if (!isRecord(schema) || schema['type'] !== 'object') {
|
|
18
|
+
return Option.none();
|
|
19
|
+
}
|
|
20
|
+
const properties = schema['properties'];
|
|
21
|
+
if (!isRecord(properties)) {
|
|
22
|
+
return Option.none();
|
|
23
|
+
}
|
|
24
|
+
return stringEnumOf(properties['_tag']).pipe(Option.flatMap(tags => tags.length === 1 ? Array_.head(tags) : Option.none()));
|
|
25
|
+
};
|
|
26
|
+
const anyOfOf = (schema) => {
|
|
27
|
+
if (!isRecord(schema)) {
|
|
28
|
+
return Option.none();
|
|
29
|
+
}
|
|
30
|
+
return Option.liftPredicate(schema['anyOf'], isReadonlyArray);
|
|
31
|
+
};
|
|
32
|
+
const isDiscriminatedUnion = (schema) => Option.exists(anyOfOf(schema), Predicate.and(Array_.isReadonlyArrayNonEmpty, Array_.every(flow(variantTagOf, Option.isSome))));
|
|
33
|
+
const payloadFieldsOf = (variant) => {
|
|
34
|
+
if (!isRecord(variant)) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
const properties = variant['properties'];
|
|
38
|
+
if (!isRecord(properties)) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
return pipe(Record.keys(properties), Array_.filter(name => name !== '_tag'));
|
|
42
|
+
};
|
|
43
|
+
const unionFieldsOf = (variant) => {
|
|
44
|
+
if (!isRecord(variant)) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
const properties = variant['properties'];
|
|
48
|
+
if (!isRecord(properties)) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
return pipe(Record.toEntries(properties), Array_.filter(([name, schema]) => name !== '_tag' && isDiscriminatedUnion(schema)), Array_.map(([name]) => name));
|
|
52
|
+
};
|
|
53
|
+
const entryOf = (variant) => variantTagOf(variant).pipe(Option.map(tag => ({
|
|
54
|
+
tag,
|
|
55
|
+
payloadFields: payloadFieldsOf(variant),
|
|
56
|
+
unionFields: unionFieldsOf(variant),
|
|
57
|
+
})));
|
|
58
|
+
const topLevelVariantsOf = (document) => {
|
|
59
|
+
if (!isRecord(document)) {
|
|
60
|
+
return Option.none();
|
|
61
|
+
}
|
|
62
|
+
return anyOfOf(document['schema']);
|
|
63
|
+
};
|
|
64
|
+
const indexEntriesOf = (members) => pipe(members, Array_.map(entryOf), Array_.getSomes);
|
|
65
|
+
/**
|
|
66
|
+
* Build a flat directory of every top-level Message variant from a JSON Schema
|
|
67
|
+
* document produced by `Schema.toJsonSchemaDocument`. The directory is small
|
|
68
|
+
* even for hundreds of variants, so an MCP client can paginate by tag without
|
|
69
|
+
* paying for the full schema.
|
|
70
|
+
*
|
|
71
|
+
* Returns `None` when the document's top-level `schema` is not a discriminated
|
|
72
|
+
* union of `_tag`-keyed structs (e.g. a single-variant Message Schema, or a
|
|
73
|
+
* shape produced by a future Effect Schema release that the summarizer does
|
|
74
|
+
* not yet understand). The caller should fall back to fetching the full
|
|
75
|
+
* document in that case.
|
|
76
|
+
*/
|
|
77
|
+
export const indexMessageSchemaDocument = (document) => Option.map(topLevelVariantsOf(document), indexEntriesOf);
|
|
78
|
+
const collapseUnionsInValue = (value) => {
|
|
79
|
+
if (Array_.isArray(value)) {
|
|
80
|
+
return Array_.map(value, collapseUnionsInValue);
|
|
81
|
+
}
|
|
82
|
+
if (!isRecord(value)) {
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
if (isDiscriminatedUnion(value)) {
|
|
86
|
+
return Option.match(anyOfOf(value), {
|
|
87
|
+
onNone: () => value,
|
|
88
|
+
onSome: members => ({
|
|
89
|
+
_summary: 'union',
|
|
90
|
+
variants: indexEntriesOf(members),
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return Record.map(value, child => collapseUnionsInValue(child));
|
|
95
|
+
};
|
|
96
|
+
const collapseUnionsInVariantPayload = (variant) => {
|
|
97
|
+
if (!isRecord(variant)) {
|
|
98
|
+
return variant;
|
|
99
|
+
}
|
|
100
|
+
const properties = variant['properties'];
|
|
101
|
+
if (!isRecord(properties)) {
|
|
102
|
+
return variant;
|
|
103
|
+
}
|
|
104
|
+
const collapsed = Record.map(properties, (schema, name) => name === '_tag' ? schema : collapseUnionsInValue(schema));
|
|
105
|
+
return { ...variant, properties: collapsed };
|
|
106
|
+
};
|
|
107
|
+
const findVariantByTag = (members, tag) => Array_.findFirst(members, variant => Option.exists(variantTagOf(variant), candidate => candidate === tag));
|
|
108
|
+
/**
|
|
109
|
+
* Idiomatic Foldkit Messages carry at most one tagged-union payload field per
|
|
110
|
+
* variant: either a `Got<Child>Message { message }` Submodel wrapper or a
|
|
111
|
+
* regular Message with one tagged-union value-type payload (e.g. `ClickedLink {
|
|
112
|
+
* request: UrlRequest }`). Multi-union-field variants are non-idiomatic;
|
|
113
|
+
* surrounding state that a child Submodel needs belongs as an argument to
|
|
114
|
+
* the child's `update`/`view`, not as a sibling field on the parent Message.
|
|
115
|
+
* Returns `None` when zero or multiple union fields exist so the path walker
|
|
116
|
+
* can produce an actionable error rather than silently picking one.
|
|
117
|
+
*/
|
|
118
|
+
const singleUnionFieldOf = (variant) => {
|
|
119
|
+
if (!isRecord(variant)) {
|
|
120
|
+
return Option.none();
|
|
121
|
+
}
|
|
122
|
+
const properties = variant['properties'];
|
|
123
|
+
if (!isRecord(properties)) {
|
|
124
|
+
return Option.none();
|
|
125
|
+
}
|
|
126
|
+
const unionEntries = pipe(Record.toEntries(properties), Array_.filter(([name, schema]) => name !== '_tag' && isDiscriminatedUnion(schema)));
|
|
127
|
+
if (unionEntries.length !== 1) {
|
|
128
|
+
return Option.none();
|
|
129
|
+
}
|
|
130
|
+
return Option.gen(function* () {
|
|
131
|
+
const [name, schema] = yield* Array_.head(unionEntries);
|
|
132
|
+
const members = yield* anyOfOf(schema);
|
|
133
|
+
return { name, members };
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
const replaceUnionField = (variant, fieldName, narrowedChild) => {
|
|
137
|
+
if (!isRecord(variant)) {
|
|
138
|
+
return variant;
|
|
139
|
+
}
|
|
140
|
+
const properties = variant['properties'];
|
|
141
|
+
if (!isRecord(properties)) {
|
|
142
|
+
return variant;
|
|
143
|
+
}
|
|
144
|
+
const updated = {
|
|
145
|
+
...properties,
|
|
146
|
+
[fieldName]: { anyOf: [narrowedChild] },
|
|
147
|
+
};
|
|
148
|
+
return { ...variant, properties: updated };
|
|
149
|
+
};
|
|
150
|
+
const stepIntoVariant = (variant, rest) => Option.gen(function* () {
|
|
151
|
+
const field = yield* singleUnionFieldOf(variant);
|
|
152
|
+
const narrowedChild = yield* narrowAtPath(field.members, rest);
|
|
153
|
+
return replaceUnionField(variant, field.name, narrowedChild);
|
|
154
|
+
});
|
|
155
|
+
const narrowAtPath = (members, segments) => Option.gen(function* () {
|
|
156
|
+
const tag = yield* Array_.head(segments);
|
|
157
|
+
const variant = yield* findVariantByTag(members, tag);
|
|
158
|
+
const rest = Array_.drop(segments, 1);
|
|
159
|
+
if (Array_.isReadonlyArrayEmpty(rest)) {
|
|
160
|
+
return collapseUnionsInVariantPayload(variant);
|
|
161
|
+
}
|
|
162
|
+
return yield* stepIntoVariant(variant, rest);
|
|
163
|
+
});
|
|
164
|
+
/**
|
|
165
|
+
* Split a dot-separated variant path into its segments. Empty segments are
|
|
166
|
+
* dropped so callers can pass user-supplied strings without first trimming
|
|
167
|
+
* leading/trailing dots.
|
|
168
|
+
*/
|
|
169
|
+
export const splitVariantPath = (variantPath) => pipe(variantPath, String_.split(PATH_SEPARATOR), Array_.filter(String_.isNonEmpty));
|
|
170
|
+
const stepToNextUnionMembers = (members, segment) => Option.gen(function* () {
|
|
171
|
+
const variant = yield* findVariantByTag(members, segment);
|
|
172
|
+
const field = yield* singleUnionFieldOf(variant);
|
|
173
|
+
return field.members;
|
|
174
|
+
});
|
|
175
|
+
const variantsAtPathPrefix = (document, segments) => Option.flatMap(topLevelVariantsOf(document), topMembers => Array_.reduce(segments, Option.some(topMembers), (currentMembers, segment) => Option.flatMap(currentMembers, members => stepToNextUnionMembers(members, segment))));
|
|
176
|
+
/**
|
|
177
|
+
* Enumerate the variant tags available as the next segment of a variant path.
|
|
178
|
+
* Given a partial path that resolves to a tagged-union field, returns the tags
|
|
179
|
+
* of every variant in that union. Useful for crafting `not-found` error
|
|
180
|
+
* messages: if `narrowToVariant` fails on `"a.b.c"`, calling this with `["a", "b"]`
|
|
181
|
+
* yields the valid choices for the third segment.
|
|
182
|
+
*
|
|
183
|
+
* Returns `None` when the prefix cannot be resolved at all (e.g. the first
|
|
184
|
+
* segment names no top-level variant, or an intermediate variant lacks a
|
|
185
|
+
* tagged-union payload field).
|
|
186
|
+
*/
|
|
187
|
+
export const variantTagsAtPathPrefix = (document, pathPrefix) => Option.map(variantsAtPathPrefix(document, pathPrefix), members => Array_.map(indexEntriesOf(members), entry => entry.tag));
|
|
188
|
+
/**
|
|
189
|
+
* Diagnose where a variant-path walk would fail. Walks back from one segment
|
|
190
|
+
* before the supplied path length, returning the deepest prefix whose
|
|
191
|
+
* tagged-union level resolves cleanly, the tags available at that level, and
|
|
192
|
+
* the next segment from the original path (the one that broke the walk). When
|
|
193
|
+
* the offending segment is a known tag at that level, the failure means the
|
|
194
|
+
* variant exists but has no tagged-union field to step into further. When it
|
|
195
|
+
* is unknown, the failure is a simple typo. Returns `None` when not even the
|
|
196
|
+
* empty prefix resolves (i.e. the document is not a discriminated union at
|
|
197
|
+
* the top level).
|
|
198
|
+
*/
|
|
199
|
+
export const diagnoseVariantPath = (document, segments) => {
|
|
200
|
+
const tryLength = (length) => {
|
|
201
|
+
if (length < 0) {
|
|
202
|
+
return Option.none();
|
|
203
|
+
}
|
|
204
|
+
const prefix = Array_.take(segments, length);
|
|
205
|
+
return Option.match(variantTagsAtPathPrefix(document, prefix), {
|
|
206
|
+
onNone: () => tryLength(length - 1),
|
|
207
|
+
onSome: available => Option.some({
|
|
208
|
+
prefix,
|
|
209
|
+
failingSegment: Array_.get(segments, length),
|
|
210
|
+
available,
|
|
211
|
+
}),
|
|
212
|
+
});
|
|
213
|
+
};
|
|
214
|
+
return tryLength(segments.length - 1);
|
|
215
|
+
};
|
|
216
|
+
/**
|
|
217
|
+
* Replace the top-level `anyOf` in a JSON Schema document with a single-element
|
|
218
|
+
* `anyOf` containing the variant(s) selected by a dot-separated variant path.
|
|
219
|
+
*
|
|
220
|
+
* `variantPath` is a dot-string of variant `_tag` values, walked through the
|
|
221
|
+
* one tagged-union payload field at each step. For example, `"GotChildMessage"`
|
|
222
|
+
* narrows the top-level union to that wrapper variant, and
|
|
223
|
+
* `"GotChildMessage.Opened"` walks into the wrapper's nested union and narrows
|
|
224
|
+
* to the inner variant. Any deeper discriminated unions inside the deepest
|
|
225
|
+
* variant's payload are collapsed to `{ _summary: 'union', variants: [...] }`
|
|
226
|
+
* placeholders so the response stays small even for deeply-nested Submodel
|
|
227
|
+
* trees; agents drill further by extending the path.
|
|
228
|
+
*
|
|
229
|
+
* The `definitions` block is kept (any `$ref` targets the narrowed variant
|
|
230
|
+
* relies on still resolve, and dead refs left over from trimmed variants are
|
|
231
|
+
* harmless), but any discriminated unions inside it are collapsed to the same
|
|
232
|
+
* `_summary` placeholder shape so a shared union annotated with an
|
|
233
|
+
* `identifier` does not balloon the response. The path walker does not
|
|
234
|
+
* resolve `$ref` indirection through `definitions`; agents that need to step
|
|
235
|
+
* through a `$ref`-shared union look up the definition by name and use the
|
|
236
|
+
* placeholder's variant list directly.
|
|
237
|
+
*
|
|
238
|
+
* Returns `None` when the document is not a top-level discriminated union, the
|
|
239
|
+
* path is empty, a segment names no variant in the current union, or an
|
|
240
|
+
* intermediate variant lacks exactly one tagged-union payload field to step
|
|
241
|
+
* into (zero or multiple union fields are both ambiguous). The "exactly one"
|
|
242
|
+
* rule encodes the Foldkit idiom (`Got<Child>Message { message }` Submodel
|
|
243
|
+
* wrappers, single-union value-type fields); apps whose Message variants need
|
|
244
|
+
* additional surrounding state should pass it as an argument to the child's
|
|
245
|
+
* `update`/`view` rather than as a sibling field on the parent Message.
|
|
246
|
+
*/
|
|
247
|
+
export const narrowToVariant = (document, variantPath) => {
|
|
248
|
+
if (!isRecord(document)) {
|
|
249
|
+
return Option.none();
|
|
250
|
+
}
|
|
251
|
+
const segments = splitVariantPath(variantPath);
|
|
252
|
+
if (Array_.isReadonlyArrayEmpty(segments)) {
|
|
253
|
+
return Option.none();
|
|
254
|
+
}
|
|
255
|
+
return Option.gen(function* () {
|
|
256
|
+
const members = yield* topLevelVariantsOf(document);
|
|
257
|
+
const narrowed = yield* narrowAtPath(members, segments);
|
|
258
|
+
return {
|
|
259
|
+
...document,
|
|
260
|
+
schema: { anyOf: [narrowed] },
|
|
261
|
+
definitions: collapseUnionsInValue(document['definitions']),
|
|
262
|
+
};
|
|
263
|
+
});
|
|
264
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/devTools/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,MAAM,EACN,OAAO,EACP,OAAO,EAEP,MAAM,EAIN,eAAe,EAEhB,MAAM,QAAQ,CAAA;AAIf,eAAO,MAAM,UAAU,KAAK,CAAA;AAM5B,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAChC,YAAY,EAAE,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;IACrC,aAAa,EAAE,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;CACvC,CAAC,CAAA;AAEF,eAAO,MAAM,SAAS,EAAE,UAGvB,CAAA;AAID,eAAO,MAAM,WAAW,GACtB,UAAU,OAAO,EACjB,SAAS,OAAO,KACf,UA6EF,CAAA;AAID,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC;IACnC,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC/B,CAAC,CAAA;AAEF,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC/B,CAAC,CAAA;AAEF,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC;IAClC,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,CAAA;IACtC,WAAW,EAAE,aAAa,CAAC,WAAW,CAAC,CAAA;IACvC,SAAS,EAAE,aAAa,CAAC,WAAW,CAAC,CAAA;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,EAAE,OAAO,CAAA;IACvB,IAAI,EAAE,UAAU,CAAA;CACjB,CAAC,CAAA;AAEF,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAChC,OAAO,EAAE,aAAa,CAAC,YAAY,CAAC,CAAA;IACpC,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC3C,cAAc,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IACtC,YAAY,EAAE,aAAa,CAAC,aAAa,CAAC,CAAA;IAC1C,eAAe,EAAE,aAAa,CAAC,WAAW,CAAC,CAAA;IAC3C,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,OAAO,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,gBAAgB,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;CACzC,CAAC,CAAA;AAEF,MAAM,MAAM,MAAM,GAAG,QAAQ,CAAC;IAC5B,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAA;IACrD,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAC/C,iBAAiB,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;CACvC,CAAC,CAAA;AAcF;;;;;GAKG;AACH,MAAM,MAAM,0BAA0B,GAAG,QAAQ,CAAC;IAChD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B,CAAC,CAAA;AAEF,eAAO,MAAM,mBAAmB,GAC9B,QAAQ,MAAM,EACd,UAAS,0BAA+B,KACvC,MAAM,CAAC,MAAM,CAAC,aAAa,
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/devTools/store.ts"],"names":[],"mappings":"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"}
|
package/dist/devTools/store.js
CHANGED
|
@@ -97,28 +97,26 @@ export const createDevToolsStore = (bridge, options = {}) => Effect.gen(function
|
|
|
97
97
|
: state.startIndex;
|
|
98
98
|
return pipe(state.keyframes, HashMap.get(keyframeIndex), Option.map(keyframeModel => pipe(state.entries, Array.drop(keyframeIndex - state.startIndex), Array.take(index - keyframeIndex + 1), Array.reduce(keyframeModel, (model, entry) => bridge.replay(model, entry.message)))), Option.getOrThrow);
|
|
99
99
|
};
|
|
100
|
-
const addKeyframeIfNeeded = (
|
|
100
|
+
const addKeyframeIfNeeded = (nextAbsoluteIndex, modelAfterUpdate) => (keyframes) => nextAbsoluteIndex % keyframeInterval === 0
|
|
101
101
|
? HashMap.set(keyframes, nextAbsoluteIndex, modelAfterUpdate)
|
|
102
102
|
: keyframes;
|
|
103
103
|
const evictOldestSegment = (state) => {
|
|
104
104
|
const nextStartIndex = state.startIndex + keyframeInterval;
|
|
105
105
|
const isPausedAtRetainedIndex = state.pausedAtIndex >= nextStartIndex ||
|
|
106
106
|
state.pausedAtIndex === INIT_INDEX;
|
|
107
|
-
return {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
};
|
|
107
|
+
return evo(state, {
|
|
108
|
+
entries: Array.drop(keyframeInterval),
|
|
109
|
+
keyframes: HashMap.remove(state.startIndex),
|
|
110
|
+
startIndex: () => nextStartIndex,
|
|
111
|
+
isPaused: isPaused => isPaused && isPausedAtRetainedIndex,
|
|
112
|
+
});
|
|
114
113
|
};
|
|
115
|
-
const recordInit = (model, commands, mountStarts = []) => SubscriptionRef.update(stateRef, state => ({
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
maybeLatestModel: Option.some(model),
|
|
114
|
+
const recordInit = (model, commands, mountStarts = []) => SubscriptionRef.update(stateRef, state => evo(state, {
|
|
115
|
+
maybeInitModel: () => Option.some(model),
|
|
116
|
+
initCommands: () => commands,
|
|
117
|
+
initMountStarts: () => mountStarts,
|
|
118
|
+
keyframes: HashMap.set(0, model),
|
|
119
|
+
maybeLatestModel: () => Option.some(model),
|
|
122
120
|
}));
|
|
123
121
|
const recordMessage = (message, modelBeforeUpdate, modelAfterUpdate, commands, isModelChanged) => SubscriptionRef.update(stateRef, state => {
|
|
124
122
|
const absoluteIndex = state.startIndex + state.entries.length;
|
|
@@ -126,9 +124,8 @@ export const createDevToolsStore = (bridge, options = {}) => Effect.gen(function
|
|
|
126
124
|
? computeDiff(modelBeforeUpdate, modelAfterUpdate)
|
|
127
125
|
: emptyDiff;
|
|
128
126
|
const hasChangedFields = HashSet.size(diff.changedPaths) > 0;
|
|
129
|
-
const nextState = {
|
|
130
|
-
|
|
131
|
-
entries: Array.append(state.entries, {
|
|
127
|
+
const nextState = evo(state, {
|
|
128
|
+
entries: Array.append({
|
|
132
129
|
tag: message._tag,
|
|
133
130
|
message,
|
|
134
131
|
commands,
|
|
@@ -138,9 +135,9 @@ export const createDevToolsStore = (bridge, options = {}) => Effect.gen(function
|
|
|
138
135
|
isModelChanged: hasChangedFields,
|
|
139
136
|
diff,
|
|
140
137
|
}),
|
|
141
|
-
keyframes: addKeyframeIfNeeded(
|
|
142
|
-
maybeLatestModel: Option.some(modelAfterUpdate),
|
|
143
|
-
};
|
|
138
|
+
keyframes: addKeyframeIfNeeded(absoluteIndex + 1, modelAfterUpdate),
|
|
139
|
+
maybeLatestModel: () => Option.some(modelAfterUpdate),
|
|
140
|
+
});
|
|
144
141
|
return nextState.entries.length > maxEntries
|
|
145
142
|
? evictOldestSegment(nextState)
|
|
146
143
|
: nextState;
|
|
@@ -161,16 +158,13 @@ export const createDevToolsStore = (bridge, options = {}) => Effect.gen(function
|
|
|
161
158
|
return state;
|
|
162
159
|
}
|
|
163
160
|
return Array.match(state.entries, {
|
|
164
|
-
onEmpty: () => ({
|
|
165
|
-
|
|
166
|
-
initMountStarts: Array.appendAll(state.initMountStarts, mountStarts),
|
|
161
|
+
onEmpty: () => evo(state, {
|
|
162
|
+
initMountStarts: Array.appendAll(mountStarts),
|
|
167
163
|
}),
|
|
168
|
-
onNonEmpty: entries => ({
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
mountStarts: Array.appendAll(last.mountStarts, mountStarts),
|
|
173
|
-
mountEnds: Array.appendAll(last.mountEnds, mountEnds),
|
|
164
|
+
onNonEmpty: entries => evo(state, {
|
|
165
|
+
entries: () => Array.modifyLastNonEmpty(entries, last => evo(last, {
|
|
166
|
+
mountStarts: Array.appendAll(mountStarts),
|
|
167
|
+
mountEnds: Array.appendAll(mountEnds),
|
|
174
168
|
})),
|
|
175
169
|
}),
|
|
176
170
|
});
|
|
@@ -194,30 +188,37 @@ export const createDevToolsStore = (bridge, options = {}) => Effect.gen(function
|
|
|
194
188
|
const jumpTo = (index) => Effect.gen(function* () {
|
|
195
189
|
const state = yield* SubscriptionRef.get(stateRef);
|
|
196
190
|
yield* bridge.render(resolveModel(state, index));
|
|
197
|
-
yield* SubscriptionRef.set(stateRef, {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
});
|
|
191
|
+
yield* SubscriptionRef.set(stateRef, evo(state, {
|
|
192
|
+
isPaused: () => true,
|
|
193
|
+
pausedAtIndex: () => index,
|
|
194
|
+
}));
|
|
202
195
|
});
|
|
203
196
|
const resume = Effect.gen(function* () {
|
|
204
|
-
yield* SubscriptionRef.update(stateRef, state => ({
|
|
205
|
-
|
|
206
|
-
isPaused: false,
|
|
197
|
+
yield* SubscriptionRef.update(stateRef, state => evo(state, {
|
|
198
|
+
isPaused: () => false,
|
|
207
199
|
}));
|
|
208
200
|
yield* bridge.markRenderPending;
|
|
209
201
|
});
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
202
|
+
// NOTE: the paused snapshot is replayed off the entries array, so wiping
|
|
203
|
+
// entries while paused strands the runtime on a historical state with no
|
|
204
|
+
// path back to live. Refuse the write until resume.
|
|
205
|
+
const clear = SubscriptionRef.update(stateRef, state => {
|
|
206
|
+
if (state.isPaused) {
|
|
207
|
+
return state;
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
return evo(state, {
|
|
211
|
+
entries: () => [],
|
|
212
|
+
startIndex: () => 0,
|
|
213
|
+
pausedAtIndex: () => 0,
|
|
214
|
+
keyframes: () => Option.match(state.maybeInitModel, {
|
|
215
|
+
onNone: () => HashMap.empty(),
|
|
216
|
+
onSome: model => HashMap.make([0, model]),
|
|
217
|
+
}),
|
|
218
|
+
maybeLatestModel: () => state.maybeInitModel,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
});
|
|
221
222
|
const getDiffAtIndex = (index) => Effect.gen(function* () {
|
|
222
223
|
if (index === INIT_INDEX) {
|
|
223
224
|
return emptyDiff;
|
|
@@ -20,6 +20,15 @@ type Hot = NonNullable<ImportMeta['hot']>;
|
|
|
20
20
|
* without author-side changes. When `maybeMessageSchema` is `None`, dispatch
|
|
21
21
|
* requests are rejected with an informative error.
|
|
22
22
|
*
|
|
23
|
+
* The bridge also derives a JSON Schema document from `maybeMessageSchema`
|
|
24
|
+
* once at boot (via `Schema.toJsonSchemaDocument`) to fulfill
|
|
25
|
+
* `RequestGetMessageSchema`, so MCP clients can discover the exact Message
|
|
26
|
+
* shapes the runtime accepts without reading the application source. A few
|
|
27
|
+
* AST nodes (symbol-keyed structs, symbol-indexed records, tuples with
|
|
28
|
+
* post-rest elements) cause `Schema.toJsonSchemaDocument` to throw; the
|
|
29
|
+
* derivation is guarded so a failure logs a warning and the schema-discovery
|
|
30
|
+
* tool returns `None` rather than crashing the bridge.
|
|
31
|
+
*
|
|
23
32
|
* Production-safe: callers must check `import.meta.hot` is defined before
|
|
24
33
|
* invoking this. The function assumes a live HMR connection.
|
|
25
34
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webSocketBridge.d.ts","sourceRoot":"","sources":["../../src/devTools/webSocketBridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,MAAM,EAIN,MAAM,EAEN,MAAM,IAAI,CAAC,EAGZ,MAAM,QAAQ,CAAA;
|
|
1
|
+
{"version":3,"file":"webSocketBridge.d.ts","sourceRoot":"","sources":["../../src/devTools/webSocketBridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,MAAM,EAIN,MAAM,EAEN,MAAM,IAAI,CAAC,EAGZ,MAAM,QAAQ,CAAA;AAwCf,OAAO,EAAE,KAAK,aAAa,EAAc,MAAM,YAAY,CAAA;AAQ3D,KAAK,GAAG,GAAG,WAAW,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAA;AA4BzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,oBAAoB,GAC/B,OAAO,aAAa,EACpB,KAAK,GAAG,EACR,UAAU,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EACnD,oBAAoB,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,KACnD,MAAM,CAAC,MAAM,CAAC,IAAI,CAsFjB,CAAA"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Array, Cause, Effect, Exit, HashMap, Match, Option, Order, Schema as S, SubscriptionRef, pipe, } from 'effect';
|
|
2
2
|
import { OptionExt } from '../effectExtensions/index.js';
|
|
3
|
-
import { EventConnected, EventDisconnected, EventFrame, KeyframeInfo, RequestFrame, ResponseDispatched, ResponseError, ResponseFrame, ResponseInit, ResponseKeyframes, ResponseMessage, ResponseMessages, ResponseModel, ResponseReplayed, ResponseResumed, ResponseRuntimeState, RuntimeInfo, } from './protocol.js';
|
|
3
|
+
import { EventConnected, EventDisconnected, EventFrame, KeyframeInfo, MessageSchemaDocumentResult, MessageSchemaIndexResult, RequestFrame, ResponseDispatched, ResponseError, ResponseFrame, ResponseInit, ResponseKeyframes, ResponseMessage, ResponseMessageSchema, ResponseMessages, ResponseModel, ResponseReplayed, ResponseResumed, ResponseRuntimeState, RuntimeInfo, } from './protocol.js';
|
|
4
|
+
import { diagnoseVariantPath, indexMessageSchemaDocument, narrowToVariant, splitVariantPath, } from './schemaSummarize.js';
|
|
4
5
|
import { toInspectableValue, toSerializedCommand, toSerializedEntry, toSerializedMount, } from './serialize.js';
|
|
5
6
|
import { INIT_INDEX } from './store.js';
|
|
6
7
|
import { formatPathNotFound, resolvePath, summarizeValue, } from './summarize.js';
|
|
@@ -9,6 +10,15 @@ const RESPONSE_CHANNEL = 'foldkit:devTools:response';
|
|
|
9
10
|
const EVENT_CHANNEL = 'foldkit:devTools:event';
|
|
10
11
|
const generateConnectionId = () => `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
11
12
|
const currentAbsoluteIndex = (entriesLength, startIndex) => (entriesLength === 0 ? INIT_INDEX : startIndex + entriesLength - 1);
|
|
13
|
+
const tryDeriveJsonSchemaDocument = (schema) => {
|
|
14
|
+
try {
|
|
15
|
+
return Option.some(S.toJsonSchemaDocument(schema));
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
console.warn('[foldkit:devTools] Failed to derive JSON Schema from Message Schema; foldkit_get_message_schema will return None.', error);
|
|
19
|
+
return Option.none();
|
|
20
|
+
}
|
|
21
|
+
};
|
|
12
22
|
/**
|
|
13
23
|
* Start the browser-side WebSocket bridge that exposes a Foldkit runtime's
|
|
14
24
|
* DevToolsStore to an external MCP server (via the Vite plugin relay).
|
|
@@ -28,6 +38,15 @@ const currentAbsoluteIndex = (entriesLength, startIndex) => (entriesLength === 0
|
|
|
28
38
|
* without author-side changes. When `maybeMessageSchema` is `None`, dispatch
|
|
29
39
|
* requests are rejected with an informative error.
|
|
30
40
|
*
|
|
41
|
+
* The bridge also derives a JSON Schema document from `maybeMessageSchema`
|
|
42
|
+
* once at boot (via `Schema.toJsonSchemaDocument`) to fulfill
|
|
43
|
+
* `RequestGetMessageSchema`, so MCP clients can discover the exact Message
|
|
44
|
+
* shapes the runtime accepts without reading the application source. A few
|
|
45
|
+
* AST nodes (symbol-keyed structs, symbol-indexed records, tuples with
|
|
46
|
+
* post-rest elements) cause `Schema.toJsonSchemaDocument` to throw; the
|
|
47
|
+
* derivation is guarded so a failure logs a warning and the schema-discovery
|
|
48
|
+
* tool returns `None` rather than crashing the bridge.
|
|
49
|
+
*
|
|
31
50
|
* Production-safe: callers must check `import.meta.hot` is defined before
|
|
32
51
|
* invoking this. The function assumes a live HMR connection.
|
|
33
52
|
*/
|
|
@@ -35,6 +54,7 @@ export const startWebSocketBridge = (store, hot, dispatch, maybeMessageSchema) =
|
|
|
35
54
|
const connectionId = generateConnectionId();
|
|
36
55
|
const capturedContext = yield* Effect.context();
|
|
37
56
|
const maybeDispatchSchema = Option.map(maybeMessageSchema, S.toCodecJson);
|
|
57
|
+
const maybeJsonSchemaDocument = Option.flatMap(maybeMessageSchema, tryDeriveJsonSchemaDocument);
|
|
38
58
|
const encodeEventFrame = S.encodeUnknownSync(EventFrame);
|
|
39
59
|
const encodeResponseFrame = S.encodeUnknownSync(ResponseFrame);
|
|
40
60
|
const sendEvent = (event) => {
|
|
@@ -54,7 +74,7 @@ export const startWebSocketBridge = (store, hot, dispatch, maybeMessageSchema) =
|
|
|
54
74
|
}),
|
|
55
75
|
}));
|
|
56
76
|
const handleRequest = (id, request) => Effect.gen(function* () {
|
|
57
|
-
const response = yield* dispatchRequest(store, dispatch, maybeDispatchSchema, request);
|
|
77
|
+
const response = yield* dispatchRequest(store, dispatch, maybeDispatchSchema, maybeJsonSchemaDocument, request);
|
|
58
78
|
sendResponse(id, response);
|
|
59
79
|
});
|
|
60
80
|
const handleRequestFrame = (frame) => {
|
|
@@ -99,7 +119,47 @@ const readModelResponse = (store, index, maybePath, expand) => Effect.gen(functi
|
|
|
99
119
|
}).pipe(Effect.catchCause(cause => Effect.succeed(ResponseError({
|
|
100
120
|
reason: `Failed to read Model at index ${index}: ${Cause.pretty(cause)}`,
|
|
101
121
|
}))));
|
|
102
|
-
const
|
|
122
|
+
const indexResponse = (document) => Option.match(indexMessageSchemaDocument(document), {
|
|
123
|
+
onNone: () => ResponseError({
|
|
124
|
+
reason: "Could not index Message Schema: the top-level shape is not a discriminated union of '_tag'-keyed structs. Open an issue if you see this against an Effect Schema released after foldkit's last sync.",
|
|
125
|
+
}),
|
|
126
|
+
onSome: variants => ResponseMessageSchema({
|
|
127
|
+
maybeResult: Option.some(MessageSchemaIndexResult({ index: { variants } })),
|
|
128
|
+
}),
|
|
129
|
+
});
|
|
130
|
+
const narrowResponse = (document, variantPath) => Option.match(narrowToVariant(document, variantPath), {
|
|
131
|
+
onNone: () => formatUnknownVariantError(document, variantPath),
|
|
132
|
+
onSome: narrowed => ResponseMessageSchema({
|
|
133
|
+
maybeResult: Option.some(MessageSchemaDocumentResult({ document: narrowed })),
|
|
134
|
+
}),
|
|
135
|
+
});
|
|
136
|
+
const buildMessageSchemaResponse = (maybeJsonSchemaDocument, maybeVariantTag) => Option.match(maybeJsonSchemaDocument, {
|
|
137
|
+
onNone: () => ResponseMessageSchema({ maybeResult: Option.none() }),
|
|
138
|
+
onSome: document => Option.match(maybeVariantTag, {
|
|
139
|
+
onNone: () => indexResponse(document),
|
|
140
|
+
onSome: variantTag => narrowResponse(document, variantTag),
|
|
141
|
+
}),
|
|
142
|
+
});
|
|
143
|
+
const formatUnknownVariantError = (document, variantPath) => {
|
|
144
|
+
const segments = splitVariantPath(variantPath);
|
|
145
|
+
return Option.match(diagnoseVariantPath(document, segments), {
|
|
146
|
+
onNone: () => ResponseError({
|
|
147
|
+
reason: `No Message variant at path '${variantPath}'. The runtime's Message Schema is not a discriminated union of '_tag'-keyed structs.`,
|
|
148
|
+
}),
|
|
149
|
+
onSome: ({ prefix, failingSegment, available }) => {
|
|
150
|
+
const prefixLabel = Array.isReadonlyArrayNonEmpty(prefix)
|
|
151
|
+
? prefix.join('.')
|
|
152
|
+
: '<top level>';
|
|
153
|
+
const failingIsKnownTag = Option.exists(failingSegment, tag => available.includes(tag));
|
|
154
|
+
const failingTag = Option.getOrElse(failingSegment, () => '');
|
|
155
|
+
const reason = failingIsKnownTag
|
|
156
|
+
? `No further structure to drill into at path '${variantPath}'. The variant '${failingTag}' at ${prefixLabel} does not carry exactly one tagged-union payload field, which is what the walker steps through. Idiomatic Foldkit Messages have at most one tagged-union field per variant (the 'message' field on Submodel wrappers, or a single value-type union); state surrounding a Submodel call belongs as an argument to the child's update/view, not as a sibling field on the parent Message.`
|
|
157
|
+
: `No Message variant at path '${variantPath}'. Available variants at ${prefixLabel}: ${available.join(', ')}.`;
|
|
158
|
+
return ResponseError({ reason });
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
const dispatchRequest = (store, dispatch, maybeDispatchSchema, maybeJsonSchemaDocument, request) => Match.value(request).pipe(Match.tagsExhaustive({
|
|
103
163
|
RequestGetModel: ({ maybePath, expand }) => Effect.gen(function* () {
|
|
104
164
|
const state = yield* SubscriptionRef.get(store.stateRef);
|
|
105
165
|
const index = currentAbsoluteIndex(state.entries.length, state.startIndex);
|
|
@@ -166,6 +226,7 @@ const dispatchRequest = (store, dispatch, maybeDispatchSchema, request) => Match
|
|
|
166
226
|
reason: `Invalid Message: ${error instanceof Error ? error.message : String(error)}\n\nReceived (typeof ${typeof message}): ${JSON.stringify(message)}`,
|
|
167
227
|
})))),
|
|
168
228
|
}),
|
|
229
|
+
RequestGetMessageSchema: ({ maybeVariantTag }) => Effect.succeed(buildMessageSchemaResponse(maybeJsonSchemaDocument, maybeVariantTag)),
|
|
169
230
|
RequestListRuntimes: () => Effect.succeed(ResponseError({
|
|
170
231
|
reason: 'RequestListRuntimes is plugin-handled and should not reach the runtime bridge',
|
|
171
232
|
})),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RoutingConfig } from './runtime.js';
|
|
2
2
|
export declare const addNavigationEventListeners: <Message>(dispatch: (message: Message) => void, routingConfig: RoutingConfig<Message>) => void;
|
|
3
|
+
export declare const addLinkClickListener: <Message>(dispatch: (message: Message) => void, routingConfig: RoutingConfig<Message>) => void;
|
|
3
4
|
export declare const addBfcacheRestoreListener: () => void;
|
|
4
5
|
//# sourceMappingURL=browserListeners.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browserListeners.d.ts","sourceRoot":"","sources":["../../src/runtime/browserListeners.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAE5C,eAAO,MAAM,2BAA2B,GAAI,OAAO,EACjD,UAAU,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,EACpC,eAAe,aAAa,CAAC,OAAO,CAAC,SAKtC,CAAA;
|
|
1
|
+
{"version":3,"file":"browserListeners.d.ts","sourceRoot":"","sources":["../../src/runtime/browserListeners.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAE5C,eAAO,MAAM,2BAA2B,GAAI,OAAO,EACjD,UAAU,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,EACpC,eAAe,aAAa,CAAC,OAAO,CAAC,SAKtC,CAAA;AAaD,eAAO,MAAM,oBAAoB,GAAI,OAAO,EAC1C,UAAU,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,EACpC,eAAe,aAAa,CAAC,OAAO,CAAC,SAoDtC,CAAA;AA4BD,eAAO,MAAM,yBAAyB,YASrC,CAAA"}
|
|
@@ -12,20 +12,32 @@ const addPopStateListener = (dispatch, routingConfig) => {
|
|
|
12
12
|
};
|
|
13
13
|
window.addEventListener('popstate', onPopState);
|
|
14
14
|
};
|
|
15
|
-
const addLinkClickListener = (dispatch, routingConfig) => {
|
|
15
|
+
export const addLinkClickListener = (dispatch, routingConfig) => {
|
|
16
16
|
const onLinkClick = (event) => {
|
|
17
|
-
const
|
|
18
|
-
|
|
17
|
+
const isNonPrimaryButton = event.button !== 0;
|
|
18
|
+
const isModifierKeyPressed = event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
|
|
19
|
+
const isDefaultPrevented = event.defaultPrevented;
|
|
20
|
+
if (isNonPrimaryButton || isModifierKeyPressed || isDefaultPrevented) {
|
|
19
21
|
return;
|
|
20
22
|
}
|
|
21
|
-
const
|
|
23
|
+
const eventTarget = event.target;
|
|
24
|
+
if (!(eventTarget instanceof Element)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const maybeLink = Option.fromNullishOr(eventTarget.closest('a'));
|
|
22
28
|
if (Option.isNone(maybeLink)) {
|
|
23
29
|
return;
|
|
24
30
|
}
|
|
25
|
-
const
|
|
31
|
+
const link = maybeLink.value;
|
|
32
|
+
const { href } = link;
|
|
26
33
|
if (String.isEmpty(href)) {
|
|
27
34
|
return;
|
|
28
35
|
}
|
|
36
|
+
const isNonSelfTarget = !String.isEmpty(link.target) && link.target !== '_self';
|
|
37
|
+
const isDownloadLink = link.hasAttribute('download');
|
|
38
|
+
if (isNonSelfTarget || isDownloadLink) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
29
41
|
event.preventDefault();
|
|
30
42
|
const linkUrl = new URL(href);
|
|
31
43
|
const currentUrl = new URL(window.location.href);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bubbling.d.ts","sourceRoot":"","sources":["../../../src/test/apps/bubbling.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAEhD,OAAO,EAAE,KAAK,IAAI,EAAQ,MAAM,qBAAqB,CAAA;AAKrD,eAAO,MAAM,KAAK;;;EAGhB,CAAA;AACF,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAA;AAOrC,QAAA,MAAM,OAAO,sLAAsD,CAAA;AACnE,KAAK,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAIlC,eAAO,MAAM,YAAY,EAAE,KAG1B,CAAA;AAID,eAAO,MAAM,MAAM,GACjB,OAAO,KAAK,EACZ,SAAS,OAAO,KACf,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,KAAK,CAAC,CAUrC,CAAA;
|
|
1
|
+
{"version":3,"file":"bubbling.d.ts","sourceRoot":"","sources":["../../../src/test/apps/bubbling.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAEhD,OAAO,EAAE,KAAK,IAAI,EAAQ,MAAM,qBAAqB,CAAA;AAKrD,eAAO,MAAM,KAAK;;;EAGhB,CAAA;AACF,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAA;AAOrC,QAAA,MAAM,OAAO,sLAAsD,CAAA;AACnE,KAAK,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAIlC,eAAO,MAAM,YAAY,EAAE,KAG1B,CAAA;AAID,eAAO,MAAM,MAAM,GACjB,OAAO,KAAK,EACZ,SAAS,OAAO,KACf,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,KAAK,CAAC,CAUrC,CAAA;AAIH,eAAO,MAAM,IAAI,GAAI,OAAO,KAAK,KAAG,IAgBnC,CAAA"}
|
|
@@ -24,8 +24,10 @@ export const update = (model, message) => M.value(message).pipe(M.withReturnType
|
|
|
24
24
|
],
|
|
25
25
|
}));
|
|
26
26
|
// VIEW
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
h.div([
|
|
30
|
-
|
|
31
|
-
])
|
|
27
|
+
export const view = (model) => {
|
|
28
|
+
const h = html();
|
|
29
|
+
return h.div([], [
|
|
30
|
+
h.div([h.Role('option'), h.OnClick(ClickedContainer())], [h.span([], [`clicks=${model.clicks}`])]),
|
|
31
|
+
h.div([h.Role('listitem'), h.OnDoubleClick(DoubleClickedContainer())], [h.span([], [`dbl=${model.doubleClicks}`])]),
|
|
32
|
+
]);
|
|
33
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"counter.d.ts","sourceRoot":"","sources":["../../../src/test/apps/counter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAc,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAExD,OAAO,KAAK,OAAO,MAAM,wBAAwB,CAAA;AACjD,OAAO,EAAE,KAAK,IAAI,EAAQ,MAAM,qBAAqB,CAAA;AAKrD,eAAO,MAAM,KAAK;;;EAGhB,CAAA;AACF,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAA;AAIrC,eAAO,MAAM,gBAAgB,8EAAwB,CAAA;AACrD,eAAO,MAAM,gBAAgB,8EAAwB,CAAA;AACrD,eAAO,MAAM,YAAY,0EAAoB,CAAA;AAC7C,eAAO,MAAM,gBAAgB;;EAA0C,CAAA;AACvE,eAAO,MAAM,mBAAmB,iFAA2B,CAAA;AAC3D,eAAO,MAAM,qBAAqB,mFAA6B,CAAA;AAC/D,eAAO,MAAM,mBAAmB,iFAA2B,CAAA;AAC3D,eAAO,MAAM,mBAAmB;;EAAgD,CAAA;AAChF,eAAO,MAAM,gBAAgB;;EAA6C,CAAA;AAE1E,eAAO,MAAM,OAAO;;;;;;IAUlB,CAAA;AACF,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAIzC,eAAO,MAAM,UAAU;;;iBAIgC,CAAA;AAEvD,eAAO,MAAM,cAAc;;;;;iBAKyC,CAAA;AAIpE,eAAO,MAAM,YAAY,EAAE,KAA6B,CAAA;AAIxD,eAAO,MAAM,MAAM,GACjB,OAAO,KAAK,EACZ,SAAS,OAAO,KACf,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAiCxD,CAAA;
|
|
1
|
+
{"version":3,"file":"counter.d.ts","sourceRoot":"","sources":["../../../src/test/apps/counter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAc,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAExD,OAAO,KAAK,OAAO,MAAM,wBAAwB,CAAA;AACjD,OAAO,EAAE,KAAK,IAAI,EAAQ,MAAM,qBAAqB,CAAA;AAKrD,eAAO,MAAM,KAAK;;;EAGhB,CAAA;AACF,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAA;AAIrC,eAAO,MAAM,gBAAgB,8EAAwB,CAAA;AACrD,eAAO,MAAM,gBAAgB,8EAAwB,CAAA;AACrD,eAAO,MAAM,YAAY,0EAAoB,CAAA;AAC7C,eAAO,MAAM,gBAAgB;;EAA0C,CAAA;AACvE,eAAO,MAAM,mBAAmB,iFAA2B,CAAA;AAC3D,eAAO,MAAM,qBAAqB,mFAA6B,CAAA;AAC/D,eAAO,MAAM,mBAAmB,iFAA2B,CAAA;AAC3D,eAAO,MAAM,mBAAmB;;EAAgD,CAAA;AAChF,eAAO,MAAM,gBAAgB;;EAA6C,CAAA;AAE1E,eAAO,MAAM,OAAO;;;;;;IAUlB,CAAA;AACF,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAIzC,eAAO,MAAM,UAAU;;;iBAIgC,CAAA;AAEvD,eAAO,MAAM,cAAc;;;;;iBAKyC,CAAA;AAIpE,eAAO,MAAM,YAAY,EAAE,KAA6B,CAAA;AAIxD,eAAO,MAAM,MAAM,GACjB,OAAO,KAAK,EACZ,SAAS,OAAO,KACf,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAiCxD,CAAA;AAIH,eAAO,MAAM,IAAI,GAAI,OAAO,KAAK,KAAG,IAiBnC,CAAA"}
|