@adobe/uix-core 0.6.3

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.
@@ -0,0 +1,83 @@
1
+ /*
2
+ Copyright 2022 Adobe. All rights reserved.
3
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may obtain a copy
5
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software distributed under
8
+ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ OF ANY KIND, either express or implied. See the License for the specific language
10
+ governing permissions and limitations under the License.
11
+ */
12
+
13
+ import { _customConsole, DebugLogger, Theme } from "./debuglog.js";
14
+ import { Emits, Unsubscriber } from "./types.js";
15
+
16
+ /**
17
+ * Adds methods for logging events
18
+ * @internal
19
+ */
20
+ export interface EmitterDebugLogger extends DebugLogger {
21
+ /**
22
+ * Listen to an event and pass the logger to the handler
23
+ * @internal
24
+ */
25
+ listen(
26
+ type: string,
27
+ listener: (logger: EmitterDebugLogger, ev: CustomEvent) => unknown
28
+ ): this;
29
+ }
30
+
31
+ /**
32
+ * Debugger for EventTarget objects like Hosts, Ports and Guests, which
33
+ * patches dispatchEvent to log events
34
+ * Adapter to attach console logging listeners to all events on an emitter.
35
+ * @internal
36
+ */
37
+ export function debugEmitter(
38
+ emitter: Emits,
39
+ opts: {
40
+ theme: Theme;
41
+ type?: string;
42
+ id?: string;
43
+ }
44
+ ): EmitterDebugLogger {
45
+ const logger = _customConsole(
46
+ opts.theme,
47
+ opts.type ||
48
+ (Object.getPrototypeOf(emitter) as typeof emitter).constructor.name,
49
+ opts.id || emitter.id
50
+ ) as EmitterDebugLogger;
51
+ const oldDispatch = emitter.dispatchEvent;
52
+ emitter.dispatchEvent = (event) => {
53
+ logger.pushState({ type: "event", name: event.type });
54
+ const retVal = oldDispatch.call(emitter, event) as boolean;
55
+ logger.popState();
56
+ return retVal;
57
+ };
58
+
59
+ const subscriptions: Unsubscriber[] = [];
60
+
61
+ const oldDetach = logger.detach;
62
+ logger.detach = () => {
63
+ oldDetach.call(logger);
64
+ subscriptions.forEach((unsubscribe) => unsubscribe());
65
+ };
66
+
67
+ /**
68
+ * Listens and passes a logger to callbacks
69
+ */
70
+ function listen(
71
+ type: string,
72
+ listener: (logger: EmitterDebugLogger, ev: CustomEvent) => unknown
73
+ ) {
74
+ subscriptions.push(
75
+ emitter.addEventListener(type, (event) => listener(logger, event))
76
+ );
77
+ return logger;
78
+ }
79
+
80
+ logger.listen = listen;
81
+
82
+ return logger;
83
+ }
@@ -0,0 +1,284 @@
1
+ /*
2
+ Copyright 2022 Adobe. All rights reserved.
3
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may obtain a copy
5
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software distributed under
8
+ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ OF ANY KIND, either express or implied. See the License for the specific language
10
+ governing permissions and limitations under the License.
11
+ */
12
+
13
+ /**
14
+ * Fancy looking console decorator.
15
+ * @hidden
16
+ * @internal
17
+ */
18
+
19
+ /** @internal */
20
+ const isDarkMode = () =>
21
+ typeof window.matchMedia === "function" &&
22
+ window.matchMedia("(prefers-color-scheme: dark)").matches;
23
+
24
+ /** @internal */
25
+ type Layout = {
26
+ padX: number;
27
+ padY: number;
28
+ rounded: number;
29
+ fontSize: number;
30
+ emphasis: Style;
31
+ };
32
+ /** @internal */
33
+ type HexColor = `#${string}` | "transparent";
34
+ /** @internal */
35
+ type Color = {
36
+ text: HexColor;
37
+ bg: HexColor;
38
+ hilight: HexColor;
39
+ shadow: HexColor;
40
+ };
41
+ /** @internal */
42
+ type ThemeSpec = Color & Layout;
43
+
44
+ /** @internal */
45
+ const Layouts: Record<string, Layout> = {
46
+ medium: {
47
+ padX: 5,
48
+ padY: 3,
49
+ rounded: 4,
50
+ fontSize: 100,
51
+ emphasis: "font-weight: bold;",
52
+ },
53
+ small: {
54
+ padX: 3,
55
+ padY: 1,
56
+ rounded: 2,
57
+ fontSize: 95,
58
+ emphasis: "font-style: italic;",
59
+ },
60
+ };
61
+
62
+ /** @internal */
63
+ const Colors: Record<string, Color> = {
64
+ yellow: {
65
+ text: "#333333",
66
+ bg: "#EBD932",
67
+ hilight: "#F7E434",
68
+ shadow: "#D1C12C",
69
+ },
70
+ green: {
71
+ text: "#333333",
72
+ bg: "#96EB5E",
73
+ hilight: "#9EF763",
74
+ shadow: "#85D154",
75
+ },
76
+ blue: {
77
+ text: "#333333",
78
+ bg: "#8DD0EB",
79
+ hilight: "#88F0F7",
80
+ shadow: "#74AED4",
81
+ },
82
+ gray: isDarkMode()
83
+ ? {
84
+ text: "#eeeeee",
85
+ bg: "transparent",
86
+ hilight: "#cecece",
87
+ shadow: "#cecece",
88
+ }
89
+ : {
90
+ text: "#333333",
91
+ bg: "#eeeeee",
92
+ hilight: "#f6f6f6",
93
+ shadow: "#cecece",
94
+ },
95
+ };
96
+
97
+ /** @internal */
98
+ type ThemeTag = `${keyof typeof Colors} ${keyof typeof Layouts}`;
99
+
100
+ /**
101
+ * @internal
102
+ */
103
+ export type Theme = ThemeSpec | ThemeTag;
104
+
105
+ /** @internal */
106
+ type LogDecorator = (...args: unknown[]) => unknown[];
107
+
108
+ /** @internal */
109
+ type Style = `${string};`;
110
+
111
+ function memoizeUnary<T, U>(fn: (arg: T) => U): typeof fn {
112
+ const cache: Map<T, U> = new Map();
113
+ return (arg) => {
114
+ if (!cache.has(arg)) {
115
+ const result = fn(arg);
116
+ cache.set(arg, result);
117
+ if (cache.size > 100) {
118
+ cache.delete(cache.keys().next().value as T);
119
+ }
120
+ return result;
121
+ }
122
+ return cache.get(arg);
123
+ };
124
+ }
125
+
126
+ const toTheme = memoizeUnary((theme: Theme): ThemeSpec => {
127
+ if (typeof theme === "string") {
128
+ const [color, size] = theme.split(" ");
129
+ return {
130
+ ...Colors[color],
131
+ ...Layouts[size],
132
+ };
133
+ }
134
+ return theme;
135
+ });
136
+
137
+ const block: Style = `display: inline-block; border: 1px solid;`;
138
+
139
+ const flatten = (side: "left" | "right"): Style =>
140
+ `padding-${side}: 0px; border-${side}-width: 0px; border-top-${side}-radius: 0px; border-bottom-${side}-radius: 0px;`;
141
+
142
+ const toColor = ({ bg, hilight, shadow, text }: Color): Style =>
143
+ `color: ${text}; background: ${bg}; border-color: ${hilight} ${shadow} ${shadow} ${hilight};`;
144
+
145
+ const toLayout = ({ fontSize, padY, padX, rounded }: Layout) =>
146
+ `font-size: ${fontSize}%; padding: ${padY}px ${padX}px; border-radius: ${rounded}px;`;
147
+
148
+ const toBubbleStyle = memoizeUnary((theme: ThemeSpec): string[] => {
149
+ const base = `${block}${toColor(theme)}${toLayout(theme)}`;
150
+ return [
151
+ `${base}${flatten("right")}`,
152
+ `${base}${flatten("left")}${theme.emphasis}`,
153
+ ] as Style[];
154
+ });
155
+
156
+ function toBubblePrepender(
157
+ bubbleLeft: string,
158
+ bubbleRight: string,
159
+ theme: ThemeSpec
160
+ ): LogDecorator {
161
+ const prefix = `%c${bubbleLeft}%c ${bubbleRight}`;
162
+ const [left, right] = toBubbleStyle(theme);
163
+ return (args: unknown[]) => {
164
+ const bubbleArgs = [prefix, left, right];
165
+ if (typeof args[0] === "string") {
166
+ bubbleArgs[0] = `${prefix}%c ${args.shift() as string}`;
167
+ bubbleArgs.push(""); // reset style
168
+ }
169
+ return [...bubbleArgs, ...args];
170
+ };
171
+ }
172
+
173
+ /** @internal */
174
+ const stateTypes = {
175
+ event: "️⚡️",
176
+ } as const;
177
+
178
+ const stateDelim = " ⤻ ";
179
+
180
+ /** @internal */
181
+ type DebugState = { type: keyof typeof stateTypes; name: string };
182
+
183
+ // Serialize to memoize.
184
+ const getStateFormatter = memoizeUnary((stateJson: string) => {
185
+ const stateStack = JSON.parse(stateJson) as unknown as DebugState[];
186
+ const firstState = stateStack.shift();
187
+ const left = stateTypes[firstState.type];
188
+ const right = [
189
+ firstState.name,
190
+ ...stateStack.map((state) => `${stateTypes[state.type]} ${state.name}`),
191
+ ].join(stateDelim);
192
+ return toBubblePrepender(left, right, toTheme("gray small"));
193
+ });
194
+ const getStatePrepender = (stateStack: DebugState[]) =>
195
+ getStateFormatter(JSON.stringify(stateStack));
196
+
197
+ const overrideMethods = ["log", "error", "warn", "info", "debug"] as const;
198
+
199
+ const identity = <T>(x: T) => x;
200
+
201
+ const noop = (): (() => undefined) => undefined;
202
+
203
+ /**
204
+ * A console, plus some methods to track event lifecycles.
205
+ * @internal
206
+ */
207
+ export interface DebugLogger extends Console {
208
+ /**
209
+ * Stop all logging; methods do nothing
210
+ * @internal
211
+ */
212
+ detach(): void;
213
+ /**
214
+ * Add an event bubble to the log during handler.
215
+ */
216
+ pushState(state: DebugState): void;
217
+ /**
218
+ * Remove the bubble when event is done dispatching
219
+ */
220
+ popState(): void;
221
+ }
222
+
223
+ /**
224
+ * Returns a console whose methods autoformat with bubbles.
225
+ * @internal
226
+ */
227
+ export function _customConsole(
228
+ theme: Theme,
229
+ type: string,
230
+ name: string
231
+ ): DebugLogger {
232
+ const prepender = toBubblePrepender(`X${type}`, name, toTheme(theme));
233
+ let statePrepender: LogDecorator = identity as LogDecorator;
234
+ const stateStack: DebugState[] = [];
235
+ const loggerProto: PropertyDescriptorMap = {
236
+ detach: {
237
+ writable: true,
238
+ configurable: true,
239
+ value(this: DebugLogger) {
240
+ overrideMethods.forEach((method) => {
241
+ this[method] = noop;
242
+ });
243
+ },
244
+ },
245
+ pushState: {
246
+ value(state: DebugState) {
247
+ stateStack.push(state);
248
+ statePrepender = getStatePrepender(stateStack);
249
+ },
250
+ },
251
+ popState: {
252
+ value() {
253
+ stateStack.pop();
254
+ statePrepender =
255
+ stateStack.length === 0
256
+ ? (identity as LogDecorator)
257
+ : getStatePrepender(stateStack);
258
+ },
259
+ },
260
+ };
261
+ const customConsole = Object.create(
262
+ console,
263
+ overrideMethods.reduce((out, level) => {
264
+ out[level] = {
265
+ writable: true,
266
+ configurable: true,
267
+ value(...args: unknown[]) {
268
+ console[level](...prepender(statePrepender(args)));
269
+ },
270
+ };
271
+ return out;
272
+ }, loggerProto)
273
+ ) as DebugLogger;
274
+ return customConsole;
275
+ }
276
+
277
+ /**
278
+ * @internal
279
+ */
280
+ export const quietConsole = new Proxy(console, {
281
+ get() {
282
+ return noop;
283
+ },
284
+ });
package/src/emitter.ts ADDED
@@ -0,0 +1,90 @@
1
+ /*
2
+ Copyright 2022 Adobe. All rights reserved.
3
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may obtain a copy
5
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software distributed under
8
+ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ OF ANY KIND, either express or implied. See the License for the specific language
10
+ governing permissions and limitations under the License.
11
+ */
12
+
13
+ import { Emits, Unsubscriber, NamedEvent } from "./types.js";
14
+
15
+ /**
16
+ * Browser-native {@link https://developer.mozilla.org/en-US/docs/Web/API/EventTarget | EventTarget}
17
+ * whose {@link Emitter.addEventListener} method returns an anonymous function
18
+ * which unsubscribes the original handler.
19
+ *
20
+ * Also provides typed events via generics. You can create or extend this class
21
+ * to define custom emitters with known event names and signatures.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * import type { NamedEvent, Emitter } from '@adobe/uix-sdk'
26
+ *
27
+ * class FizzBuzzEmitter extends Emitter<
28
+ * NamedEvent<"fizz", { fizzCount: number }> |
29
+ * NamedEvent<"buzz", { buzzCount: number }> |
30
+ * NamedEvent<"fizzbuzz">
31
+ * > {
32
+ * }
33
+ * ```
34
+ * The `FizzBuzzEmitter` class will now type check its events and event
35
+ * listeners, providing autosuggest in editors.
36
+ *
37
+ * @see [EventTarget - MDN](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget)
38
+ *
39
+ * @public
40
+ */
41
+ export class Emitter<Events extends NamedEvent>
42
+ extends EventTarget
43
+ implements Emits<Events>
44
+ {
45
+ /**
46
+ * An arbitrary string to uniquely identify this emitter and its events.
47
+ * @public
48
+ */
49
+ id: string;
50
+ constructor(id: string) {
51
+ super();
52
+ this.id = id;
53
+ }
54
+ /**
55
+ * Convenience method to construct and dispatch custom events.
56
+ *
57
+ * @param type - Name of one of the allowed events this can emit
58
+ * @param detail - Object to expose in the {@link https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail | CustomEvent#detail}
59
+ * property.
60
+ * @public
61
+ */
62
+ protected emit<Event extends Events>(
63
+ type: Event["type"],
64
+ detail: Event["detail"]
65
+ ): void {
66
+ const event = new CustomEvent<typeof detail>(type, { detail });
67
+ this.dispatchEvent(event);
68
+ }
69
+ /**
70
+ * Subscribe to an event and receive an unsubscribe callback.
71
+ * @see [EventTarget.addEventListener - MDN](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener)
72
+ *
73
+ * Identical to `EventTarget.addEventListener`, but returns an "unsubscriber"
74
+ * function which detaches the listener when invoked. Solves an ergonomic
75
+ * problem with native EventTargets where it's impossible to detach listeners
76
+ * without having a reference to the original handler.
77
+ *
78
+ * @typeParam E - Name of one of the allowed events this can emit
79
+ * @param type - Event type
80
+ * @param listener - Event handler
81
+ * @returns Call to unsubscribe listener.
82
+ */
83
+ addEventListener<
84
+ Type extends Events["type"],
85
+ Event extends Extract<Events, { type: Type }>
86
+ >(type: Type, listener: (ev: Event) => unknown): Unsubscriber {
87
+ super.addEventListener(type, listener);
88
+ return () => super.removeEventListener(type, listener);
89
+ }
90
+ }
package/src/index.ts ADDED
@@ -0,0 +1,23 @@
1
+ /*
2
+ Copyright 2022 Adobe. All rights reserved.
3
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may obtain a copy
5
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software distributed under
8
+ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ OF ANY KIND, either express or implied. See the License for the specific language
10
+ governing permissions and limitations under the License.
11
+ */
12
+
13
+ /**
14
+ * Core utilities, types and contracts for the Adobe UIX SDK.
15
+ * @packageDocumentation
16
+ */
17
+
18
+ export * from "./debug-emitter";
19
+ export * from "./debuglog";
20
+ export * from "./emitter";
21
+ export * from "./namespace-proxy";
22
+ export * from "./timeout-promise";
23
+ export * from "./types";
@@ -0,0 +1,82 @@
1
+ /*
2
+ Copyright 2022 Adobe. All rights reserved.
3
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may obtain a copy
5
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software distributed under
8
+ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ OF ANY KIND, either express or implied. See the License for the specific language
10
+ governing permissions and limitations under the License.
11
+ */
12
+
13
+ /* eslint-disable @typescript-eslint/no-explicit-any */
14
+ import { RemoteHostApis, RemoteMethodInvoker } from "./types.js";
15
+
16
+ /**
17
+ * Build a fake object that turns "method calls" into RPC messages
18
+ * The resulting object will recursively make more fake proxies on demand until
19
+ * one of the looked-up properties is invoked as a function.
20
+ * Then it will call the passed `invoke` method with a {@link HostMethodAddress}
21
+ * that can send the method invocation as an RPC message to another realm.
22
+ *
23
+ * @example
24
+ * ```js
25
+ * const invoker = (methodAddress) => console.log(
26
+ * address.path,
27
+ * address.name,
28
+ * address.args
29
+ * );
30
+ * const ns = makeNamespaceProxy(invoker);
31
+ *
32
+ * // looking up any property on the object will work
33
+ *
34
+ * ns.example.builds.method.call.message("foo", 1);
35
+ *
36
+ * // Console will log:
37
+ * ['example','builds','method','call']
38
+ * 'message'
39
+ * ["foo", 1]
40
+ *```
41
+ * @internal
42
+ *
43
+ * @param invoke - Callback that receives address
44
+ */
45
+ export function makeNamespaceProxy<ProxiedApi extends object>(
46
+ invoke: RemoteMethodInvoker<unknown>,
47
+ path: string[] = []
48
+ ): RemoteHostApis<ProxiedApi> {
49
+ const handler: ProxyHandler<Record<string, any>> = {
50
+ get: (target, prop) => {
51
+ if (typeof prop === "string") {
52
+ if (!Reflect.has(target, prop)) {
53
+ const next = makeNamespaceProxy(invoke, path.concat(prop));
54
+ Reflect.set(target, prop, next);
55
+ }
56
+ return Reflect.get(target, prop) as unknown;
57
+ } else {
58
+ throw new Error(
59
+ `Cannot look up a symbol ${String(prop)} on a host connection proxy.`
60
+ );
61
+ }
62
+ },
63
+ };
64
+ const target = {} as unknown as RemoteHostApis<ProxiedApi>;
65
+ // Only trap the apply if there's at least two levels of namespace.
66
+ // uix.host() is not a function, and neither is uix.host.bareMethod().
67
+ if (path.length < 2) {
68
+ return new Proxy<RemoteHostApis<ProxiedApi>>(target, handler);
69
+ }
70
+ const invoker = (...args: unknown[]) =>
71
+ invoke({
72
+ path: path.slice(0, -1),
73
+ name: path[path.length - 1],
74
+ args,
75
+ });
76
+ return new Proxy<typeof invoker>(invoker, {
77
+ ...handler,
78
+ apply(target, _, args: unknown[]) {
79
+ return target(...args);
80
+ },
81
+ }) as unknown as typeof target;
82
+ }
@@ -0,0 +1,36 @@
1
+ /*
2
+ Copyright 2022 Adobe. All rights reserved.
3
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may obtain a copy
5
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software distributed under
8
+ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ OF ANY KIND, either express or implied. See the License for the specific language
10
+ governing permissions and limitations under the License.
11
+ */
12
+
13
+ /**
14
+ * Add a timeout to a Promise. The returned Promise will resolve to the value of
15
+ * the original Promise, but if it doesn't resolve within the timeout interval,
16
+ * it will reject with a timeout error.
17
+ * @internal
18
+ *
19
+ * @param timeoutMs - Time to wait (ms) before rejecting
20
+ * @param promise - Original promise to set a timeout for
21
+ * @returns - Promise that rejects after X milliseconds have passed
22
+ */
23
+ export function timeoutPromise<T>(timeoutMs: number, promise: Promise<T>) {
24
+ return new Promise((resolve, reject) => {
25
+ const timeout = setTimeout(
26
+ () => reject(new Error(`Timed out after ${timeoutMs}ms`)),
27
+ timeoutMs
28
+ );
29
+ promise
30
+ .then((result) => {
31
+ clearTimeout(timeout);
32
+ resolve(result);
33
+ })
34
+ .catch(reject);
35
+ });
36
+ }