Skip to content

Commit

Permalink
use decorator metadata to store metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
SirPepe committed Aug 16, 2024
1 parent 46cd2c1 commit 635baac
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 36 deletions.
14 changes: 12 additions & 2 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -1712,8 +1712,18 @@ create your own events while using TypeScript.
### `NO_VALUE`

Transformers can return a special symbol to indicate that they were unable to
parse an input. This symbol is exported by Ornament as `NO_VALUE` or available
under the key `"ORNAMENT_NO_VALUE"` in the global symbol registry.
parse an input. This symbol is exported by Ornament as `NO_VALUE` and is also
available behind the key `"ORNAMENT_NO_VALUE"` in the global symbol registry.

### `METADATA`

Ornament, being a collection of decorators, stores its metadata in
[Decorator Metadata](https://github.com/tc39/proposal-decorator-metadata). To
avoid collisions with other libraries, the actual metadata is hidden behind a
symbol that is exported by Ornament as `METADATA` or available behind the key
`"ORNAMENT_METADATA"` in the global symbol registry. The contents of the
metadata record should not be considered part of Ornament's stable API and
could change at any moment. Use with caution!

## Troubleshooting

Expand Down
16 changes: 16 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Changelog

## 2.1.0

### FEATURE: Decorator metadata

Ornament, being a collection of decorators, now stores its metadata in
[Decorator Metadata](https://github.com/tc39/proposal-decorator-metadata). To
avoid collisions with other libraries, the actual metadata is hidden behind a
symbol that is exported by Ornament as `METADATA` or available behind the key
`"ORNAMENT_METADATA"` in the global symbol registry. The contents of the
metadata record should not be considered part of Ornament's stable API and
could change at any moment. Use with caution!

### Other changes in 2.0.1

Bump dependencies.

## 2.0.0

### BREAKING: Event name mapping removed from `@subscribe()`
Expand Down
61 changes: 28 additions & 33 deletions src/decorators.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { listen, trigger } from "./bus.js";
import { EMPTY_OBJ, NO_VALUE } from "./lib.js";
import { EMPTY_OBJ, METADATA, NO_VALUE } from "./lib.js";
import {
type Transformer,
type ClassAccessorDecorator,
Expand All @@ -8,40 +8,32 @@ import {
NonOptional,
} from "./types.js";

// Decorator Metadata does as of June 2024 not work reliably in Babel. Therefore
// metadata in this module is a bunch of manually managed WeakMaps until Babel's
// issues are fixed. The first bit of metadata maps debounced methods to their
// originals, scoped by component instance. This is required to make @init()
// calls run synchronously, even if @debounce() was applied to the method in
// question.
const ALL_DEBOUNCED_METHODS = new WeakMap<
object,
WeakMap<Method<any, any>, Method<any, any>>
>();

// This should really be scoped on a class-by-class basis, but the @attr()
// decorator has no context without decorator metadata (which, again, is too
// unreliable in Babel as of June 2024). The list of observable attributes must
// be available before the accessor initializers run, so the only way forward is
// to observe every attribute defined by @attr() on all classes.
const ALL_ATTRIBUTES = new Set<string>();
type Metadata = {
attributes: Set<string>;
methods: WeakMap<Method<any, any>, Method<any, any>>;
};

// Can be rewritten to support decorator metadata once that's fixed.
function getMetadata(key: "attributes"): Set<string>;
function getMetadata(
key: "attributes",
context: { readonly metadata: DecoratorMetadata },
): Set<string>;
function getMetadata(
key: "methods",
context: object,
context: { readonly metadata: DecoratorMetadata },
): WeakMap<Method<any, any>, Method<any, any>>;
function getMetadata(key: "attributes" | "methods", context?: any): any {
function getMetadata(
key: "attributes" | "methods",
context: { readonly metadata: DecoratorMetadata },
): any {
const metadata = ((context.metadata[METADATA] as Metadata) ??= {
attributes: new Set(),
methods: new WeakMap(),
});
if (key === "attributes") {
return ALL_ATTRIBUTES;
}
let methodMap = ALL_DEBOUNCED_METHODS.get(context);
if (!methodMap) {
methodMap = new WeakMap();
ALL_DEBOUNCED_METHODS.set(context, methodMap);
return metadata.attributes;
}
return methodMap;
return metadata.methods;
}

// Explained in @enhance()
Expand Down Expand Up @@ -119,7 +111,10 @@ export function enhance<T extends CustomElementConstructor>(): (
}

static get observedAttributes(): string[] {
return [...originalObservedAttributes, ...getMetadata("attributes")];
return [
...originalObservedAttributes,
...getMetadata("attributes", context),
];
}

connectedCallback(): void {
Expand Down Expand Up @@ -251,7 +246,7 @@ export function init<T extends HTMLElement>(): LifecycleDecorator<
assertContext(context, "init", "method/function");
runContextInitializerOnOrnamentInit(context, (instance: T): void => {
const method = context.access.get(instance);
(getMetadata("methods", instance).get(method) ?? method).call(instance);
(getMetadata("methods", context).get(method) ?? method).call(instance);
});
};
}
Expand Down Expand Up @@ -689,7 +684,7 @@ export function attr<T extends HTMLElement, V>(
// Add the name to the set of all observed attributes, even if "reflective"
// is false. The content attribute must in all cases be observed to enable
// the message bus to emit events.
getMetadata("attributes").add(contentAttrName);
getMetadata("attributes", context).add(contentAttrName);

// If the attribute needs to be observed and the accessor initializes,
// register the attribute handler callback with the current element
Expand Down Expand Up @@ -874,7 +869,7 @@ export function debounce<
return context.addInitializer(function (): void {
const func = context.access.get(this);
const debounced = fn(func).bind(this);
getMetadata("methods", this).set(debounced, func);
getMetadata("methods", context).set(debounced, func);
context.access.set(this, debounced as F);
});
}
Expand All @@ -885,7 +880,7 @@ export function debounce<
const debounced = fn(func);
if (!context.static) {
context.addInitializer(function (): void {
getMetadata("methods", this).set(debounced, func);
getMetadata("methods", context).set(debounced, func);
});
}
return debounced as F;
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ export * from "./bus.js";
export * from "./decorators.js";
export * from "./transformers.js";
export { type Transformer } from "./types.js";
export { NO_VALUE } from "./lib.js";
export { NO_VALUE, METADATA } from "./lib.js";
1 change: 1 addition & 0 deletions src/lib.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const NO_VALUE: unique symbol = Symbol.for("ORNAMENT_NO_VALUE");
export const METADATA: unique symbol = Symbol.for("ORNAMENT_METADATA");
export const EVENT_BUS_TARGET: unique symbol = Symbol.for("ORNAMENT_BUS");
export const isArray = Array.isArray;
export const EMPTY_OBJ = {};

0 comments on commit 635baac

Please sign in to comment.