@adobe/data-lit 0.10.31 → 0.10.33

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.
Files changed (76) hide show
  1. package/README.md +12 -14
  2. package/dist/elements/application-host.d.ts +6 -2
  3. package/dist/elements/application-host.js +17 -1
  4. package/dist/elements/application-host.js.map +1 -1
  5. package/dist/functions/iterate-self-and-ancestors.d.ts +1 -0
  6. package/dist/functions/iterate-self-and-ancestors.js +2 -2
  7. package/dist/functions/iterate-self-and-ancestors.js.map +1 -1
  8. package/dist/hooks/component/hooks-controller.js +16 -5
  9. package/dist/hooks/component/hooks-controller.js.map +1 -1
  10. package/dist/hooks/use-drag-observe.js +2 -1
  11. package/dist/hooks/use-drag-observe.js.map +1 -1
  12. package/dist/hooks/use-drag-transaction.d.ts +5 -0
  13. package/dist/hooks/use-drag-transaction.js +42 -22
  14. package/dist/hooks/use-drag-transaction.js.map +1 -1
  15. package/dist/hooks/use-draggable.js +70 -48
  16. package/dist/hooks/use-draggable.js.map +1 -1
  17. package/dist/hooks/use-effect.js +6 -2
  18. package/dist/hooks/use-effect.js.map +1 -1
  19. package/dist/hooks/use-element.d.ts +4 -2
  20. package/dist/hooks/use-element.js +10 -51
  21. package/dist/hooks/use-element.js.map +1 -1
  22. package/dist/hooks/use-memo.js +11 -9
  23. package/dist/hooks/use-memo.js.map +1 -1
  24. package/dist/hooks/use-observable.d.ts +4 -0
  25. package/dist/hooks/use-observable.js +13 -8
  26. package/dist/hooks/use-observable.js.map +1 -1
  27. package/dist/hooks/use-ref.js +4 -3
  28. package/dist/hooks/use-ref.js.map +1 -1
  29. package/dist/hooks/use-window-event.js +1 -1
  30. package/dist/hooks/use-window-event.js.map +1 -1
  31. package/package.json +3 -3
  32. package/src/decorators/apply-decorator.test.ts +37 -0
  33. package/src/decorators/apply-service-decorators.test.ts +53 -0
  34. package/src/decorators/require-service.test.ts +31 -0
  35. package/src/elements/application-element.test.ts +80 -0
  36. package/src/elements/application-host.test.ts +57 -0
  37. package/src/elements/application-host.ts +16 -3
  38. package/src/elements/database-element.test.ts +111 -0
  39. package/src/functions/iterate-self-and-ancestors.test.ts +29 -0
  40. package/src/functions/iterate-self-and-ancestors.ts +2 -2
  41. package/src/hooks/attach-decorator.test.ts +45 -0
  42. package/src/hooks/component/hooks-controller.cleanup.test.ts +65 -0
  43. package/src/hooks/component/hooks-controller.ts +15 -5
  44. package/src/hooks/component/stack.test.ts +37 -0
  45. package/src/hooks/test-host.test-utils.ts +43 -0
  46. package/src/hooks/use-connected.test.ts +88 -0
  47. package/src/hooks/use-debounce.test.ts +69 -0
  48. package/src/hooks/use-drag-generator.test.ts +67 -0
  49. package/src/hooks/use-drag-observe.test.ts +74 -0
  50. package/src/hooks/use-drag-observe.ts +2 -1
  51. package/src/hooks/use-drag-transaction.test.ts +109 -0
  52. package/src/hooks/use-drag-transaction.ts +48 -27
  53. package/src/hooks/use-draggable.test.ts +148 -0
  54. package/src/hooks/use-draggable.ts +73 -48
  55. package/src/hooks/use-effect.test.ts +51 -0
  56. package/src/hooks/use-effect.ts +5 -2
  57. package/src/hooks/use-element.test.ts +82 -0
  58. package/src/hooks/use-element.ts +17 -60
  59. package/src/hooks/use-memo.test.ts +66 -0
  60. package/src/hooks/use-memo.ts +16 -11
  61. package/src/hooks/use-observable-values.test.ts +53 -0
  62. package/src/hooks/use-observable.test.ts +63 -0
  63. package/src/hooks/use-observable.ts +13 -8
  64. package/src/hooks/use-pointer-observe.test.ts +59 -0
  65. package/src/hooks/use-ref.test.ts +50 -0
  66. package/src/hooks/use-ref.ts +4 -3
  67. package/src/hooks/use-updated.test.ts +98 -0
  68. package/src/hooks/use-window-event.test.ts +61 -0
  69. package/src/hooks/use-window-event.ts +1 -1
  70. package/src/hooks/with-hooks.test.ts +45 -0
  71. package/src/index.test.ts +26 -0
  72. package/src/public-types.type-test.ts +17 -0
  73. package/tsconfig.json +1 -0
  74. package/tsconfig.test.json +9 -0
  75. package/tsconfig.tsbuildinfo +1 -1
  76. package/vitest.config.ts +7 -0
@@ -1,13 +1,15 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
- import { useEffect } from "./use-effect.js";
3
- import { useState } from "./use-state.js";
2
+ import { equalsShallow } from "@adobe/data/equals-shallow";
3
+ import { Component_stack } from "./component/stack.js";
4
4
  export function useMemo(calculateValue, dependencies = []) {
5
- const [state, setState] = useState(undefined);
6
- let currentValue = state;
7
- useEffect(() => {
8
- currentValue = calculateValue();
9
- setState(currentValue);
10
- }, dependencies);
11
- return currentValue;
5
+ const component = Component_stack.active();
6
+ const hookIndex = component.hookIndex++;
7
+ const memo = component.hooks[hookIndex];
8
+ if (memo && equalsShallow(dependencies, memo.dependencies)) {
9
+ return memo.value;
10
+ }
11
+ const value = calculateValue();
12
+ component.hooks[hookIndex] = { value, dependencies };
13
+ return value;
12
14
  }
13
15
  //# sourceMappingURL=use-memo.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-memo.js","sourceRoot":"","sources":["../../src/hooks/use-memo.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAEvD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C,MAAM,UAAU,OAAO,CAAI,cAAuB,EAAE,eAA0B,EAAE;IAC5E,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAgB,SAAS,CAAC,CAAC;IAC7D,IAAI,YAAY,GAAG,KAAK,CAAC;IAEzB,SAAS,CAAC,GAAG,EAAE;QACX,YAAY,GAAG,cAAc,EAAE,CAAC;QAChC,QAAQ,CAAC,YAAY,CAAC,CAAC;IAC3B,CAAC,EAAE,YAAY,CAAC,CAAC;IAEjB,OAAO,YAAa,CAAC;AACzB,CAAC"}
1
+ {"version":3,"file":"use-memo.js","sourceRoot":"","sources":["../../src/hooks/use-memo.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAEvD,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAOvD,MAAM,UAAU,OAAO,CAAI,cAAuB,EAAE,eAA0B,EAAE;IAC5E,MAAM,SAAS,GAAG,eAAe,CAAC,MAAM,EAAE,CAAC;IAC3C,MAAM,SAAS,GAAG,SAAS,CAAC,SAAS,EAAE,CAAC;IACxC,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,SAAS,CAAwB,CAAC;IAC/D,IAAI,IAAI,IAAI,aAAa,CAAC,YAAY,EAAE,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC;QACzD,OAAO,IAAI,CAAC,KAAK,CAAC;IACtB,CAAC;IACD,MAAM,KAAK,GAAG,cAAc,EAAE,CAAC;IAC/B,SAAS,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE,KAAK,EAAE,YAAY,EAAoB,CAAC;IACvE,OAAO,KAAK,CAAC;AACjB,CAAC"}
@@ -1,2 +1,6 @@
1
1
  import { Observe } from "@adobe/data/observe";
2
+ /**
3
+ * Subscribes to `observable` and returns its latest value. Every notification
4
+ * requests a render, including one that repeats the previous value or reference.
5
+ */
2
6
  export declare function useObservable<T>(observable: Observe<T>): T | undefined;
@@ -1,13 +1,18 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
- import { useState } from "./use-state.js";
2
+ import { Component_stack } from "./component/stack.js";
3
3
  import { useEffect } from "./use-effect.js";
4
+ import { useRef } from "./use-ref.js";
5
+ /**
6
+ * Subscribes to `observable` and returns its latest value. Every notification
7
+ * requests a render, including one that repeats the previous value or reference.
8
+ */
4
9
  export function useObservable(observable) {
5
- let [value, setValue] = useState(undefined);
6
- useEffect(() => {
7
- return observable(newValue => {
8
- setValue(value = newValue);
9
- });
10
- }, [observable]);
11
- return value;
10
+ const component = Component_stack.active();
11
+ const latest = useRef(undefined);
12
+ useEffect(() => observable(value => {
13
+ latest.current = value;
14
+ component.requestUpdate();
15
+ }), [observable]);
16
+ return latest.current;
12
17
  }
13
18
  //# sourceMappingURL=use-observable.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-observable.js","sourceRoot":"","sources":["../../src/hooks/use-observable.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAEvD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAG5C,MAAM,UAAU,aAAa,CAAI,UAAsB;IACnD,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAgB,SAAS,CAAC,CAAC;IAC3D,SAAS,CAAC,GAAG,EAAE;QACX,OAAO,UAAU,CAAC,QAAQ,CAAC,EAAE;YACzB,QAAQ,CAAC,KAAK,GAAG,QAAQ,CAAC,CAAC;QAC/B,CAAC,CAAC,CAAC;IACP,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;IACjB,OAAO,KAAK,CAAC;AACjB,CAAC"}
1
+ {"version":3,"file":"use-observable.js","sourceRoot":"","sources":["../../src/hooks/use-observable.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAEvD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAGtC;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAI,UAAsB;IACnD,MAAM,SAAS,GAAG,eAAe,CAAC,MAAM,EAAE,CAAC;IAC3C,MAAM,MAAM,GAAG,MAAM,CAAgB,SAAS,CAAC,CAAC;IAChD,SAAS,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE;QAC/B,MAAM,CAAC,OAAO,GAAG,KAAK,CAAC;QACvB,SAAS,CAAC,aAAa,EAAE,CAAC;IAC9B,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;IAClB,OAAO,MAAM,CAAC,OAAO,CAAC;AAC1B,CAAC"}
@@ -1,7 +1,8 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
- import { useState } from "./use-state.js";
2
+ import { Component_stack } from "./component/stack.js";
3
3
  export function useRef(initialValue) {
4
- const [state] = useState({ current: initialValue });
5
- return state;
4
+ const component = Component_stack.active();
5
+ const hookIndex = component.hookIndex++;
6
+ return component.hooks[hookIndex] ??= { current: initialValue };
6
7
  }
7
8
  //# sourceMappingURL=use-ref.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-ref.js","sourceRoot":"","sources":["../../src/hooks/use-ref.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAEvD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C,MAAM,UAAU,MAAM,CAAI,YAAe;IACrC,MAAM,CAAC,KAAK,CAAC,GAAG,QAAQ,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC,CAAC;IACpD,OAAO,KAAK,CAAC;AACjB,CAAC"}
1
+ {"version":3,"file":"use-ref.js","sourceRoot":"","sources":["../../src/hooks/use-ref.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAEvD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAEvD,MAAM,UAAU,MAAM,CAAI,YAAe;IACrC,MAAM,SAAS,GAAG,eAAe,CAAC,MAAM,EAAE,CAAC;IAC3C,MAAM,SAAS,GAAG,SAAS,CAAC,SAAS,EAAE,CAAC;IACxC,OAAO,SAAS,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC;AACpE,CAAC"}
@@ -6,6 +6,6 @@ export function useWindowEvent(event, callbackFactory, deps = []) {
6
6
  useEffect(() => {
7
7
  window.addEventListener(event, callback);
8
8
  return () => window.removeEventListener(event, callback);
9
- }, [callback]);
9
+ }, [event, callback]);
10
10
  }
11
11
  //# sourceMappingURL=use-window-event.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-window-event.js","sourceRoot":"","sources":["../../src/hooks/use-window-event.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAEvD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAExC,MAAM,UAAU,cAAc,CAC1B,KAAQ,EACR,eAAqD,EACrD,OAAc,EAAE;IAEhB,MAAM,QAAQ,GAAG,OAAO,CAAC,eAAe,EAAE,IAAI,CAAC,CAAC;IAChD,SAAS,CAAC,GAAG,EAAE;QACX,MAAM,CAAC,gBAAgB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QACzC,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IAC7D,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;AACnB,CAAC"}
1
+ {"version":3,"file":"use-window-event.js","sourceRoot":"","sources":["../../src/hooks/use-window-event.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAEvD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAExC,MAAM,UAAU,cAAc,CAC1B,KAAQ,EACR,eAAqD,EACrD,OAAc,EAAE;IAEhB,MAAM,QAAQ,GAAG,OAAO,CAAC,eAAe,EAAE,IAAI,CAAC,CAAC;IAChD,SAAS,CAAC,GAAG,EAAE;QACX,MAAM,CAAC,gBAAgB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QACzC,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IAC7D,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC;AAC1B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/data-lit",
3
- "version": "0.10.31",
3
+ "version": "0.10.33",
4
4
  "description": "Adobe data Lit bindings - hooks, elements, decorators",
5
5
  "type": "module",
6
6
  "private": false,
@@ -12,7 +12,7 @@
12
12
  "access": "public"
13
13
  },
14
14
  "dependencies": {
15
- "@adobe/data": "0.10.31"
15
+ "@adobe/data": "0.10.33"
16
16
  },
17
17
  "peerDependencies": {
18
18
  "lit": "^3.3.1"
@@ -34,6 +34,6 @@
34
34
  "dev": "tsc -b -w",
35
35
  "link": "pnpm build && pnpm link --global",
36
36
  "test": "vitest --run --passWithNoTests",
37
- "typecheck": "tsc -b"
37
+ "typecheck": "tsc -b && tsc -p tsconfig.test.json"
38
38
  }
39
39
  }
@@ -0,0 +1,37 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { applyDecorator } from "./apply-decorator.js";
4
+
5
+ const double = (_target: unknown, _key: string, descriptor: PropertyDescriptor) => {
6
+ const original = descriptor.value as (this: unknown) => number;
7
+ return { ...descriptor, value(this: unknown) { return original.call(this) * 2; } };
8
+ };
9
+
10
+ describe("applyDecorator", () => {
11
+ it("decorates a method found on the prototype chain and defines it on the target", () => {
12
+ class Base { value = 2; get() { return this.value; } }
13
+ class Derived extends Base {}
14
+ applyDecorator(Derived.prototype, "get", double);
15
+ expect(new Derived().get()).toBe(4);
16
+ expect(new Base().get()).toBe(2);
17
+ expect(Object.prototype.hasOwnProperty.call(Derived.prototype, "get")).toBe(true);
18
+ });
19
+
20
+ it("decorates an own property and passes target, key, and descriptor", () => {
21
+ const method = () => 1;
22
+ const target = { method };
23
+ const decorator = vi.fn((_target: unknown, _key: string, descriptor: PropertyDescriptor) => descriptor);
24
+ applyDecorator(target, "method", decorator);
25
+ expect(decorator).toHaveBeenCalledWith(target, "method", expect.objectContaining({ value: method }));
26
+ });
27
+
28
+ it("defines exactly the descriptor the decorator returns", () => {
29
+ const target = { method: () => 1 };
30
+ applyDecorator(target, "method", () => ({ value: 5, enumerable: false, writable: false, configurable: true }));
31
+ expect(Object.getOwnPropertyDescriptor(target, "method")).toEqual({ value: 5, enumerable: false, writable: false, configurable: true });
32
+ });
33
+
34
+ it("throws when the property is not on the prototype chain", () => {
35
+ expect(() => applyDecorator({}, "missing", double)).toThrow("Property missing not found in prototype chain");
36
+ });
37
+ });
@@ -0,0 +1,53 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ // @vitest-environment happy-dom
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import { applyServiceDecorators } from "./apply-service-decorators.js";
5
+ import { Component_stack } from "../hooks/component/stack.js";
6
+ import { useState } from "../hooks/use-state.js";
7
+
8
+ function createElement(render: (this: unknown) => unknown, service?: unknown) {
9
+ return Object.assign(new EventTarget(), {
10
+ service,
11
+ hookIndex: 0,
12
+ hooks: [] as unknown[],
13
+ updatedListeners: new Set<() => void>(),
14
+ requestUpdate: vi.fn(),
15
+ isConnected: true,
16
+ render,
17
+ });
18
+ }
19
+
20
+ describe("applyServiceDecorators", () => {
21
+ it("renders null without calling render while there is no service", () => {
22
+ const render = vi.fn(() => "rendered");
23
+ const element = createElement(render);
24
+ applyServiceDecorators(element);
25
+ expect(element.render()).toBeNull();
26
+ expect(render).not.toHaveBeenCalled();
27
+ });
28
+
29
+ it("renders with hooks once a service is set", () => {
30
+ const element = createElement(function (this: unknown) {
31
+ expect(Component_stack.active()).toBe(this);
32
+ return useState("value")[0];
33
+ }, {});
34
+ applyServiceDecorators(element);
35
+ expect(element.render()).toBe("value");
36
+ expect(Component_stack.active()).toBeUndefined();
37
+ });
38
+
39
+ it("decorates a render inherited from the prototype", () => {
40
+ class Base { service = {}; render() { return "inherited"; } }
41
+ const element = Object.assign(new Base(), { hooks: [], hookIndex: 0 });
42
+ applyServiceDecorators(element);
43
+ expect(Object.prototype.hasOwnProperty.call(element, "render")).toBe(true);
44
+ expect(element.render()).toBe("inherited");
45
+ });
46
+
47
+ it("restores the hook stack when render throws", () => {
48
+ const element = createElement(() => { throw new Error("render failed"); }, {});
49
+ applyServiceDecorators(element);
50
+ expect(() => element.render()).toThrow("render failed");
51
+ expect(Component_stack.active()).toBeUndefined();
52
+ });
53
+ });
@@ -0,0 +1,31 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { requireService } from "./require-service.js";
4
+
5
+ function guard(render: (this: { service?: unknown }) => unknown) {
6
+ return requireService()({}, "render", { value: render }).value as (this: { service?: unknown }) => unknown;
7
+ }
8
+
9
+ describe("requireService", () => {
10
+ it("returns null without calling render while there is no service", () => {
11
+ const render = vi.fn(() => "rendered");
12
+ expect(guard(render).call({})).toBeNull();
13
+ expect(render).not.toHaveBeenCalled();
14
+ });
15
+
16
+ it("calls render with the receiver once a service is set", () => {
17
+ const host = { service: {} as unknown };
18
+ const render = vi.fn(function (this: unknown) { return this; });
19
+ expect(guard(render).call(host)).toBe(host);
20
+ });
21
+
22
+ it("propagates errors from render", () => {
23
+ const render = () => { throw new Error("render failed"); };
24
+ expect(() => guard(render).call({ service: {} })).toThrow("render failed");
25
+ });
26
+
27
+ it("returns the descriptor it modified", () => {
28
+ const descriptor: PropertyDescriptor = { value: () => 1 };
29
+ expect(requireService()({}, "render", descriptor)).toBe(descriptor);
30
+ });
31
+ });
@@ -0,0 +1,80 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ // @vitest-environment happy-dom
3
+ import { afterEach, describe, expect, it } from "vitest";
4
+ import { html } from "lit";
5
+ import type { Service } from "@adobe/data/service";
6
+ import { ApplicationElement } from "./application-element.js";
7
+ import { useState } from "../hooks/use-state.js";
8
+
9
+ type MainService = Service & { kind: "main" };
10
+ const createService = (kind: string) => ({ serviceName: kind, kind }) as unknown as MainService;
11
+
12
+ class ProbeElement extends ApplicationElement<MainService> {
13
+ override render() {
14
+ const [value] = useState("hooked");
15
+ return html`${value}`;
16
+ }
17
+ }
18
+ customElements.define("application-element-probe", ProbeElement);
19
+
20
+ class UnrenderedElement extends ApplicationElement<MainService> {}
21
+ customElements.define("application-element-unrendered", UnrenderedElement);
22
+
23
+ function under(parent: Element) {
24
+ const element = document.createElement("application-element-probe") as ProbeElement;
25
+ parent.append(element);
26
+ return element;
27
+ }
28
+
29
+ afterEach(async () => {
30
+ document.body.replaceChildren();
31
+ await Promise.resolve();
32
+ });
33
+
34
+ describe("ApplicationElement", () => {
35
+ it("finds the nearest ancestor service on connect", () => {
36
+ const outer = Object.assign(document.createElement("div"), { service: createService("outer") });
37
+ const inner = Object.assign(document.createElement("div"), { service: createService("inner") });
38
+ outer.append(inner);
39
+ document.body.append(outer);
40
+ expect(under(inner).service).toBe(inner.service);
41
+ });
42
+
43
+ it("keeps an injected service", () => {
44
+ const parent = Object.assign(document.createElement("div"), { service: createService("ancestor") });
45
+ document.body.append(parent);
46
+ const injected = createService("injected");
47
+ const element = document.createElement("application-element-probe") as ProbeElement;
48
+ element.service = injected;
49
+ parent.append(element);
50
+ expect(element.service).toBe(injected);
51
+ });
52
+
53
+ it("skips ancestors that fail the type guard", () => {
54
+ const matching = Object.assign(document.createElement("div"), { service: createService("main") });
55
+ const other = Object.assign(document.createElement("div"), { service: { serviceName: "other" } });
56
+ matching.append(other);
57
+ document.body.append(matching);
58
+ const element = document.createElement("application-element-probe") as ProbeElement;
59
+ element.typeGuard = (service): service is MainService => (service as MainService | undefined)?.kind === "main";
60
+ other.append(element);
61
+ expect(element.service).toBe(matching.service);
62
+ });
63
+
64
+ it("skips values that are not services and leaves service unset without a provider", () => {
65
+ const parent = Object.assign(document.createElement("div"), { service: { notAService: true } });
66
+ document.body.append(parent);
67
+ expect(under(parent).service).toBeUndefined();
68
+ });
69
+
70
+ it("renders with hooks", async () => {
71
+ const element = under(document.body);
72
+ await element.updateComplete;
73
+ expect(element.shadowRoot?.textContent).toBe("hooked");
74
+ });
75
+
76
+ it("throws from the default render", () => {
77
+ const element = new UnrenderedElement();
78
+ expect(() => element.render()).toThrow("render function must be overridden");
79
+ });
80
+ });
@@ -0,0 +1,57 @@
1
+ // @vitest-environment happy-dom
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+ import { html } from "lit";
4
+ import type { Service } from "@adobe/data/service";
5
+ import "./application-host.js";
6
+
7
+ function deferred<T>() {
8
+ let resolve!: (value: T) => void;
9
+ const promise = new Promise<T>(r => { resolve = r; });
10
+ return { promise, resolve };
11
+ }
12
+
13
+ afterEach(() => {
14
+ document.body.replaceChildren();
15
+ });
16
+
17
+ describe("ApplicationHost", () => {
18
+ it("creates one service across a DOM move", async () => {
19
+ const service = { serviceName: "main" } as unknown as Service;
20
+ const pending = deferred<Service>();
21
+ const createService = vi.fn(() => pending.promise);
22
+ const host = document.createElement("application-host");
23
+ host.createService = createService;
24
+ host.renderElement = () => html``;
25
+
26
+ const [parentA, parentB] = [document.createElement("div"), document.createElement("div")];
27
+ document.body.append(parentA, parentB);
28
+ parentA.append(host);
29
+ parentB.append(host);
30
+ pending.resolve(service);
31
+ await pending.promise;
32
+ await host.updateComplete;
33
+
34
+ expect(createService).toHaveBeenCalledTimes(1);
35
+ expect(host.service).toBe(service);
36
+
37
+ parentA.append(host);
38
+ await host.updateComplete;
39
+ expect(createService).toHaveBeenCalledTimes(1);
40
+ });
41
+
42
+ it("retries creation after a failure", async () => {
43
+ const service = { serviceName: "main" } as unknown as Service;
44
+ const createService = vi.fn()
45
+ .mockRejectedValueOnce(new Error("offline"))
46
+ .mockResolvedValueOnce(service);
47
+ const host = document.createElement("application-host");
48
+ host.createService = createService;
49
+ host.renderElement = () => html``;
50
+
51
+ await expect(host.connectedCallback()).rejects.toThrow("offline");
52
+ await host.connectedCallback();
53
+
54
+ expect(createService).toHaveBeenCalledTimes(2);
55
+ expect(host.service).toBe(service);
56
+ });
57
+ });
@@ -3,13 +3,12 @@
3
3
  import { LitElement, nothing, TemplateResult, css } from 'lit';
4
4
  import { customElement, property } from 'lit/decorators.js';
5
5
  import { Service } from '@adobe/data/service';
6
- import { ApplicationElement } from './application-element.js';
7
6
 
8
7
  const tagName = "application-host";
9
8
 
10
9
  declare global {
11
10
  interface HTMLElementTagNameMap {
12
- 'application-element': ApplicationElement<Service>;
11
+ 'application-host': ApplicationHost;
13
12
  }
14
13
  }
15
14
 
@@ -31,9 +30,23 @@ export class ApplicationHost<MainService extends Service = Service> extends LitE
31
30
  @property({ attribute: false })
32
31
  public service!: MainService;
33
32
 
33
+ #pendingService?: Promise<MainService>;
34
+
35
+ /**
36
+ * Creates the service once per host: reconnecting, including a DOM move, reuses the
37
+ * service or the in-flight creation. A failed creation is retried on the next connect.
38
+ */
34
39
  override async connectedCallback() {
35
40
  super.connectedCallback();
36
- this.service = await this.createService();
41
+ if (this.service) return;
42
+ const pending = this.#pendingService ??= this.createService();
43
+ try {
44
+ const service = await pending;
45
+ this.service ??= service;
46
+ } catch (error) {
47
+ if (this.#pendingService === pending) this.#pendingService = undefined;
48
+ throw error;
49
+ }
37
50
  }
38
51
 
39
52
  override render() {
@@ -0,0 +1,111 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ // @vitest-environment happy-dom
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
+ import { html } from "lit";
5
+ import { Database } from "@adobe/data/ecs";
6
+ import { DatabaseElement } from "./database-element.js";
7
+
8
+ const rootPlugin = Database.Plugin.create({ resources: { name: { default: "root" as string } } });
9
+ const childPlugin = Database.Plugin.create({ resources: { count: { default: 0 as number } } });
10
+
11
+ class ProbeElement extends DatabaseElement<typeof childPlugin> {
12
+ get plugin() {
13
+ return childPlugin;
14
+ }
15
+ fullDatabase() {
16
+ return this.database;
17
+ }
18
+ override render() {
19
+ return html`${this.database.resources.count}`;
20
+ }
21
+ }
22
+ customElements.define("database-element-probe", ProbeElement);
23
+
24
+ class UnrenderedElement extends DatabaseElement<typeof childPlugin> {
25
+ get plugin() {
26
+ return childPlugin;
27
+ }
28
+ }
29
+ customElements.define("database-element-unrendered", UnrenderedElement);
30
+
31
+ const create = () => document.createElement("database-element-probe") as ProbeElement;
32
+ const resources = (db: unknown) => (db as { resources: Record<string, unknown> }).resources;
33
+
34
+ afterEach(async () => {
35
+ document.body.replaceChildren();
36
+ await Promise.resolve();
37
+ });
38
+
39
+ describe("DatabaseElement", () => {
40
+ it("creates its own database when no ancestor provides one", async () => {
41
+ const element = create();
42
+ document.body.append(element);
43
+ expect(Database.is(element.fullDatabase())).toBe(true);
44
+ expect(resources(element.service).count).toBe(0);
45
+ await element.updateComplete;
46
+ expect(element.shadowRoot?.textContent).toBe("0");
47
+ });
48
+
49
+ it("extends the nearest ancestor database with its plugin", () => {
50
+ const root = Database.create(rootPlugin);
51
+ const parent = Object.assign(document.createElement("div"), { service: root });
52
+ document.body.append(parent);
53
+ const element = create();
54
+ parent.append(element);
55
+ expect(resources(element.service).name).toBe("root");
56
+ expect(resources(element.service).count).toBe(0);
57
+ });
58
+
59
+ it("finds the database of an ancestor database element", () => {
60
+ const outer = create();
61
+ document.body.append(outer);
62
+ const inner = create();
63
+ outer.append(inner);
64
+ expect(inner.fullDatabase()).toBe(outer.fullDatabase());
65
+ });
66
+
67
+ it("skips ancestor services that are not databases", () => {
68
+ const parent = Object.assign(document.createElement("div"), { service: { serviceName: "other" } });
69
+ document.body.append(parent);
70
+ const element = create();
71
+ parent.append(element);
72
+ expect(Database.is(element.fullDatabase())).toBe(true);
73
+ });
74
+
75
+ it("keeps an injected database and requests an update when it changes", () => {
76
+ const injected = Database.create(childPlugin);
77
+ const element = create();
78
+ const requestUpdate = vi.spyOn(element, "requestUpdate");
79
+ element.service = injected;
80
+ expect(requestUpdate).toHaveBeenCalledWith("service", undefined);
81
+ document.body.append(element);
82
+ expect(element.fullDatabase()).toBe(injected);
83
+ });
84
+
85
+ it("routes a database bound before the element upgraded through the service setter", () => {
86
+ const injected = Database.create(childPlugin);
87
+ const element = document.createElement("database-element-late");
88
+ (element as unknown as { service: unknown }).service = injected;
89
+ document.body.append(element);
90
+
91
+ class LateElement extends ProbeElement {}
92
+ customElements.define("database-element-late", LateElement);
93
+ const upgraded = element as unknown as LateElement;
94
+ expect(Object.prototype.hasOwnProperty.call(upgraded, "service")).toBe(false);
95
+ expect(upgraded.fullDatabase()).toBe(injected);
96
+ });
97
+
98
+ it("keeps its database across a reconnect", () => {
99
+ const element = create();
100
+ document.body.append(element);
101
+ const database = element.fullDatabase();
102
+ element.remove();
103
+ document.body.append(element);
104
+ expect(element.fullDatabase()).toBe(database);
105
+ });
106
+
107
+ it("throws from the default render", () => {
108
+ const element = new UnrenderedElement();
109
+ expect(() => element.render()).toThrow("render function must be overridden");
110
+ });
111
+ });
@@ -0,0 +1,29 @@
1
+ // @vitest-environment happy-dom
2
+ import { describe, expect, it } from "vitest";
3
+ import { iterateSelfAndAncestors } from "./iterate-self-and-ancestors.js";
4
+
5
+ const tags = (element: Element) => [...iterateSelfAndAncestors(element)].map(e => e.tagName.toLowerCase());
6
+
7
+ describe("iterateSelfAndAncestors", () => {
8
+ it("walks light-DOM parents", () => {
9
+ const section = document.createElement("section");
10
+ const span = section.appendChild(document.createElement("div")).appendChild(document.createElement("span"));
11
+ expect(tags(span)).toEqual(["span", "div", "section"]);
12
+ });
13
+
14
+ it("crosses from a shadow root to its host", () => {
15
+ const host = document.createElement("section");
16
+ const inner = host.attachShadow({ mode: "open" }).appendChild(document.createElement("article"));
17
+ expect(tags(inner)).toEqual(["article", "section"]);
18
+ });
19
+
20
+ it("follows an assigned slot through the shadow tree before the light parent", () => {
21
+ const host = document.createElement("section");
22
+ const article = host.attachShadow({ mode: "open" }).appendChild(document.createElement("article"));
23
+ const slot = article.appendChild(document.createElement("slot"));
24
+ const span = host.appendChild(document.createElement("span"));
25
+ Object.defineProperty(span, "assignedSlot", { value: slot });
26
+
27
+ expect(tags(span)).toEqual(["span", "slot", "article", "section"]);
28
+ });
29
+ });
@@ -1,13 +1,13 @@
1
1
  // © 2026 Adobe. MIT License. See /LICENSE for details.
2
2
 
3
+ /** Yields `element` and its ancestors along the composed tree: assigned slots, parents, then shadow hosts. */
3
4
  export function* iterateSelfAndAncestors(element: Element): IterableIterator<Element> {
4
5
  let current: Element | null = element;
5
6
 
6
7
  while (current) {
7
8
  yield current;
8
9
 
9
- if (current instanceof HTMLSlotElement && current.assignedSlot) {
10
- // Move to the slot's parent if the current element is assigned to a slot
10
+ if (current.assignedSlot) {
11
11
  current = current.assignedSlot;
12
12
  } else if (current.parentNode instanceof Element) {
13
13
  // Move up to the parent node if it is an element
@@ -0,0 +1,45 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { attachDecorator } from "./attach-decorator.js";
4
+
5
+ const double = (_target: unknown, _key: string, descriptor: PropertyDescriptor) => {
6
+ const original = descriptor.value as (this: unknown, ...args: unknown[]) => number;
7
+ return { ...descriptor, value(this: unknown, ...args: unknown[]) { return original.apply(this, args) * 2; } };
8
+ };
9
+
10
+ describe("attachDecorator", () => {
11
+ it("decorates an inherited method and installs it on the instance", () => {
12
+ class Base { value = 3; get() { return this.value; } }
13
+ const instance = new Base();
14
+ attachDecorator(instance, "get", double);
15
+ expect(instance.get()).toBe(6);
16
+ expect(Object.prototype.hasOwnProperty.call(instance, "get")).toBe(true);
17
+ expect(new Base().get()).toBe(3);
18
+ });
19
+
20
+ it("decorates an own method", () => {
21
+ const instance = { get: () => 2 };
22
+ attachDecorator(instance, "get", double);
23
+ expect(instance.get()).toBe(4);
24
+ });
25
+
26
+ it("passes the instance, property name, and current method to the decorator", () => {
27
+ const method = () => 1;
28
+ const instance = { method };
29
+ const decorator = vi.fn((_target: unknown, _key: string, descriptor: PropertyDescriptor) => descriptor);
30
+ attachDecorator(instance, "method", decorator);
31
+ expect(decorator).toHaveBeenCalledWith(instance, "method", expect.objectContaining({ value: method }));
32
+ });
33
+
34
+ it("applies repeated decorators in order", () => {
35
+ const instance = { get: () => 1 };
36
+ attachDecorator(instance, "get", double);
37
+ attachDecorator(instance, "get", double);
38
+ expect(instance.get()).toBe(4);
39
+ });
40
+
41
+ it("throws for a missing property", () => {
42
+ const instance = {} as { missing?: () => void };
43
+ expect(() => attachDecorator(instance, "missing", double)).toThrow("Property missing does not exist");
44
+ });
45
+ });