mancha 0.7.3 → 0.8.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/README.md +27 -11
- package/dist/cli.js +1 -1
- package/dist/core.d.ts +2 -36
- package/dist/core.js +5 -83
- package/dist/css_gen_utils.js +2 -2
- package/dist/dome.d.ts +2 -0
- package/dist/dome.js +11 -0
- package/dist/mancha.js +1 -1
- package/dist/plugins.js +92 -91
- package/dist/store.d.ts +50 -0
- package/dist/store.js +182 -0
- package/package.json +2 -2
- package/yarn-error.log +2069 -1064
- package/dist/reactive.d.ts +0 -82
- package/dist/reactive.js +0 -255
package/dist/plugins.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { appendChild, attributeNameToCamelCase, cloneAttribute, createElement, firstElementChild, getAttribute, getNodeValue, insertBefore, removeAttribute, removeChild, replaceChildren, replaceWith, setAttribute, setNodeValue, setTextContent, traverse, } from "./dome.js";
|
|
1
|
+
import { appendChild, attributeNameToCamelCase, cloneAttribute, createElement, ellipsize, firstElementChild, getAttribute, getNodeValue, insertBefore, nodeToString, removeAttribute, removeChild, replaceChildren, replaceWith, setAttribute, setNodeValue, setTextContent, traverse, } from "./dome.js";
|
|
2
2
|
import { isRelativePath } from "./core.js";
|
|
3
3
|
import { Iterator } from "./iterator.js";
|
|
4
|
+
import { makeAsyncEvalFunction } from "./store.js";
|
|
4
5
|
const KW_ATTRIBUTES = new Set([
|
|
5
6
|
":bind",
|
|
6
7
|
":bind-events",
|
|
@@ -18,7 +19,7 @@ export var RendererPlugins;
|
|
|
18
19
|
// Early exit: node must be an <include> element.
|
|
19
20
|
if (elem.tagName?.toLocaleLowerCase() !== "include")
|
|
20
21
|
return;
|
|
21
|
-
this.log("<include> tag found in:\n", node);
|
|
22
|
+
this.log("<include> tag found in:\n", nodeToString(node, 128));
|
|
22
23
|
this.log("<include> params:", params);
|
|
23
24
|
// Early exit: <include> tags must have a src attribute.
|
|
24
25
|
const src = getAttribute(elem, "src");
|
|
@@ -129,7 +130,7 @@ export var RendererPlugins;
|
|
|
129
130
|
if (elem.tagName?.toLowerCase() === "template" && getAttribute(elem, "is")) {
|
|
130
131
|
const tagName = getAttribute(elem, "is")?.toLowerCase();
|
|
131
132
|
if (!this._customElements.has(tagName)) {
|
|
132
|
-
this.log(`Registering custom element: ${tagName}\n`, elem);
|
|
133
|
+
this.log(`Registering custom element: ${tagName}\n`, nodeToString(elem, 128));
|
|
133
134
|
this._customElements.set(tagName, elem.cloneNode(true));
|
|
134
135
|
// Remove the node from the DOM.
|
|
135
136
|
removeChild(elem.parentNode, elem);
|
|
@@ -140,7 +141,7 @@ export var RendererPlugins;
|
|
|
140
141
|
const elem = node;
|
|
141
142
|
const tagName = elem.tagName?.toLowerCase();
|
|
142
143
|
if (this._customElements.has(tagName)) {
|
|
143
|
-
this.log(`Processing custom element: ${tagName}\n`, elem);
|
|
144
|
+
this.log(`Processing custom element: ${tagName}\n`, nodeToString(elem, 128));
|
|
144
145
|
const template = this._customElements.get(tagName);
|
|
145
146
|
const clone = (template.content || template).cloneNode(true);
|
|
146
147
|
// Add whatever attributes the custom element tag had to the first child.
|
|
@@ -159,25 +160,23 @@ export var RendererPlugins;
|
|
|
159
160
|
}
|
|
160
161
|
};
|
|
161
162
|
RendererPlugins.resolveTextNodeExpressions = async function (node, params) {
|
|
162
|
-
if (node.nodeType !== 3)
|
|
163
|
-
return;
|
|
164
163
|
const content = getNodeValue(node) || "";
|
|
165
|
-
|
|
164
|
+
if (node.nodeType !== 3 || !content?.trim())
|
|
165
|
+
return;
|
|
166
|
+
this.log(`Processing node content value:\n`, ellipsize(content, 128));
|
|
166
167
|
// Identify all the expressions found in the content.
|
|
167
168
|
const matcher = new RegExp(/{{ ([^}]+) }}/gm);
|
|
168
169
|
const expressions = Array.from(content.matchAll(matcher)).map((match) => match[1]);
|
|
169
170
|
// To update the node, we have to re-evaluate all of the expressions since that's much simpler
|
|
170
171
|
// than caching results.
|
|
171
|
-
|
|
172
|
+
return this.effect(function () {
|
|
172
173
|
let updatedContent = content;
|
|
173
174
|
for (const expr of expressions) {
|
|
174
|
-
const
|
|
175
|
+
const result = this.eval(expr, { $elem: node });
|
|
175
176
|
updatedContent = updatedContent.replace(`{{ ${expr} }}`, String(result));
|
|
176
177
|
}
|
|
177
178
|
setNodeValue(node, updatedContent);
|
|
178
|
-
};
|
|
179
|
-
// Trigger the eval and pass our full node update function as the callback.
|
|
180
|
-
await Promise.all(expressions.map((expr) => this.watchExpr(expr, { $elem: node }, updateNode)));
|
|
179
|
+
});
|
|
181
180
|
};
|
|
182
181
|
RendererPlugins.resolveDataAttribute = async function (node, params) {
|
|
183
182
|
if (this._skipNodes.has(node))
|
|
@@ -185,26 +184,24 @@ export var RendererPlugins;
|
|
|
185
184
|
const elem = node;
|
|
186
185
|
const dataAttr = getAttribute(elem, ":data");
|
|
187
186
|
if (dataAttr) {
|
|
188
|
-
this.log(":data attribute found in:\n", node);
|
|
187
|
+
this.log(":data attribute found in:\n", nodeToString(node, 128));
|
|
189
188
|
// Remove the attribute from the node.
|
|
190
189
|
removeAttribute(elem, ":data");
|
|
191
190
|
// Create a subrenderer and process the tag, unless it's the root node.
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
await subrenderer.update(result);
|
|
201
|
-
// Skip all the children of the current node.
|
|
191
|
+
const subrenderer = params?.rootNode === node ? this : this.clone();
|
|
192
|
+
node.renderer = subrenderer;
|
|
193
|
+
// Do not call eval() directly, we will use an async version instead.
|
|
194
|
+
const fn = makeAsyncEvalFunction(dataAttr, this.evalkeys);
|
|
195
|
+
const result = await fn.call(subrenderer.$, { $elem: node });
|
|
196
|
+
await Promise.all(Object.entries(result).map(([key, value]) => subrenderer.set(key, value)));
|
|
197
|
+
// Skip all the children of the current node, if it's a subrenderer.
|
|
198
|
+
if (subrenderer !== this) {
|
|
202
199
|
for (const child of traverse(node, this._skipNodes)) {
|
|
203
200
|
this._skipNodes.add(child);
|
|
204
201
|
}
|
|
205
|
-
// Mount the current node with the subrenderer.
|
|
206
|
-
await subrenderer.mount(node, params);
|
|
207
202
|
}
|
|
203
|
+
// Mount the current node with the subrenderer.
|
|
204
|
+
await subrenderer.mount(node, params);
|
|
208
205
|
}
|
|
209
206
|
};
|
|
210
207
|
RendererPlugins.resolveWatchAttribute = async function (node, params) {
|
|
@@ -213,11 +210,13 @@ export var RendererPlugins;
|
|
|
213
210
|
const elem = node;
|
|
214
211
|
const watchAttr = getAttribute(elem, "@watch");
|
|
215
212
|
if (watchAttr) {
|
|
216
|
-
this.log("@watch attribute found in:\n", node);
|
|
213
|
+
this.log("@watch attribute found in:\n", nodeToString(node, 128));
|
|
217
214
|
// Remove the attribute from the node.
|
|
218
215
|
removeAttribute(elem, "@watch");
|
|
219
216
|
// Compute the function's result.
|
|
220
|
-
await this.
|
|
217
|
+
await this.effect(function () {
|
|
218
|
+
return this.eval(watchAttr, { $elem: node });
|
|
219
|
+
});
|
|
221
220
|
}
|
|
222
221
|
};
|
|
223
222
|
RendererPlugins.resolveTextAttributes = async function (node, params) {
|
|
@@ -226,11 +225,13 @@ export var RendererPlugins;
|
|
|
226
225
|
const elem = node;
|
|
227
226
|
const textAttr = getAttribute(elem, "$text");
|
|
228
227
|
if (textAttr) {
|
|
229
|
-
this.log("$text attribute found in:\n", node);
|
|
228
|
+
this.log("$text attribute found in:\n", nodeToString(node, 128));
|
|
230
229
|
// Remove the attribute from the node.
|
|
231
230
|
removeAttribute(elem, "$text");
|
|
232
231
|
// Compute the function's result and track dependencies.
|
|
233
|
-
|
|
232
|
+
return this.effect(function () {
|
|
233
|
+
setTextContent(node, this.eval(textAttr, { $elem: node }));
|
|
234
|
+
});
|
|
234
235
|
}
|
|
235
236
|
};
|
|
236
237
|
RendererPlugins.resolveHtmlAttribute = async function (node, params) {
|
|
@@ -239,16 +240,18 @@ export var RendererPlugins;
|
|
|
239
240
|
const elem = node;
|
|
240
241
|
const htmlAttr = getAttribute(elem, "$html");
|
|
241
242
|
if (htmlAttr) {
|
|
242
|
-
this.log("$html attribute found in:\n", node);
|
|
243
|
+
this.log("$html attribute found in:\n", nodeToString(node, 128));
|
|
243
244
|
// Remove the attribute from the node.
|
|
244
245
|
removeAttribute(elem, "$html");
|
|
245
|
-
// Obtain a subrenderer for the node contents.
|
|
246
|
-
const subrenderer = this.clone();
|
|
247
246
|
// Compute the function's result and track dependencies.
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
247
|
+
return this.effect(function () {
|
|
248
|
+
const result = this.eval(htmlAttr, { $elem: node });
|
|
249
|
+
return new Promise(async (resolve) => {
|
|
250
|
+
const fragment = await this.preprocessString(result, params);
|
|
251
|
+
await this.renderNode(fragment);
|
|
252
|
+
replaceChildren(elem, fragment);
|
|
253
|
+
resolve();
|
|
254
|
+
});
|
|
252
255
|
});
|
|
253
256
|
}
|
|
254
257
|
};
|
|
@@ -258,12 +261,14 @@ export var RendererPlugins;
|
|
|
258
261
|
const elem = node;
|
|
259
262
|
for (const attr of Array.from(elem.attributes || [])) {
|
|
260
263
|
if (attr.name.startsWith("$") && !KW_ATTRIBUTES.has(attr.name)) {
|
|
261
|
-
this.log(attr.name, "attribute found in:\n", node);
|
|
264
|
+
this.log(attr.name, "attribute found in:\n", nodeToString(node, 128));
|
|
262
265
|
// Remove the attribute from the node.
|
|
263
266
|
removeAttribute(elem, attr.name);
|
|
264
267
|
// Compute the function's result and track dependencies.
|
|
265
268
|
const propName = attributeNameToCamelCase(attr.name.slice(1));
|
|
266
|
-
await this.
|
|
269
|
+
await this.effect(function () {
|
|
270
|
+
node[propName] = this.eval(attr.value, { $elem: node });
|
|
271
|
+
});
|
|
267
272
|
}
|
|
268
273
|
}
|
|
269
274
|
};
|
|
@@ -273,12 +278,14 @@ export var RendererPlugins;
|
|
|
273
278
|
const elem = node;
|
|
274
279
|
for (const attr of Array.from(elem.attributes || [])) {
|
|
275
280
|
if (attr.name.startsWith(":") && !KW_ATTRIBUTES.has(attr.name)) {
|
|
276
|
-
this.log(attr.name, "attribute found in:\n", node);
|
|
281
|
+
this.log(attr.name, "attribute found in:\n", nodeToString(node, 128));
|
|
277
282
|
// Remove the processed attributes from node.
|
|
278
283
|
removeAttribute(elem, attr.name);
|
|
279
284
|
// Compute the function's result and track dependencies.
|
|
280
285
|
const attrName = attr.name.slice(1);
|
|
281
|
-
|
|
286
|
+
this.effect(function () {
|
|
287
|
+
setAttribute(elem, attrName, this.eval(attr.value, { $elem: node }));
|
|
288
|
+
});
|
|
282
289
|
}
|
|
283
290
|
}
|
|
284
291
|
};
|
|
@@ -288,7 +295,7 @@ export var RendererPlugins;
|
|
|
288
295
|
const elem = node;
|
|
289
296
|
for (const attr of Array.from(elem.attributes || [])) {
|
|
290
297
|
if (attr.name.startsWith("@") && !KW_ATTRIBUTES.has(attr.name)) {
|
|
291
|
-
this.log(attr.name, "attribute found in:\n", node);
|
|
298
|
+
this.log(attr.name, "attribute found in:\n", nodeToString(node, 128));
|
|
292
299
|
// Remove the processed attributes from node.
|
|
293
300
|
removeAttribute(elem, attr.name);
|
|
294
301
|
node.addEventListener?.(attr.name.substring(1), (event) => {
|
|
@@ -303,7 +310,7 @@ export var RendererPlugins;
|
|
|
303
310
|
const elem = node;
|
|
304
311
|
const forAttr = getAttribute(elem, ":for")?.trim();
|
|
305
312
|
if (forAttr) {
|
|
306
|
-
this.log(":for attribute found in:\n", node);
|
|
313
|
+
this.log(":for attribute found in:\n", nodeToString(node, 128));
|
|
307
314
|
// Remove the processed attributes from node.
|
|
308
315
|
removeAttribute(elem, ":for");
|
|
309
316
|
// Ensure the node and its children are not processed by subsequent steps.
|
|
@@ -316,7 +323,7 @@ export var RendererPlugins;
|
|
|
316
323
|
insertBefore(parent, template, node);
|
|
317
324
|
removeChild(parent, node);
|
|
318
325
|
appendChild(template, node);
|
|
319
|
-
this.log(":for template:\n", template);
|
|
326
|
+
this.log(":for template:\n", nodeToString(template, 128));
|
|
320
327
|
// Tokenize the input by splitting it based on the format "{key} in {expression}".
|
|
321
328
|
const tokens = forAttr.split(" in ", 2);
|
|
322
329
|
if (tokens.length !== 2) {
|
|
@@ -326,51 +333,41 @@ export var RendererPlugins;
|
|
|
326
333
|
const children = [];
|
|
327
334
|
// Compute the container expression and track dependencies.
|
|
328
335
|
const [loopKey, itemsExpr] = tokens;
|
|
329
|
-
await this.
|
|
336
|
+
await this.effect(function () {
|
|
337
|
+
const items = this.eval(itemsExpr, { $elem: node });
|
|
330
338
|
this.log(":for list items:", items);
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
//
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
364
|
-
// Release the lock.
|
|
365
|
-
resolve();
|
|
366
|
-
}))
|
|
367
|
-
.catch((exc) => {
|
|
368
|
-
console.error(exc);
|
|
369
|
-
throw new Error(exc);
|
|
370
|
-
})
|
|
371
|
-
.then();
|
|
372
|
-
// Return the lock so the whole operation can be awaited.
|
|
373
|
-
return this.lock;
|
|
339
|
+
// Remove all the previously added children, if any.
|
|
340
|
+
children.splice(0, children.length).forEach((child) => {
|
|
341
|
+
removeChild(parent, child);
|
|
342
|
+
this._skipNodes.delete(child);
|
|
343
|
+
});
|
|
344
|
+
// Validate that the expression returns a list of items.
|
|
345
|
+
if (!Array.isArray(items)) {
|
|
346
|
+
console.error(`Expression did not yield a list: \`${itemsExpr}\` => \`${items}\``);
|
|
347
|
+
return Promise.resolve();
|
|
348
|
+
}
|
|
349
|
+
// Loop through the container items.
|
|
350
|
+
const awaiters = [];
|
|
351
|
+
for (const item of items) {
|
|
352
|
+
// Create a subrenderer that will hold the loop item and all node descendants.
|
|
353
|
+
const subrenderer = this.clone();
|
|
354
|
+
subrenderer.set(loopKey, item);
|
|
355
|
+
// Create a new HTML element for each item and add them to parent node.
|
|
356
|
+
const copy = node.cloneNode(true);
|
|
357
|
+
// Also add the new element to the store.
|
|
358
|
+
children.push(copy);
|
|
359
|
+
// Since the element will be handled by a subrenderer, skip it in parent renderer.
|
|
360
|
+
this._skipNodes.add(copy);
|
|
361
|
+
// Render the element using the subrenderer.
|
|
362
|
+
awaiters.push(subrenderer.mount(copy, params));
|
|
363
|
+
this.log("Rendered list child:\n", nodeToString(copy, 128));
|
|
364
|
+
}
|
|
365
|
+
// Insert the new children into the parent container.
|
|
366
|
+
const reference = template.nextSibling;
|
|
367
|
+
for (const child of children) {
|
|
368
|
+
insertBefore(parent, child, reference);
|
|
369
|
+
}
|
|
370
|
+
return Promise.all(awaiters);
|
|
374
371
|
});
|
|
375
372
|
}
|
|
376
373
|
};
|
|
@@ -380,7 +377,7 @@ export var RendererPlugins;
|
|
|
380
377
|
const elem = node;
|
|
381
378
|
const bindExpr = getAttribute(elem, ":bind");
|
|
382
379
|
if (bindExpr) {
|
|
383
|
-
this.log(":bind attribute found in:\n", node);
|
|
380
|
+
this.log(":bind attribute found in:\n", nodeToString(node, 128));
|
|
384
381
|
// The change events we listen for can be overriden by user.
|
|
385
382
|
const defaultEvents = ["change", "input"];
|
|
386
383
|
const updateEvents = getAttribute(elem, ":bind-events")?.split(",") || defaultEvents;
|
|
@@ -391,7 +388,10 @@ export var RendererPlugins;
|
|
|
391
388
|
const prop = getAttribute(elem, "type") === "checkbox" ? "checked" : "value";
|
|
392
389
|
// Watch for updates in the store and bind our property ==> node value.
|
|
393
390
|
const propExpr = `$elem.${prop} = ${bindExpr}`;
|
|
394
|
-
|
|
391
|
+
this.effect(function () {
|
|
392
|
+
const result = this.eval(propExpr, { $elem: node });
|
|
393
|
+
elem[prop] = result;
|
|
394
|
+
});
|
|
395
395
|
// Bind node value ==> our property.
|
|
396
396
|
const nodeExpr = `${bindExpr} = $elem.${prop}`;
|
|
397
397
|
// Watch for updates in the node's value.
|
|
@@ -406,7 +406,7 @@ export var RendererPlugins;
|
|
|
406
406
|
const elem = node;
|
|
407
407
|
const showExpr = getAttribute(elem, ":show");
|
|
408
408
|
if (showExpr) {
|
|
409
|
-
this.log(":show attribute found in:\n", node);
|
|
409
|
+
this.log(":show attribute found in:\n", nodeToString(node, 128));
|
|
410
410
|
// Remove the processed attributes from node.
|
|
411
411
|
removeAttribute(elem, ":show");
|
|
412
412
|
// TODO: Instead of using element display, insert a dummy <template> to track position of
|
|
@@ -422,7 +422,8 @@ export var RendererPlugins;
|
|
|
422
422
|
?.at(1)
|
|
423
423
|
?.trim();
|
|
424
424
|
// Compute the function's result and track dependencies.
|
|
425
|
-
|
|
425
|
+
this.effect(function () {
|
|
426
|
+
const result = this.eval(showExpr, { $elem: node });
|
|
426
427
|
// If the result is false, set the node's display to none.
|
|
427
428
|
if (elem.style)
|
|
428
429
|
elem.style.display = result ? display : "none";
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
type SignalStoreProxy = SignalStore & {
|
|
2
|
+
[key: string]: any;
|
|
3
|
+
};
|
|
4
|
+
type Observer<T> = (this: SignalStoreProxy) => T;
|
|
5
|
+
declare abstract class IDebouncer {
|
|
6
|
+
timeouts: Map<Function, ReturnType<typeof setTimeout>>;
|
|
7
|
+
debounce<T>(millis: number, callback: () => T | Promise<T>): Promise<T>;
|
|
8
|
+
}
|
|
9
|
+
/** Default debouncer time in millis. */
|
|
10
|
+
export declare const REACTIVE_DEBOUNCE_MILLIS = 10;
|
|
11
|
+
/**
|
|
12
|
+
* Creates an evaluation function based on the provided code and arguments.
|
|
13
|
+
* @param code The code to be evaluated.
|
|
14
|
+
* @param args The arguments to be passed to the evaluation function. Default is an empty array.
|
|
15
|
+
* @returns The evaluation function.
|
|
16
|
+
*/
|
|
17
|
+
export declare function makeEvalFunction(code: string, args?: string[]): Function;
|
|
18
|
+
export declare function makeAsyncEvalFunction(code: string, args?: string[]): Function;
|
|
19
|
+
export declare class SignalStore extends IDebouncer {
|
|
20
|
+
protected readonly evalkeys: string[];
|
|
21
|
+
protected readonly expressionCache: Map<string, Function>;
|
|
22
|
+
protected readonly store: Map<string, unknown>;
|
|
23
|
+
protected readonly observers: Map<string, Set<Observer<unknown>>>;
|
|
24
|
+
protected _observer: Observer<unknown> | null;
|
|
25
|
+
_lock: Promise<void>;
|
|
26
|
+
constructor(data?: {
|
|
27
|
+
[key: string]: any;
|
|
28
|
+
});
|
|
29
|
+
private wrapFunction;
|
|
30
|
+
private wrapObject;
|
|
31
|
+
private watch;
|
|
32
|
+
private notify;
|
|
33
|
+
get<T>(key: string, observer?: Observer<T>): unknown | null;
|
|
34
|
+
set(key: string, value: unknown): Promise<void>;
|
|
35
|
+
del(key: string): void;
|
|
36
|
+
has(key: string): boolean;
|
|
37
|
+
effect<T>(observer: Observer<T>): T;
|
|
38
|
+
private proxify;
|
|
39
|
+
get $(): SignalStoreProxy;
|
|
40
|
+
/**
|
|
41
|
+
* Retrieves or creates a cached expression function based on the provided expression.
|
|
42
|
+
* @param expr - The expression to retrieve or create a cached function for.
|
|
43
|
+
* @returns The cached expression function.
|
|
44
|
+
*/
|
|
45
|
+
private cachedExpressionFunction;
|
|
46
|
+
eval(expr: string, args?: {
|
|
47
|
+
[key: string]: any;
|
|
48
|
+
}): unknown;
|
|
49
|
+
}
|
|
50
|
+
export {};
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
class IDebouncer {
|
|
2
|
+
timeouts = new Map();
|
|
3
|
+
debounce(millis, callback) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const timeout = this.timeouts.get(callback);
|
|
6
|
+
if (timeout)
|
|
7
|
+
clearTimeout(timeout);
|
|
8
|
+
this.timeouts.set(callback, setTimeout(() => {
|
|
9
|
+
try {
|
|
10
|
+
resolve(callback());
|
|
11
|
+
this.timeouts.delete(callback);
|
|
12
|
+
}
|
|
13
|
+
catch (exc) {
|
|
14
|
+
reject(exc);
|
|
15
|
+
}
|
|
16
|
+
}, millis));
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Default debouncer time in millis. */
|
|
21
|
+
export const REACTIVE_DEBOUNCE_MILLIS = 10;
|
|
22
|
+
/**
|
|
23
|
+
* Creates an evaluation function based on the provided code and arguments.
|
|
24
|
+
* @param code The code to be evaluated.
|
|
25
|
+
* @param args The arguments to be passed to the evaluation function. Default is an empty array.
|
|
26
|
+
* @returns The evaluation function.
|
|
27
|
+
*/
|
|
28
|
+
export function makeEvalFunction(code, args = []) {
|
|
29
|
+
return new Function(...args, `with (this) { return (${code}); }`);
|
|
30
|
+
}
|
|
31
|
+
export function makeAsyncEvalFunction(code, args = []) {
|
|
32
|
+
return new Function(...args, `with (this) { return (async () => (${code}))(); }`);
|
|
33
|
+
}
|
|
34
|
+
function isProxified(object) {
|
|
35
|
+
return object instanceof SignalStore || object["__is_proxy__"];
|
|
36
|
+
}
|
|
37
|
+
export class SignalStore extends IDebouncer {
|
|
38
|
+
evalkeys = ["$elem", "$event"];
|
|
39
|
+
expressionCache = new Map();
|
|
40
|
+
store = new Map();
|
|
41
|
+
observers = new Map();
|
|
42
|
+
_observer = null;
|
|
43
|
+
_lock = Promise.resolve();
|
|
44
|
+
constructor(data) {
|
|
45
|
+
super();
|
|
46
|
+
for (let [key, value] of Object.entries(data || {})) {
|
|
47
|
+
// Use our set method to ensure that callbacks and wrappers are appropriately set, but ignore
|
|
48
|
+
// the return value since we know that no observers will be triggered.
|
|
49
|
+
this.set(key, value);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
wrapFunction(fn) {
|
|
53
|
+
return (...args) => fn.call(this.$, ...args);
|
|
54
|
+
}
|
|
55
|
+
wrapObject(obj, callback) {
|
|
56
|
+
// If this object is already a proxy or not a plain object (or array), return it as-is.
|
|
57
|
+
if (obj == null || isProxified(obj) || (obj.constructor !== Object && !Array.isArray(obj))) {
|
|
58
|
+
return obj;
|
|
59
|
+
}
|
|
60
|
+
return new Proxy(obj, {
|
|
61
|
+
deleteProperty: (target, property) => {
|
|
62
|
+
if (property in target) {
|
|
63
|
+
delete target[property];
|
|
64
|
+
callback();
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
set: (target, prop, value, receiver) => {
|
|
72
|
+
if (typeof value === "object" && obj != null)
|
|
73
|
+
value = this.wrapObject(value, callback);
|
|
74
|
+
const ret = Reflect.set(target, prop, value, receiver);
|
|
75
|
+
callback();
|
|
76
|
+
return ret;
|
|
77
|
+
},
|
|
78
|
+
get: (target, prop, receiver) => {
|
|
79
|
+
if (prop === "__is_proxy__")
|
|
80
|
+
return true;
|
|
81
|
+
return Reflect.get(target, prop, receiver);
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
watch(key, observer) {
|
|
86
|
+
if (!this.observers.has(key)) {
|
|
87
|
+
this.observers.set(key, new Set());
|
|
88
|
+
}
|
|
89
|
+
if (!this.observers.get(key)?.has(observer)) {
|
|
90
|
+
this.observers.get(key)?.add(observer);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async notify(key) {
|
|
94
|
+
const observers = Array.from(this.observers.get(key) || []);
|
|
95
|
+
await this.debounce(REACTIVE_DEBOUNCE_MILLIS, () => Promise.all(observers.map((observer) => observer.call(this.proxify(observer)))));
|
|
96
|
+
}
|
|
97
|
+
get(key, observer) {
|
|
98
|
+
if (observer)
|
|
99
|
+
this.watch(key, observer);
|
|
100
|
+
return this.store.get(key);
|
|
101
|
+
}
|
|
102
|
+
async set(key, value) {
|
|
103
|
+
if (value === this.store.get(key))
|
|
104
|
+
return;
|
|
105
|
+
const callback = () => this.notify(key);
|
|
106
|
+
if (value && typeof value === "function") {
|
|
107
|
+
value = this.wrapFunction(value);
|
|
108
|
+
}
|
|
109
|
+
if (value && typeof value === "object") {
|
|
110
|
+
value = this.wrapObject(value, callback);
|
|
111
|
+
}
|
|
112
|
+
this.store.set(key, value);
|
|
113
|
+
await callback();
|
|
114
|
+
}
|
|
115
|
+
del(key) {
|
|
116
|
+
this.store.delete(key);
|
|
117
|
+
this.observers.delete(key);
|
|
118
|
+
}
|
|
119
|
+
has(key) {
|
|
120
|
+
return this.store.has(key);
|
|
121
|
+
}
|
|
122
|
+
effect(observer) {
|
|
123
|
+
return observer.call(this.proxify(observer));
|
|
124
|
+
}
|
|
125
|
+
proxify(observer) {
|
|
126
|
+
const keys = Array.from(this.store.entries()).map(([key]) => key);
|
|
127
|
+
const keyval = Object.fromEntries(keys.map((key) => [key, undefined]));
|
|
128
|
+
return new Proxy(keyval, {
|
|
129
|
+
get: (_, prop, receiver) => {
|
|
130
|
+
if (typeof prop === "string" && this.store.has(prop)) {
|
|
131
|
+
return this.get(prop, observer);
|
|
132
|
+
}
|
|
133
|
+
else if (prop === "$") {
|
|
134
|
+
return this.proxify(observer);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
return Reflect.get(this, prop, receiver);
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
set: (_, prop, value, receiver) => {
|
|
141
|
+
if (typeof prop !== "string" || prop in this) {
|
|
142
|
+
Reflect.set(this, prop, value, receiver);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
this.set(prop, value);
|
|
146
|
+
}
|
|
147
|
+
return true;
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
get $() {
|
|
152
|
+
return this.proxify();
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Retrieves or creates a cached expression function based on the provided expression.
|
|
156
|
+
* @param expr - The expression to retrieve or create a cached function for.
|
|
157
|
+
* @returns The cached expression function.
|
|
158
|
+
*/
|
|
159
|
+
cachedExpressionFunction(expr) {
|
|
160
|
+
if (!this.expressionCache.has(expr)) {
|
|
161
|
+
this.expressionCache.set(expr, makeEvalFunction(expr, this.evalkeys));
|
|
162
|
+
}
|
|
163
|
+
return this.expressionCache.get(expr);
|
|
164
|
+
}
|
|
165
|
+
eval(expr, args = {}) {
|
|
166
|
+
// Determine whether we have already been proxified to avoid doing it again.
|
|
167
|
+
const thisArg = this._observer ? this : this.$;
|
|
168
|
+
if (this.store.has(expr)) {
|
|
169
|
+
// Shortcut: if the expression is just an item from the value store, use that directly.
|
|
170
|
+
return thisArg[expr];
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
// Otherwise, perform the expression evaluation.
|
|
174
|
+
const fn = this.cachedExpressionFunction(expr);
|
|
175
|
+
const argvals = this.evalkeys.map((key) => args[key]);
|
|
176
|
+
if (Object.keys(args).some((key) => !this.evalkeys.includes(key))) {
|
|
177
|
+
throw new Error(`Invalid argument key, must be one of: ${this.evalkeys.join(", ")}`);
|
|
178
|
+
}
|
|
179
|
+
return fn.call(thisArg, ...argvals);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
package/package.json
CHANGED