@async/framework 0.3.0 → 0.5.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/src/loader.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { renderComponent } from "./component.js";
2
2
  import { createHandlerRegistry } from "./handlers.js";
3
- import { createSignalRegistry } from "./signals.js";
3
+ import { createSignalRegistry, isSignalRef } from "./signals.js";
4
4
  import { matchAttribute, normalizeAttributeConfig, readAttribute } from "./attributes.js";
5
5
 
6
+ const inlineBindingPrefix = "__async:inline:";
7
+
6
8
  export function AsyncLoader({ root, signals, handlers, server, router, cache, attributes } = {}) {
7
9
  const documentRef = root?.ownerDocument ?? root ?? globalThis.document;
8
10
  const rootNode = root ?? documentRef;
@@ -16,6 +18,9 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
16
18
  const visibleElements = new WeakSet();
17
19
  const boundaryState = new WeakMap();
18
20
  const renderingBoundaries = new WeakSet();
21
+ const inlineBindings = new Map();
22
+ const scopedCleanups = new WeakMap();
23
+ let inlineBindingCounter = 0;
19
24
  let destroyed = false;
20
25
 
21
26
  const api = {
@@ -36,6 +41,7 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
36
41
  scan(rootOrFragment = rootNode) {
37
42
  assertActive();
38
43
  bindSignalAttributes(rootOrFragment);
44
+ bindClassAttributes(rootOrFragment);
39
45
  bindEventAttributes(rootOrFragment);
40
46
  bindBoundaries(rootOrFragment);
41
47
  runPseudoEvents(rootOrFragment);
@@ -48,6 +54,7 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
48
54
  if (!boundary) {
49
55
  throw new Error(`Boundary "${boundaryId}" was not found.`);
50
56
  }
57
+ cleanupChildren(boundary);
51
58
  boundary.replaceChildren(toFragment(fragmentOrTemplate, documentRef));
52
59
  api.scan(boundary);
53
60
  return boundary;
@@ -64,11 +71,12 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
64
71
  cache: api.cache,
65
72
  attributes: attributeConfig
66
73
  });
74
+ cleanupChildren(target);
67
75
  target.replaceChildren(toFragment(rendered.html, target.ownerDocument));
68
76
  api.scan(target);
69
77
  rendered.mount(target);
70
78
  rendered.visible(target, api._observeVisible);
71
- cleanups.add(rendered.cleanup);
79
+ addCleanup(rendered.cleanup, target, "children");
72
80
  return rendered;
73
81
  },
74
82
 
@@ -78,17 +86,34 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
78
86
  }
79
87
  destroyed = true;
80
88
  for (const cleanup of [...cleanups]) {
81
- cleanup();
89
+ runCleanup(cleanup);
82
90
  }
83
91
  cleanups.clear();
84
92
  },
85
93
 
86
94
  _observeVisible(target, fn) {
87
95
  return observeVisible(target, fn);
96
+ },
97
+
98
+ _registerBinding(value) {
99
+ const id = `${inlineBindingPrefix}${++inlineBindingCounter}`;
100
+ inlineBindings.set(id, value);
101
+ return id;
102
+ },
103
+
104
+ _releaseBinding(id) {
105
+ inlineBindings.delete(id);
88
106
  }
89
107
  };
90
108
 
91
109
  signalRegistry._setContext?.({ server: api.server, router: api.router, loader: api, cache: api.cache });
110
+ api.server?._setContext?.({
111
+ signals: signalRegistry,
112
+ handlers: handlerRegistry,
113
+ loader: api,
114
+ router: api.router,
115
+ cache: api.cache
116
+ });
92
117
 
93
118
  function bindEventAttributes(scope) {
94
119
  for (const element of elementsIn(scope)) {
@@ -100,7 +125,7 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
100
125
  if (!eventName) {
101
126
  continue;
102
127
  }
103
- if (eventName === "mount" || eventName === "visible") {
128
+ if (eventName === "attach" || eventName === "mount" || eventName === "visible") {
104
129
  continue;
105
130
  }
106
131
  bindEvent(element, eventName, element.getAttribute(name));
@@ -137,7 +162,7 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
137
162
  };
138
163
 
139
164
  element.addEventListener(eventName, listener);
140
- cleanups.add(() => element.removeEventListener(eventName, listener));
165
+ addCleanup(() => element.removeEventListener(eventName, listener), element);
141
166
  }
142
167
 
143
168
  function bindSignalAttributes(scope) {
@@ -172,18 +197,71 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
172
197
  bindSignal(element, `attr:${attr}:${path}`, path, (value) => updateAttribute(element, attr, value));
173
198
  continue;
174
199
  }
200
+ if (signalName.startsWith("prop:")) {
201
+ const prop = signalName.slice("prop:".length);
202
+ const path = element.getAttribute(name);
203
+ bindSignal(element, `prop:${prop}:${path}`, path, (value) => updateProperty(element, prop, value));
204
+ continue;
205
+ }
175
206
  if (signalName.startsWith("class:")) {
176
207
  const className = signalName.slice("class:".length);
177
208
  const path = element.getAttribute(name);
178
- bindSignal(element, `class:${className}:${path}`, path, (value) => {
179
- element.classList.toggle(className, Boolean(value));
180
- });
209
+ if (className === "" || className === "{}") {
210
+ bindClass(element, className, path);
211
+ } else {
212
+ bindSignal(element, `class:${className}:${path}`, path, (value) => {
213
+ element.classList.toggle(className, Boolean(value));
214
+ });
215
+ }
216
+ continue;
217
+ }
218
+ if (signalName === "class") {
219
+ const path = element.getAttribute(name);
220
+ bindClass(element, "{}", path);
181
221
  }
182
222
  }
183
223
  }
184
224
  }
185
225
 
186
- function bindSignal(element, key, path, apply) {
226
+ function bindClassAttributes(scope) {
227
+ for (const element of elementsIn(scope)) {
228
+ for (const name of element.getAttributeNames?.() ?? []) {
229
+ const className = matchAttribute(name, attributeConfig, "class");
230
+ if (className == null) {
231
+ continue;
232
+ }
233
+ bindClass(element, className, element.getAttribute(name));
234
+ }
235
+ }
236
+ }
237
+
238
+ function bindClass(element, className, path) {
239
+ if (className === "" || className === "{}") {
240
+ const staticClasses = readClassTokens(element);
241
+ let previous = new Set();
242
+ bindSignal(element, `class:{}:${path}`, path, (value) => {
243
+ const next = normalizeClassTokens(value);
244
+ const current = readClassTokens(element);
245
+ for (const token of previous) {
246
+ if (!next.has(token) && !staticClasses.has(token)) {
247
+ current.delete(token);
248
+ }
249
+ }
250
+ for (const token of next) {
251
+ current.add(token);
252
+ }
253
+ writeClassTokens(element, current);
254
+ previous = next;
255
+ }, { rawInline: true });
256
+ return;
257
+ }
258
+
259
+ bindSignal(element, `class:${className}:${path}`, path, (value) => {
260
+ updateClassToken(element, className, Boolean(value));
261
+ });
262
+ }
263
+
264
+ function bindSignal(element, key, path, apply, options = {}) {
187
265
  const bound = signalBindings.get(element) ?? new Set();
188
266
  if (bound.has(key)) {
189
267
  return;
@@ -191,8 +269,9 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
191
269
  bound.add(key);
192
270
  signalBindings.set(element, bound);
193
271
 
194
- apply(signalRegistry.get(path));
195
- cleanups.add(signalRegistry.subscribe(path, apply));
272
+ const read = () => readBinding(path, options);
273
+ apply(read());
274
+ addCleanup(subscribeBinding(path, () => apply(read())), element);
196
275
  }
197
276
 
198
277
  function bindValueWriter(element, path) {
@@ -200,11 +279,42 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
200
279
  bindEvent(element, "change", `__async:set:${path}`);
201
280
  if (!handlerRegistry.resolve(`__async:set:${path}`)) {
202
281
  handlerRegistry.register(`__async:set:${path}`, function writeValue({ element }) {
203
- signalRegistry.set(path, element.value);
282
+ writeBinding(path, element.value);
204
283
  });
205
284
  }
206
285
  }
207
286
 
287
+ function readBinding(path, options = {}) {
288
+ if (isInlineBinding(path)) {
289
+ const value = inlineBindings.get(path);
290
+ return options.rawInline ? value : resolveInlineValue(value);
291
+ }
292
+ return signalRegistry.get(path);
293
+ }
294
+
295
+ function writeBinding(path, value) {
296
+ if (!isInlineBinding(path)) {
297
+ return signalRegistry.set(path, value);
298
+ }
299
+ const binding = inlineBindings.get(path);
300
+ if (isSignalRef(binding)) {
301
+ return binding.set(value);
302
+ }
303
+ throw new Error(`Inline binding "${path}" is not writable.`);
304
+ }
305
+
306
+ function subscribeBinding(path, fn) {
307
+ if (!isInlineBinding(path)) {
308
+ return signalRegistry.subscribe(path, fn);
309
+ }
310
+ const cleanups = collectSignalRefs(inlineBindings.get(path)).map((ref) => ref.subscribe(fn));
311
+ return () => {
312
+ for (const cleanup of cleanups) {
313
+ cleanup();
314
+ }
315
+ };
316
+ }
317
+
208
318
  function bindBoundaries(scope) {
209
319
  for (const boundary of elementsIn(scope)) {
210
320
  if (renderingBoundaries.has(boundary)) {
@@ -225,7 +335,7 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
225
335
  cleanup: signalRegistry.subscribe(`${id}.$status`, () => renderBoundary(boundary))
226
336
  };
227
337
  boundaryState.set(boundary, state);
228
- cleanups.add(state.cleanup);
338
+ addCleanup(state.cleanup, boundary);
229
339
  }
230
340
  renderBoundary(boundary);
231
341
  }
@@ -241,6 +351,7 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
241
351
  if (!template) {
242
352
  return;
243
353
  }
354
+ cleanupChildren(boundary);
244
355
  boundary.replaceChildren(template.content.cloneNode(true));
245
356
  renderingBoundaries.add(boundary);
246
357
  try {
@@ -252,15 +363,17 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
252
363
 
253
364
  function runPseudoEvents(scope) {
254
365
  for (const element of elementsIn(scope)) {
255
- const ref = readAttribute(element, attributeConfig, "on", "mount");
256
- if (ref == null) {
366
+ const refs = readPseudoRefs(element, ["attach", "mount"]);
367
+ if (refs.length === 0) {
257
368
  continue;
258
369
  }
259
370
  if (mountedElements.has(element)) {
260
371
  continue;
261
372
  }
262
373
  mountedElements.add(element);
263
- runPseudo(element, ref);
374
+ for (const ref of refs) {
375
+ runPseudo(element, ref);
376
+ }
264
377
  }
265
378
 
266
379
  for (const element of elementsIn(scope)) {
@@ -272,10 +385,21 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
272
385
  continue;
273
386
  }
274
387
  visibleElements.add(element);
275
- cleanups.add(observeVisible(element, () => runPseudo(element, ref)));
388
+ addCleanup(observeVisible(element, () => runPseudo(element, ref)), element);
276
389
  }
277
390
  }
278
391
 
392
+ function readPseudoRefs(element, names) {
393
+ const refs = [];
394
+ for (const name of names) {
395
+ const ref = readAttribute(element, attributeConfig, "on", name);
396
+ if (ref != null) {
397
+ refs.push(ref);
398
+ }
399
+ }
400
+ return refs;
401
+ }
402
+
279
403
  async function runPseudo(element, ref) {
280
404
  try {
281
405
  const results = await handlerRegistry.run(ref, {
@@ -291,7 +415,7 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
291
415
  });
292
416
  for (const result of results) {
293
417
  if (typeof result === "function") {
294
- cleanups.add(result);
418
+ addCleanup(result, element);
295
419
  }
296
420
  }
297
421
  } catch (error) {
@@ -327,9 +451,170 @@ export function AsyncLoader({ root, signals, handlers, server, router, cache, at
327
451
  }
328
452
  }
329
453
 
454
+ function addCleanup(cleanup, owner, mode = "self") {
455
+ if (typeof cleanup !== "function") {
456
+ return cleanup;
457
+ }
458
+ cleanups.add(cleanup);
459
+ if (owner) {
460
+ const records = scopedCleanups.get(owner) ?? [];
461
+ records.push({ cleanup, mode });
462
+ scopedCleanups.set(owner, records);
463
+ }
464
+ return cleanup;
465
+ }
466
+
467
+ function runCleanup(cleanup) {
468
+ if (typeof cleanup !== "function" || !cleanups.has(cleanup)) {
469
+ return;
470
+ }
471
+ cleanups.delete(cleanup);
472
+ cleanup();
473
+ }
474
+
475
+ function cleanupChildren(container) {
476
+ runScopedCleanups(container, "children");
477
+ for (const child of [...(container.childNodes ?? [])]) {
478
+ cleanupNode(child);
479
+ }
480
+ }
481
+
482
+ function cleanupNode(node) {
483
+ if (node.nodeType !== 1) {
484
+ return;
485
+ }
486
+ for (const element of elementsIn(node)) {
487
+ runScopedCleanups(element);
488
+ }
489
+ }
490
+
491
+ function runScopedCleanups(element, mode) {
492
+ const records = scopedCleanups.get(element);
493
+ if (!records) {
494
+ return;
495
+ }
496
+ const remaining = [];
497
+ for (const record of records) {
498
+ if (mode && record.mode !== mode) {
499
+ remaining.push(record);
500
+ continue;
501
+ }
502
+ runCleanup(record.cleanup);
503
+ }
504
+ if (remaining.length > 0) {
505
+ scopedCleanups.set(element, remaining);
506
+ } else {
507
+ scopedCleanups.delete(element);
508
+ }
509
+ }
510
+
330
511
  return api;
331
512
  }
332
513
 
514
+ function normalizeClassTokens(value, tokens = new Set()) {
515
+ if (value == null || value === false) {
516
+ return tokens;
517
+ }
518
+ if (isSignalRef(value)) {
519
+ const signalValue = value.value;
520
+ if (signalValue === true) {
521
+ tokens.add(signalClassName(value.id));
522
+ return tokens;
523
+ }
524
+ return normalizeClassTokens(signalValue, tokens);
525
+ }
526
+ if (typeof value === "string") {
527
+ for (const token of value.split(/\s+/).filter(Boolean)) {
528
+ tokens.add(token);
529
+ }
530
+ return tokens;
531
+ }
532
+ if (Array.isArray(value)) {
533
+ for (const item of value) {
534
+ normalizeClassTokens(item, tokens);
535
+ }
536
+ return tokens;
537
+ }
538
+ if (typeof value === "object") {
539
+ for (const [token, enabled] of Object.entries(value)) {
540
+ const value = isSignalRef(enabled) ? enabled.value : enabled;
541
+ if (value) {
542
+ normalizeClassTokens(token, tokens);
543
+ }
544
+ }
545
+ return tokens;
546
+ }
547
+ if (value !== true) {
548
+ tokens.add(String(value));
549
+ }
550
+ return tokens;
551
+ }
552
+
553
+ function resolveInlineValue(value) {
554
+ if (isSignalRef(value)) {
555
+ return value.value;
556
+ }
557
+ if (Array.isArray(value)) {
558
+ return value.map(resolveInlineValue);
559
+ }
560
+ if (value && typeof value === "object") {
561
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, resolveInlineValue(entry)]));
562
+ }
563
+ return value;
564
+ }
565
+
566
+ function collectSignalRefs(value, refs = new Map()) {
567
+ if (isSignalRef(value)) {
568
+ refs.set(value.id, value);
569
+ return [...refs.values()];
570
+ }
571
+ if (Array.isArray(value)) {
572
+ for (const item of value) {
573
+ collectSignalRefs(item, refs);
574
+ }
575
+ return [...refs.values()];
576
+ }
577
+ if (value && typeof value === "object") {
578
+ for (const item of Object.values(value)) {
579
+ collectSignalRefs(item, refs);
580
+ }
581
+ }
582
+ return [...refs.values()];
583
+ }
584
+
585
+ function isInlineBinding(value) {
586
+ return typeof value === "string" && value.startsWith(inlineBindingPrefix);
587
+ }
588
+
589
+ function signalClassName(id) {
590
+ return id.split(".").at(-1);
591
+ }
592
+
593
+ function updateClassToken(element, className, enabled) {
594
+ const tokens = readClassTokens(element);
595
+ for (const token of normalizeClassTokens(className)) {
596
+ if (enabled) {
597
+ tokens.add(token);
598
+ } else {
599
+ tokens.delete(token);
600
+ }
601
+ }
602
+ writeClassTokens(element, tokens);
603
+ }
604
+
605
+ function readClassTokens(element) {
606
+ return normalizeClassTokens(element.getAttribute("class") ?? "");
607
+ }
608
+
609
+ function writeClassTokens(element, tokens) {
610
+ const value = [...tokens].join(" ");
611
+ if (value.length === 0) {
612
+ element.removeAttribute("class");
613
+ return;
614
+ }
615
+ element.setAttribute("class", value);
616
+ }
617
+
333
618
  function collectBoundaryTemplates(boundary, id, attributeConfig) {
334
619
  const templates = {};
335
620
  for (const template of [...boundary.children].filter((child) => child.tagName === "TEMPLATE")) {
@@ -370,6 +655,14 @@ function updateAttribute(element, attr, value) {
370
655
  }
371
656
  }
372
657
 
658
+ function updateProperty(element, prop, value) {
659
+ if (value == null) {
660
+ element[prop] = "";
661
+ return;
662
+ }
663
+ element[prop] = value;
664
+ }
665
+
373
666
  function selectAll(scope, selector) {
374
667
  const elements = [];
375
668
  if (scope?.nodeType === 1 && scope.matches?.(selector)) {
package/src/partials.js CHANGED
@@ -26,6 +26,11 @@ export function createPartialRegistry(initialMap = {}, options = {}) {
26
26
  return registry;
27
27
  },
28
28
 
29
+ unregister(id) {
30
+ assertId(id);
31
+ return entries.delete(id);
32
+ },
33
+
29
34
  resolve(id) {
30
35
  assertId(id);
31
36
  return entries.get(id);
@@ -46,7 +51,7 @@ export function createPartialRegistry(initialMap = {}, options = {}) {
46
51
  partials: registry
47
52
  };
48
53
  const result = await fn.call(partialContext, props);
49
- return normalizePartialResult(result);
54
+ return normalizePartialResult(result, partialContext);
50
55
  },
51
56
 
52
57
  _adoptMany() {
@@ -58,18 +63,18 @@ export function createPartialRegistry(initialMap = {}, options = {}) {
58
63
  return registry;
59
64
  }
60
65
 
61
- export function normalizePartialResult(result) {
66
+ export function normalizePartialResult(result, context = {}) {
62
67
  if (isPartialEnvelope(result)) {
63
68
  return {
64
69
  ...result,
65
- html: Object.hasOwn(result, "html") ? renderPartialValue(result.html) : result.html
70
+ html: Object.hasOwn(result, "html") ? renderPartialValue(result.html, context) : result.html
66
71
  };
67
72
  }
68
73
 
69
- return { html: renderPartialValue(result) };
74
+ return { html: renderPartialValue(result, context) };
70
75
  }
71
76
 
72
- function renderPartialValue(value) {
77
+ function renderPartialValue(value, context) {
73
78
  if (value?.nodeType) {
74
79
  return value;
75
80
  }
@@ -77,9 +82,17 @@ function renderPartialValue(value) {
77
82
  return value;
78
83
  }
79
84
  if (isTemplateResult(value)) {
80
- return renderTemplate(value);
85
+ return renderTemplate(value, templateRenderOptions(context));
81
86
  }
82
- return renderTemplate(value);
87
+ return renderTemplate(value, templateRenderOptions(context));
88
+ }
89
+
90
+ function templateRenderOptions(context) {
91
+ return {
92
+ attributes: context.loader?.attributes,
93
+ signals: context.signals,
94
+ bind: context.loader?._registerBinding?.bind(context.loader)
95
+ };
83
96
  }
84
97
 
85
98
  function isPartialEnvelope(value) {
@@ -34,6 +34,10 @@ export function createRegistryStore(initial = {}, options = {}) {
34
34
  return value;
35
35
  },
36
36
 
37
+ unregister(type, id) {
38
+ return registry.delete(type, id);
39
+ },
40
+
37
41
  delete(type, id) {
38
42
  return registry._map(type).delete(id);
39
43
  },
package/src/router.js CHANGED
@@ -41,6 +41,15 @@ export function createRouteRegistry(initialMap = {}, options = {}) {
41
41
  return registry;
42
42
  },
43
43
 
44
+ unregister(pattern) {
45
+ assertPattern(pattern);
46
+ const index = routes.findIndex((candidate) => candidate.pattern === pattern);
47
+ if (index !== -1) {
48
+ routes.splice(index, 1);
49
+ }
50
+ return entries.delete(pattern);
51
+ },
52
+
44
53
  match(url) {
45
54
  const path = toUrl(url).pathname;
46
55
  for (const candidate of routes) {
package/src/server.js CHANGED
@@ -28,6 +28,11 @@ export function createServerRegistry(initialMap = {}, options = {}) {
28
28
  return registry;
29
29
  },
30
30
 
31
+ unregister(id) {
32
+ assertServerId(id);
33
+ return entries.delete(id);
34
+ },
35
+
31
36
  resolve(id) {
32
37
  assertServerId(id);
33
38
  return entries.get(id);
@@ -43,7 +48,7 @@ export function createServerRegistry(initialMap = {}, options = {}) {
43
48
  let runContext;
44
49
  const server = createServerNamespace((childId, childArgs, childContext = {}) => {
45
50
  return registry.run(childId, childArgs, { ...runContext, ...childContext });
46
- });
51
+ }, {}, () => runContext);
47
52
 
48
53
  const mergedContext = {
49
54
  ...defaults,
@@ -76,7 +81,7 @@ export function createServerRegistry(initialMap = {}, options = {}) {
76
81
  }, registryStore, type);
77
82
 
78
83
  registry.registerMany(initialMap);
79
- return createServerNamespace((id, args, context) => registry.run(id, args, context), registry);
84
+ return createServerNamespace((id, args, context) => registry.run(id, args, context), registry, () => defaults);
80
85
  }
81
86
 
82
87
  export function createServerProxy({
@@ -127,7 +132,7 @@ export function createServerProxy({
127
132
  _setContext(context = {}) {
128
133
  Object.assign(defaults, context);
129
134
  }
130
- });
135
+ }, () => defaults);
131
136
  }
132
137
 
133
138
  export function resolveServerCommandArguments(args, context = {}) {
@@ -205,7 +210,7 @@ export function defaultInput(context = {}) {
205
210
  };
206
211
  }
207
212
 
208
- function createServerNamespace(run, root = {}) {
213
+ function createServerNamespace(run, root = {}, contextProvider = () => ({})) {
209
214
  const cache = new Map();
210
215
 
211
216
  function namespace(parts) {
@@ -214,11 +219,14 @@ function createServerNamespace(run, root = {}) {
214
219
  return cache.get(cacheKey);
215
220
  }
216
221
 
217
- const callable = (...args) => {
222
+ const callable = async (...args) => {
218
223
  if (parts.length === 0) {
219
224
  throw new Error("Server namespace is not directly callable.");
220
225
  }
221
- return Promise.resolve(run(parts.join("."), args)).then(unwrapServerResult);
226
+ const context = contextProvider() ?? {};
227
+ const result = await run(parts.join("."), args, context);
228
+ await applyServerResult(result, context);
229
+ return unwrapServerResult(result);
222
230
  };
223
231
 
224
232
  const proxy = new Proxy(callable, {
@@ -229,6 +237,18 @@ function createServerNamespace(run, root = {}) {
229
237
  if (prop in _target) {
230
238
  return _target[prop];
231
239
  }
240
+ if (parts.length === 0 && prop === "_withContext") {
241
+ return (context = {}) => createServerNamespace(run, root, () => ({
242
+ ...(contextProvider() ?? {}),
243
+ ...context
244
+ }));
245
+ }
246
+ if (parts.length === 0 && prop === "run" && typeof root.run === "function") {
247
+ return (id, args = [], context = {}) => root.run(id, args, {
248
+ ...(contextProvider() ?? {}),
249
+ ...context
250
+ });
251
+ }
232
252
  if (parts.length === 0 && Object.hasOwn(root, prop)) {
233
253
  return root[prop];
234
254
  }
package/src/signals.js CHANGED
@@ -121,7 +121,7 @@ export function createSignalRegistry(initialMap = {}, options = {}) {
121
121
  const registryStore = options.registry ?? createRegistryStore();
122
122
  const type = options.type ?? "signal";
123
123
  const entries = registryStore._map(type);
124
- const registryCleanups = new Set();
124
+ const registryCleanups = new Map();
125
125
  const runtimeContext = {};
126
126
  const boundEntries = new Set();
127
127
 
@@ -144,6 +144,19 @@ export function createSignalRegistry(initialMap = {}, options = {}) {
144
144
  return registry;
145
145
  },
146
146
 
147
+ unregister(id) {
148
+ assertId(id);
149
+ if (!entries.has(id)) {
150
+ return false;
151
+ }
152
+ registryCleanups.get(id)?.();
153
+ registryCleanups.delete(id);
154
+ entries.get(id)?._dispose?.();
155
+ entries.delete(id);
156
+ boundEntries.delete(id);
157
+ return true;
158
+ },
159
+
147
160
  ensure(id, initial) {
148
161
  assertId(id);
149
162
  if (!entries.has(id)) {
@@ -256,7 +269,7 @@ export function createSignalRegistry(initialMap = {}, options = {}) {
256
269
  },
257
270
 
258
271
  destroy() {
259
- for (const cleanup of registryCleanups) {
272
+ for (const cleanup of registryCleanups.values()) {
260
273
  cleanup();
261
274
  }
262
275
  registryCleanups.clear();
@@ -313,7 +326,7 @@ export function createSignalRegistry(initialMap = {}, options = {}) {
313
326
  boundEntries.add(id);
314
327
  const cleanup = entry._bindRegistry(registry, id);
315
328
  if (typeof cleanup === "function") {
316
- registryCleanups.add(cleanup);
329
+ registryCleanups.set(id, cleanup);
317
330
  }
318
331
  }
319
332
  }