@expressive/mvc 0.73.0 → 0.78.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 +137 -0
- package/dist/component.d.ts +87 -0
- package/dist/component.js +111 -0
- package/dist/component.js.map +1 -0
- package/dist/context.d.ts +56 -0
- package/dist/context.js +201 -0
- package/dist/context.js.map +1 -0
- package/dist/field/def.d.ts +17 -0
- package/dist/field/def.js +29 -0
- package/dist/field/def.js.map +1 -0
- package/dist/field/get.d.ts +57 -0
- package/dist/field/get.js +90 -0
- package/dist/field/get.js.map +1 -0
- package/dist/field/hot.d.ts +16 -0
- package/dist/field/hot.js +98 -0
- package/dist/field/hot.js.map +1 -0
- package/dist/field/ref.d.ts +78 -0
- package/dist/field/ref.js +85 -0
- package/dist/field/ref.js.map +1 -0
- package/dist/field/set.d.ts +59 -0
- package/dist/field/set.js +84 -0
- package/dist/field/set.js.map +1 -0
- package/dist/index.d.ts +10 -502
- package/dist/index.js +11 -872
- package/dist/jsx-dev-runtime.d.ts +2 -0
- package/dist/jsx-dev-runtime.js +3 -0
- package/dist/jsx-runtime.d.ts +90 -0
- package/dist/jsx-runtime.js +57 -0
- package/dist/jsx-runtime.js.map +1 -0
- package/dist/observable.d.ts +58 -0
- package/dist/observable.js +184 -0
- package/dist/observable.js.map +1 -0
- package/dist/state.d.ts +311 -0
- package/dist/state.js +424 -0
- package/dist/state.js.map +1 -0
- package/package.json +35 -18
- package/dist/esm/context.d.ts +0 -62
- package/dist/esm/context.js +0 -158
- package/dist/esm/index.d.ts +0 -8
- package/dist/esm/index.js +0 -9
- package/dist/esm/instruction/get.d.ts +0 -36
- package/dist/esm/instruction/get.js +0 -77
- package/dist/esm/instruction/ref.d.ts +0 -71
- package/dist/esm/instruction/ref.js +0 -68
- package/dist/esm/instruction/set.d.ts +0 -69
- package/dist/esm/instruction/set.js +0 -118
- package/dist/esm/instruction/use.d.ts +0 -23
- package/dist/esm/instruction/use.js +0 -56
- package/dist/esm/observable.d.ts +0 -42
- package/dist/esm/observable.js +0 -121
- package/dist/esm/state.d.ts +0 -234
- package/dist/esm/state.js +0 -293
- package/dist/index.js.map +0 -1
package/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<h1 align="center">@expressive/mvc</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
Class-based reactive state management - framework-agnostic core.
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/@expressive/mvc"><img alt="NPM" src="https://badge.fury.io/js/%40expressive%2Fmvc.svg"></a>
|
|
9
|
+
<img src="https://img.shields.io/badge/Coverage-100%25-brightgreen.svg">
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
The core of [Expressive MVC](https://github.com/gabeklein/expressive-mvc): reactive primitives built around plain classes, with no framework dependency. Provides the `State` model, instructions, context, and the renderer-agnostic `Component` that adapters like [`@expressive/react`](https://www.npmjs.com/package/@expressive/react) build on.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @expressive/mvc
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## State
|
|
21
|
+
|
|
22
|
+
A `State` is a class whose fields are reactive - read to subscribe, assign to update. Getters are cached computed values.
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { State } from '@expressive/mvc';
|
|
26
|
+
|
|
27
|
+
class Counter extends State {
|
|
28
|
+
count = 0;
|
|
29
|
+
increment = () => this.count++;
|
|
30
|
+
|
|
31
|
+
get doubled() {
|
|
32
|
+
return this.count * 2; // recomputes only when count changes
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const counter = Counter.new();
|
|
37
|
+
|
|
38
|
+
// effect runs now, then again whenever read values change
|
|
39
|
+
counter.get(({ count }) => {
|
|
40
|
+
console.log(`count is ${count}`);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
counter.increment(); // -> "count is 1"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Models run and test anywhere - no renderer required.
|
|
47
|
+
|
|
48
|
+
## Instructions
|
|
49
|
+
|
|
50
|
+
Field initializers that change how a property behaves:
|
|
51
|
+
|
|
52
|
+
| | |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `ref()` | a mutable reference (e.g. a DOM node) that's still reactive |
|
|
55
|
+
| `set()` | computed values, smart setters, side-effects on assignment |
|
|
56
|
+
| `get()` | dependency injection - pull another `State` from context |
|
|
57
|
+
| `hot()` | a shallow-reactive array or object |
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { State, set, hot } from '@expressive/mvc';
|
|
61
|
+
|
|
62
|
+
class Cart extends State {
|
|
63
|
+
items = hot<Item[]>([]); // reactive collection
|
|
64
|
+
coupon = set('', code => apply(code)); // side-effect on assignment
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Lifecycle
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
class Timer extends State {
|
|
72
|
+
seconds = 0;
|
|
73
|
+
|
|
74
|
+
new() { // runs once on activation
|
|
75
|
+
const id = setInterval(() => this.seconds++, 1000);
|
|
76
|
+
return () => clearInterval(id); // returned cleanup runs on destroy
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const timer = Timer.new(); // construct + activate
|
|
81
|
+
timer.set(null); // destroy - runs cleanup, freezes state
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Context & composition
|
|
85
|
+
|
|
86
|
+
States find each other through context, and compose by holding one another:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { State, get } from '@expressive/mvc';
|
|
90
|
+
|
|
91
|
+
class Session extends State {
|
|
92
|
+
user = 'guest';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
class Profile extends State {
|
|
96
|
+
session = get(Session); // injected from the nearest provider
|
|
97
|
+
address = new Address(); // nested state - its changes bubble up
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
An adapter (e.g. [`@expressive/react`](https://www.npmjs.com/package/@expressive/react)) supplies the provider that places a `State` in a tree; `get()` pulls it back out.
|
|
102
|
+
|
|
103
|
+
## Events
|
|
104
|
+
|
|
105
|
+
Any property or arbitrary key can be dispatched and listened to:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
const off = counter.set('refresh', () => reload()); // listen
|
|
109
|
+
counter.set('refresh'); // dispatch
|
|
110
|
+
off(); // stop listening
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Framework-agnostic components
|
|
114
|
+
|
|
115
|
+
`@expressive/mvc` ships its own JSX runtime, so you can author components - including reusable libraries - against the core and let any host render them:
|
|
116
|
+
|
|
117
|
+
```jsonc
|
|
118
|
+
// tsconfig.json
|
|
119
|
+
{
|
|
120
|
+
"compilerOptions": {
|
|
121
|
+
"jsx": "react-jsx",
|
|
122
|
+
"jsxImportSource": "@expressive/mvc"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`Component` - a `State` that renders - lives here as the agnostic base. For using it in an app (rendering, props, subcomponents, suspense), see **[`@expressive/react`](https://www.npmjs.com/package/@expressive/react)**.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
To render state in a UI framework, add an adapter: [`@expressive/react`](https://www.npmjs.com/package/@expressive/react) or `@expressive/preact`.
|
|
132
|
+
|
|
133
|
+
Full guide and API reference → **[github.com/gabeklein/expressive-mvc](https://github.com/gabeklein/expressive-mvc)**
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { State } from "./state.js";
|
|
2
|
+
import { Host } from "./jsx-runtime.js";
|
|
3
|
+
|
|
4
|
+
//#region src/component.d.ts
|
|
5
|
+
type IfEquals<X, Y, A, B> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? A : B;
|
|
6
|
+
/** Keys of T which are settable (excludes get-only accessors and `readonly`). */
|
|
7
|
+
type Acceptable<T> = { [P in keyof T]-?: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P, never> }[keyof T];
|
|
8
|
+
declare namespace Component {
|
|
9
|
+
/**
|
|
10
|
+
* Host element type produced by `Component.render`. Delegates to the
|
|
11
|
+
* {@link Host} manifest on the jsx-runtime entry; resolves to `unknown`
|
|
12
|
+
* until an adapter augments it with `node`.
|
|
13
|
+
*/
|
|
14
|
+
type Node = Host extends {
|
|
15
|
+
node: infer T;
|
|
16
|
+
} ? T : unknown;
|
|
17
|
+
interface BaseProps<T extends Component> {
|
|
18
|
+
/**
|
|
19
|
+
* Callback for newly created instance. Only called once.
|
|
20
|
+
*
|
|
21
|
+
* Runs after props apply but **before** the `new()` lifecycle hook - so it
|
|
22
|
+
* may configure state that `new()` then observes. To react to a fully
|
|
23
|
+
* initialized instance instead, use `watch` or an effect.
|
|
24
|
+
*/
|
|
25
|
+
is?: (instance: T) => void;
|
|
26
|
+
/**
|
|
27
|
+
* Fallback to show when suspended or in error recovery.
|
|
28
|
+
* Pass `false` to opt out of the component's own suspense boundary,
|
|
29
|
+
* letting suspension bubble to an ancestor.
|
|
30
|
+
*/
|
|
31
|
+
fallback?: Component.Node;
|
|
32
|
+
}
|
|
33
|
+
type StateProps<T extends State> = { [K in Exclude<keyof T, keyof Component> & Acceptable<T>]?: T[K] };
|
|
34
|
+
type RenderProps<T> = [T] extends [(props: infer P) => any] ? [keyof NonNullable<P>] extends [never] ? {
|
|
35
|
+
children?: Component.Node;
|
|
36
|
+
} : NonNullable<P> : {
|
|
37
|
+
children?: Component.Node;
|
|
38
|
+
};
|
|
39
|
+
type Props<T extends Component> = StateProps<T> & BaseProps<T> & RenderProps<T['render']>;
|
|
40
|
+
}
|
|
41
|
+
declare class Component extends State {
|
|
42
|
+
/**
|
|
43
|
+
* All JSX attributes passed to this component.
|
|
44
|
+
* Includes state-derived props, render props, and built-in props like `is` and `fallback`.
|
|
45
|
+
*
|
|
46
|
+
* Will incorperate extra props you declare as props parameter in `render` method.
|
|
47
|
+
*/
|
|
48
|
+
readonly props: Component.Props<this>;
|
|
49
|
+
/**
|
|
50
|
+
* Content to display while this component or its children are suspended.
|
|
51
|
+
* Will cover suspense by pending properties in `this.render` as well.
|
|
52
|
+
* Can be set as a class property or overridden via the `fallback` JSX prop.
|
|
53
|
+
*
|
|
54
|
+
* Defaults to null - nothing will be rendered.
|
|
55
|
+
* Set `false` to opt out of having a boundary at all - suspension then
|
|
56
|
+
* bubbles to the nearest ancestor boundary.
|
|
57
|
+
*/
|
|
58
|
+
fallback: Component.Node;
|
|
59
|
+
constructor(props?: any, ...rest: any[]);
|
|
60
|
+
/**
|
|
61
|
+
* Output for this component. Override to define custom JSX.
|
|
62
|
+
*
|
|
63
|
+
* Properties accessed via `this` are reactive and trigger a render when they
|
|
64
|
+
* change, in addition to props. Accepts an optional parameter to receive extra
|
|
65
|
+
* props from JSX, beyond those merged to state properties; declare its shape
|
|
66
|
+
* via `props = {} as { ... }`. Without a parameter (the default below),
|
|
67
|
+
* children pass through.
|
|
68
|
+
*
|
|
69
|
+
* The constructor installs a composed `render` per instance; the bootstrap
|
|
70
|
+
* `type` pass seals this and any override non-configurable, keeping it the
|
|
71
|
+
* content-render seam the chain reads.
|
|
72
|
+
*/
|
|
73
|
+
render(props?: {}): Component.Node;
|
|
74
|
+
/**
|
|
75
|
+
* Called when a child component throws during render.
|
|
76
|
+
* While this is pending, `fallback` is displayed.
|
|
77
|
+
* When resolved, the error boundary resets and `render` is called again.
|
|
78
|
+
*
|
|
79
|
+
* Override to handle errors - set `this.fallback` for error-specific UI,
|
|
80
|
+
* await async recovery, or await user interaction before retrying. If you
|
|
81
|
+
* assign a fallback within catch, it will be reverted after resolved.
|
|
82
|
+
*/
|
|
83
|
+
catch?(error: Error): Promise<void> | void;
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
export { Component };
|
|
87
|
+
//# sourceMappingURL=component.d.ts.map
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { State, unbind } from "./state.js";
|
|
2
|
+
import { Context } from "./context.js";
|
|
3
|
+
import { set } from "./field/set.js";
|
|
4
|
+
|
|
5
|
+
//#region src/component.ts
|
|
6
|
+
const PENDING = /* @__PURE__ */ new WeakMap();
|
|
7
|
+
/** Per-class composed content render. */
|
|
8
|
+
const CHAIN = /* @__PURE__ */ new WeakMap();
|
|
9
|
+
var Component = class extends State {
|
|
10
|
+
constructor(props, ...rest) {
|
|
11
|
+
if (props == null) props = {};
|
|
12
|
+
const seen = {};
|
|
13
|
+
const copy = PENDING.get(props);
|
|
14
|
+
if (typeof props == "object") merge(props);
|
|
15
|
+
function merge(props) {
|
|
16
|
+
for (const k in props) seen[k] = void 0;
|
|
17
|
+
return {
|
|
18
|
+
...seen,
|
|
19
|
+
...props
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
super(copy ? [] : [
|
|
23
|
+
props,
|
|
24
|
+
rest.filter((x) => !(x instanceof Context)),
|
|
25
|
+
() => {
|
|
26
|
+
props.is?.(this);
|
|
27
|
+
Object.defineProperty(this, "props", { enumerable: false });
|
|
28
|
+
PENDING.delete(props);
|
|
29
|
+
}
|
|
30
|
+
]);
|
|
31
|
+
this.fallback = set(null);
|
|
32
|
+
if (copy) return copy;
|
|
33
|
+
PENDING.set(props, this);
|
|
34
|
+
this.props = props;
|
|
35
|
+
this.set("props", () => {
|
|
36
|
+
this.set(merge(this.props));
|
|
37
|
+
});
|
|
38
|
+
Object.defineProperty(this, "render", {
|
|
39
|
+
writable: true,
|
|
40
|
+
configurable: true,
|
|
41
|
+
value: render(this)
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Output for this component. Override to define custom JSX.
|
|
46
|
+
*
|
|
47
|
+
* Properties accessed via `this` are reactive and trigger a render when they
|
|
48
|
+
* change, in addition to props. Accepts an optional parameter to receive extra
|
|
49
|
+
* props from JSX, beyond those merged to state properties; declare its shape
|
|
50
|
+
* via `props = {} as { ... }`. Without a parameter (the default below),
|
|
51
|
+
* children pass through.
|
|
52
|
+
*
|
|
53
|
+
* The constructor installs a composed `render` per instance; the bootstrap
|
|
54
|
+
* `type` pass seals this and any override non-configurable, keeping it the
|
|
55
|
+
* content-render seam the chain reads.
|
|
56
|
+
*/
|
|
57
|
+
render(props) {
|
|
58
|
+
const { children } = props || this.props;
|
|
59
|
+
return children || null;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Seal each class's own `render` non-configurable at bootstrap so member
|
|
64
|
+
* classification leaves it unbound - it stays the content-render seam the chain
|
|
65
|
+
* reads (and which adapters like preact detect as the class-component marker).
|
|
66
|
+
*/
|
|
67
|
+
Component.on({ type(type) {
|
|
68
|
+
const desc = Object.getOwnPropertyDescriptor(type.prototype, "render");
|
|
69
|
+
if (desc && typeof desc.value == "function") Object.defineProperty(type.prototype, "render", {
|
|
70
|
+
...desc,
|
|
71
|
+
configurable: false
|
|
72
|
+
});
|
|
73
|
+
} });
|
|
74
|
+
/**
|
|
75
|
+
* Build (or fetch the cached) composed content render for an instance's class.
|
|
76
|
+
* Walks the prototype chain for content renders strictly below Component - the
|
|
77
|
+
* default render on Component.prototype is excluded and serves as the fallback
|
|
78
|
+
* when a class authors none.
|
|
79
|
+
*/
|
|
80
|
+
function render(target) {
|
|
81
|
+
const cached = CHAIN.get(target.constructor);
|
|
82
|
+
if (cached) return cached;
|
|
83
|
+
let render;
|
|
84
|
+
let proto = target;
|
|
85
|
+
while ((proto = Object.getPrototypeOf(proto)) !== Component.prototype) {
|
|
86
|
+
const desc = Object.getOwnPropertyDescriptor(proto, "render");
|
|
87
|
+
if (desc) {
|
|
88
|
+
const layer = unbind(desc.get || desc.value);
|
|
89
|
+
render = render ? compose(layer, render) : layer;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!render) render = Component.prototype.render;
|
|
93
|
+
CHAIN.set(target.constructor, render);
|
|
94
|
+
return render;
|
|
95
|
+
}
|
|
96
|
+
/** Wrap an outer render so it receives `inner` as a lazy `children` getter. */
|
|
97
|
+
function compose(outer, inner) {
|
|
98
|
+
return function(props) {
|
|
99
|
+
const self = this;
|
|
100
|
+
return outer.call(self, {
|
|
101
|
+
...props,
|
|
102
|
+
get children() {
|
|
103
|
+
return inner.call(self, props);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
//#endregion
|
|
110
|
+
export { Component };
|
|
111
|
+
//# sourceMappingURL=component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"component.js","names":[],"sources":["../src/component.ts"],"sourcesContent":["import { Context } from './context';\nimport { set } from './field/set';\nimport { State, unbind } from './state';\n\nimport type { Host } from './jsx-runtime';\n\nconst PENDING = new WeakMap<object, Component>();\n\n/** Per-class composed content render. */\nconst CHAIN = new WeakMap<Function, Function>();\n\ntype IfEquals<X, Y, A, B> =\n (<T>() => T extends X ? 1 : 2) extends\n (<T>() => T extends Y ? 1 : 2) ? A : B;\n\n/** Keys of T which are settable (excludes get-only accessors and `readonly`). */\ntype Acceptable<T> = {\n [P in keyof T]-?: IfEquals<\n { [Q in P]: T[P] },\n { -readonly [Q in P]: T[P] },\n P, never\n >;\n}[keyof T];\n\ndeclare namespace Component {\n /**\n * Host element type produced by `Component.render`. Delegates to the\n * {@link Host} manifest on the jsx-runtime entry; resolves to `unknown`\n * until an adapter augments it with `node`.\n */\n type Node = Host extends { node: infer T } ? T : unknown;\n\n interface BaseProps<T extends Component> {\n /**\n * Callback for newly created instance. Only called once.\n *\n * Runs after props apply but **before** the `new()` lifecycle hook - so it\n * may configure state that `new()` then observes. To react to a fully\n * initialized instance instead, use `watch` or an effect.\n */\n is?: (instance: T) => void;\n\n /**\n * Fallback to show when suspended or in error recovery.\n * Pass `false` to opt out of the component's own suspense boundary,\n * letting suspension bubble to an ancestor.\n */\n fallback?: Component.Node;\n }\n\n type StateProps<T extends State> = {\n [K in Exclude<keyof T, keyof Component> & Acceptable<T>]?: T[K];\n };\n\n type RenderProps<T> = [T] extends [(props: infer P) => any]\n ? [keyof NonNullable<P>] extends [never]\n ? { children?: Component.Node }\n : NonNullable<P>\n : { children?: Component.Node };\n\n type Props<T extends Component> =\n & StateProps<T>\n & BaseProps<T>\n & RenderProps<T['render']>;\n}\n\nclass Component extends State {\n /**\n * All JSX attributes passed to this component.\n * Includes state-derived props, render props, and built-in props like `is` and `fallback`.\n *\n * Will incorperate extra props you declare as props parameter in `render` method.\n */\n declare readonly props: Component.Props<this>;\n\n /**\n * Content to display while this component or its children are suspended.\n * Will cover suspense by pending properties in `this.render` as well.\n * Can be set as a class property or overridden via the `fallback` JSX prop.\n *\n * Defaults to null - nothing will be rendered.\n * Set `false` to opt out of having a boundary at all - suspension then\n * bubbles to the nearest ancestor boundary.\n */\n fallback: Component.Node = set(null);\n\n constructor(props?: any, ...rest: any[]) {\n if (props == null) props = {};\n\n const seen = {} as Record<string, undefined>;\n const copy = PENDING.get(props);\n\n if (typeof props == 'object') merge(props);\n\n function merge(props: {}) {\n for (const k in props) seen[k] = undefined;\n return { ...seen, ...props };\n }\n\n super(copy ? [] : [\n props,\n rest.filter((x) => !(x instanceof Context)),\n () => {\n props.is?.(this);\n Object.defineProperty(this, 'props', { enumerable: false });\n PENDING.delete(props);\n }\n ]);\n\n if (copy) return copy;\n\n PENDING.set(props, this);\n\n this.props = props;\n this.set('props', () => {\n this.set(merge(this.props));\n });\n\n Object.defineProperty(this, 'render', {\n writable: true,\n configurable: true,\n value: render(this)\n });\n }\n\n /**\n * Output for this component. Override to define custom JSX.\n *\n * Properties accessed via `this` are reactive and trigger a render when they\n * change, in addition to props. Accepts an optional parameter to receive extra\n * props from JSX, beyond those merged to state properties; declare its shape\n * via `props = {} as { ... }`. Without a parameter (the default below),\n * children pass through.\n *\n * The constructor installs a composed `render` per instance; the bootstrap\n * `type` pass seals this and any override non-configurable, keeping it the\n * content-render seam the chain reads.\n */\n render(props?: {}): Component.Node {\n const { children } = (props || this.props) as { children?: Component.Node };\n return children || null;\n }\n\n /**\n * Called when a child component throws during render.\n * While this is pending, `fallback` is displayed.\n * When resolved, the error boundary resets and `render` is called again.\n *\n * Override to handle errors - set `this.fallback` for error-specific UI,\n * await async recovery, or await user interaction before retrying. If you\n * assign a fallback within catch, it will be reverted after resolved.\n */\n catch?(error: Error): Promise<void> | void;\n}\n\n/**\n * Seal each class's own `render` non-configurable at bootstrap so member\n * classification leaves it unbound - it stays the content-render seam the chain\n * reads (and which adapters like preact detect as the class-component marker).\n */\nComponent.on({\n type(type) {\n const desc = Object.getOwnPropertyDescriptor(type.prototype, 'render');\n\n if (desc && typeof desc.value == 'function')\n Object.defineProperty(type.prototype, 'render', {\n ...desc,\n configurable: false\n });\n }\n});\n\n/**\n * Build (or fetch the cached) composed content render for an instance's class.\n * Walks the prototype chain for content renders strictly below Component - the\n * default render on Component.prototype is excluded and serves as the fallback\n * when a class authors none.\n */\nfunction render(target: Component) {\n const cached = CHAIN.get(target.constructor);\n\n if (cached) return cached;\n\n let render: Function | undefined;\n let proto = target;\n\n while ((proto = Object.getPrototypeOf(proto)) !== Component.prototype) {\n const desc = Object.getOwnPropertyDescriptor(proto, 'render');\n\n if (desc) {\n const layer = unbind(desc.get || desc.value);\n render = render ? compose(layer, render) : layer;\n }\n }\n\n if (!render)\n render = Component.prototype.render;\n\n CHAIN.set(target.constructor, render);\n\n return render;\n}\n\n/** Wrap an outer render so it receives `inner` as a lazy `children` getter. */\nfunction compose(outer: Function, inner: Function): Function {\n return function (this: Component, props?: {}) {\n const self = this;\n return outer.call(self, {\n ...props,\n get children() {\n return inner.call(self, props);\n }\n });\n };\n}\n\nexport { Component };\n"],"mappings":";;;;;AAMA,MAAM,0BAAU,IAAI,SAA4B;;AAGhD,MAAM,wBAAQ,IAAI,SAA6B;AAyD/C,IAAM,YAAN,cAAwB,MAAM;CAoB5B,YAAY,OAAa,GAAG,MAAa;AACvC,MAAI,SAAS,KAAM,SAAQ,EAAE;EAE7B,MAAM,OAAO,EAAE;EACf,MAAM,OAAO,QAAQ,IAAI,MAAM;AAE/B,MAAI,OAAO,SAAS,SAAU,OAAM,MAAM;EAE1C,SAAS,MAAM,OAAW;AACxB,QAAK,MAAM,KAAK,MAAO,MAAK,KAAK;AACjC,UAAO;IAAE,GAAG;IAAM,GAAG;IAAO;;AAG9B,QAAM,OAAO,EAAE,GAAG;GAChB;GACA,KAAK,QAAQ,MAAM,EAAE,aAAa,SAAS;SACrC;AACJ,UAAM,KAAK,KAAK;AAChB,WAAO,eAAe,MAAM,SAAS,EAAE,YAAY,OAAO,CAAC;AAC3D,YAAQ,OAAO,MAAM;;GAExB,CAAC;kBAvBuB,IAAI,KAAK;AAyBlC,MAAI,KAAM,QAAO;AAEjB,UAAQ,IAAI,OAAO,KAAK;AAExB,OAAK,QAAQ;AACb,OAAK,IAAI,eAAe;AACtB,QAAK,IAAI,MAAM,KAAK,MAAM,CAAC;IAC3B;AAEF,SAAO,eAAe,MAAM,UAAU;GACpC,UAAU;GACV,cAAc;GACd,OAAO,OAAO,KAAK;GACpB,CAAC;;;;;;;;;;;;;;;CAgBJ,OAAO,OAA4B;EACjC,MAAM,EAAE,aAAc,SAAS,KAAK;AACpC,SAAO,YAAY;;;;;;;;AAoBvB,UAAU,GAAG,EACX,KAAK,MAAM;CACT,MAAM,OAAO,OAAO,yBAAyB,KAAK,WAAW,SAAS;AAEtE,KAAI,QAAQ,OAAO,KAAK,SAAS,WAC/B,QAAO,eAAe,KAAK,WAAW,UAAU;EAC9C,GAAG;EACH,cAAc;EACf,CAAC;GAEP,CAAC;;;;;;;AAQF,SAAS,OAAO,QAAmB;CACjC,MAAM,SAAS,MAAM,IAAI,OAAO,YAAY;AAE5C,KAAI,OAAQ,QAAO;CAEnB,IAAI;CACJ,IAAI,QAAQ;AAEZ,SAAQ,QAAQ,OAAO,eAAe,MAAM,MAAM,UAAU,WAAW;EACrE,MAAM,OAAO,OAAO,yBAAyB,OAAO,SAAS;AAE7D,MAAI,MAAM;GACR,MAAM,QAAQ,OAAO,KAAK,OAAO,KAAK,MAAM;AAC5C,YAAS,SAAS,QAAQ,OAAO,OAAO,GAAG;;;AAI/C,KAAI,CAAC,OACH,UAAS,UAAU,UAAU;AAE/B,OAAM,IAAI,OAAO,aAAa,OAAO;AAErC,QAAO;;;AAIT,SAAS,QAAQ,OAAiB,OAA2B;AAC3D,QAAO,SAA2B,OAAY;EAC5C,MAAM,OAAO;AACb,SAAO,MAAM,KAAK,MAAM;GACtB,GAAG;GACH,IAAI,WAAW;AACb,WAAO,MAAM,KAAK,MAAM,MAAM;;GAEjC,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { State } from "./state.js";
|
|
2
|
+
|
|
3
|
+
//#region src/context.d.ts
|
|
4
|
+
type Accept<T extends State = State> = T | State.Type<T> | Record<string | number, T | State.Type<T>>;
|
|
5
|
+
type Expect<T extends State = State> = (state: T, downstream: boolean) => (() => void) | void;
|
|
6
|
+
declare namespace Context {
|
|
7
|
+
export { Accept, Expect };
|
|
8
|
+
}
|
|
9
|
+
declare class Context {
|
|
10
|
+
static get root(): Context;
|
|
11
|
+
/** Get the context for a State. Adapters may override to provide framework context. */
|
|
12
|
+
static get(state?: State): Context;
|
|
13
|
+
id: string;
|
|
14
|
+
parent?: Context;
|
|
15
|
+
scope: Set<Context>;
|
|
16
|
+
consume: Map<State.Extends<State>, Set<[Expect<State>, boolean | undefined]> | null>;
|
|
17
|
+
provide: Map<State.Extends<State>, Set<[State, boolean]> | null>;
|
|
18
|
+
protected inputs: Record<string | number, State | State.Extends>;
|
|
19
|
+
private cleanup;
|
|
20
|
+
constructor(arg?: Context | Accept);
|
|
21
|
+
private traverse;
|
|
22
|
+
private register;
|
|
23
|
+
/** Find specified type upstream. Throws if not found. */
|
|
24
|
+
get<T extends State>(Type: State.Extends<T>, required?: true): T;
|
|
25
|
+
/** Find specified type upstream. Returns undefined if not found. */
|
|
26
|
+
get<T extends State>(Type: State.Extends<T>, required?: boolean): T | undefined;
|
|
27
|
+
/** Subscribe to a type becoming available in context. */
|
|
28
|
+
get<T extends State>(Type: State.Extends<T>, callback: Expect<T>, downstream?: boolean): () => void;
|
|
29
|
+
/** Lookup excluding a specific instance (used by State.prototype.get for upstream-of-self semantics). */
|
|
30
|
+
get<T extends State>(Type: State.Extends<T>, arg: boolean | Expect<T> | undefined, downstream: boolean | undefined, skip: State): T | undefined | (() => void);
|
|
31
|
+
/**
|
|
32
|
+
* Register one or more States to this context.
|
|
33
|
+
*
|
|
34
|
+
* Context will add or remove States as needed to keep with provided input.
|
|
35
|
+
*
|
|
36
|
+
* @param inputs State, State class, or map of States / State classes to register.
|
|
37
|
+
* @param forEach Optional callback to run for each State registered.
|
|
38
|
+
*/
|
|
39
|
+
set<T extends State>(inputs: Accept<T>, forEach?: (state: T) => (() => void) | void): this;
|
|
40
|
+
add(I: State, explicit?: boolean): () => void;
|
|
41
|
+
/**
|
|
42
|
+
* Create a child context, optionally registering one or more States to it.
|
|
43
|
+
*
|
|
44
|
+
* @param inputs State, State class, or map of States / State classes to register.
|
|
45
|
+
*/
|
|
46
|
+
push(inputs?: Accept): Context;
|
|
47
|
+
/**
|
|
48
|
+
* Remove all States from this context.
|
|
49
|
+
*
|
|
50
|
+
* Will also run any cleanup callbacks registered when States were added.
|
|
51
|
+
*/
|
|
52
|
+
pop(): void;
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
export { Context };
|
|
56
|
+
//# sourceMappingURL=context.d.ts.map
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { event, listener } from "./observable.js";
|
|
2
|
+
import { State, uid } from "./state.js";
|
|
3
|
+
|
|
4
|
+
//#region src/context.ts
|
|
5
|
+
const LOOKUP = /* @__PURE__ */ new WeakMap();
|
|
6
|
+
let ROOT;
|
|
7
|
+
var Context = class Context {
|
|
8
|
+
static get root() {
|
|
9
|
+
return ROOT ??= new Context();
|
|
10
|
+
}
|
|
11
|
+
/** Get the context for a State. Adapters may override to provide framework context. */
|
|
12
|
+
static get(state) {
|
|
13
|
+
return state && LOOKUP.get(state.is) || Context.root;
|
|
14
|
+
}
|
|
15
|
+
constructor(arg) {
|
|
16
|
+
this.id = uid();
|
|
17
|
+
this.scope = /* @__PURE__ */ new Set();
|
|
18
|
+
this.consume = /* @__PURE__ */ new Map();
|
|
19
|
+
this.provide = /* @__PURE__ */ new Map();
|
|
20
|
+
this.inputs = {};
|
|
21
|
+
this.cleanup = /* @__PURE__ */ new Map();
|
|
22
|
+
if (arg instanceof Context) {
|
|
23
|
+
this.parent = arg;
|
|
24
|
+
arg.scope.add(this);
|
|
25
|
+
} else if (arg) this.set(arg);
|
|
26
|
+
}
|
|
27
|
+
traverse(accept) {
|
|
28
|
+
const queue = [...this.scope];
|
|
29
|
+
for (const ctx of queue) if (accept(ctx) !== false) for (const c of ctx.scope) queue.push(c);
|
|
30
|
+
}
|
|
31
|
+
register(type, value, asConsumer) {
|
|
32
|
+
const mode = asConsumer ? "consume" : "provide";
|
|
33
|
+
let set = this[mode].get(type);
|
|
34
|
+
if (!set) this[mode].set(type, set = /* @__PURE__ */ new Set());
|
|
35
|
+
set.add(value);
|
|
36
|
+
for (let ctx = this.parent; ctx; ctx = ctx.parent) {
|
|
37
|
+
if (ctx[mode].has(type)) break;
|
|
38
|
+
ctx[mode].set(type, null);
|
|
39
|
+
}
|
|
40
|
+
return () => set.delete(value);
|
|
41
|
+
}
|
|
42
|
+
get(Type, arg, downstream, skip) {
|
|
43
|
+
let found;
|
|
44
|
+
let priority = false;
|
|
45
|
+
for (let ctx = this; ctx; ctx = ctx.parent) {
|
|
46
|
+
const entries = ctx.provide.get(Type);
|
|
47
|
+
if (!entries) continue;
|
|
48
|
+
for (const [state, explicit] of entries) {
|
|
49
|
+
if (state === skip || found === state) continue;
|
|
50
|
+
if (!found || explicit && !priority) {
|
|
51
|
+
found = state;
|
|
52
|
+
priority = explicit;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (!priority) {
|
|
56
|
+
found = null;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
if (explicit) throw new Error(`Did find ${Type} in context, but multiple were defined.`);
|
|
60
|
+
}
|
|
61
|
+
if (found !== void 0) break;
|
|
62
|
+
}
|
|
63
|
+
if (typeof arg === "function") {
|
|
64
|
+
if (found && !downstream) arg(found, false);
|
|
65
|
+
if (downstream !== false) this.traverse((ctx) => {
|
|
66
|
+
const has = ctx.provide.get(Type);
|
|
67
|
+
if (has) {
|
|
68
|
+
for (const x of has) if (x[0] !== skip) arg(x[0], true);
|
|
69
|
+
}
|
|
70
|
+
return has !== void 0;
|
|
71
|
+
});
|
|
72
|
+
return this.register(Type, [arg, downstream], true);
|
|
73
|
+
}
|
|
74
|
+
if (found) return found;
|
|
75
|
+
if (found === null) return null;
|
|
76
|
+
if (arg !== false) throw new Error(`Could not find ${Type} in context.`);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Register one or more States to this context.
|
|
80
|
+
*
|
|
81
|
+
* Context will add or remove States as needed to keep with provided input.
|
|
82
|
+
*
|
|
83
|
+
* @param inputs State, State class, or map of States / State classes to register.
|
|
84
|
+
* @param forEach Optional callback to run for each State registered.
|
|
85
|
+
*/
|
|
86
|
+
set(inputs, forEach) {
|
|
87
|
+
const init = /* @__PURE__ */ new Set();
|
|
88
|
+
const { cleanup } = this;
|
|
89
|
+
if (typeof inputs == "function" || inputs instanceof State) inputs = { [0]: inputs };
|
|
90
|
+
for (const K of Object.keys({
|
|
91
|
+
...this.inputs,
|
|
92
|
+
...inputs
|
|
93
|
+
})) {
|
|
94
|
+
const V = inputs[K];
|
|
95
|
+
const E = this.inputs[K];
|
|
96
|
+
if (E === V) continue;
|
|
97
|
+
if (E) {
|
|
98
|
+
cleanup.get(K)?.();
|
|
99
|
+
cleanup.delete(K);
|
|
100
|
+
}
|
|
101
|
+
if (!V) continue;
|
|
102
|
+
if (!(State.is(V) || V instanceof State)) {
|
|
103
|
+
const as = K == "0" || K == String(V) ? V : `${V} (as '${K}')`;
|
|
104
|
+
throw new Error(`Context can only include an instance or class of State but got ${as}.`);
|
|
105
|
+
}
|
|
106
|
+
const state = State.is(V) ? new V() : V.is;
|
|
107
|
+
const remove = this.add(state, true);
|
|
108
|
+
init.add(() => {
|
|
109
|
+
event(state);
|
|
110
|
+
const dispose = forEach && forEach(state);
|
|
111
|
+
cleanup.set(K, () => {
|
|
112
|
+
if (dispose) dispose();
|
|
113
|
+
remove();
|
|
114
|
+
if (state !== V) event(state, null);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
for (const i of init) i();
|
|
119
|
+
this.inputs = inputs;
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
add(I, explicit = false) {
|
|
123
|
+
const { cleanup, provide } = this;
|
|
124
|
+
const root = this === Context.root;
|
|
125
|
+
const TT = [];
|
|
126
|
+
function conflict(T) {
|
|
127
|
+
const entries = provide.get(T);
|
|
128
|
+
if (entries) {
|
|
129
|
+
for (const entry of entries) if (!entry[1] && entry[0] !== I) return entries.delete(entry);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (let T = I.constructor; T !== State; T = Object.getPrototypeOf(T)) if (explicit || !root || !conflict(T)) TT.push(T);
|
|
133
|
+
const expects = /* @__PURE__ */ new Map();
|
|
134
|
+
const onDone = /* @__PURE__ */ new Set();
|
|
135
|
+
for (const T of TT) onDone.add(this.register(T, [I, explicit]));
|
|
136
|
+
function queue(ctx, downstream) {
|
|
137
|
+
let found = false;
|
|
138
|
+
for (const T of TT) {
|
|
139
|
+
const list = ctx.consume.get(T);
|
|
140
|
+
if (list !== void 0) found = true;
|
|
141
|
+
if (list) {
|
|
142
|
+
for (const [cb, filter] of list) if (filter === downstream || filter == null) expects.set(cb, () => {
|
|
143
|
+
const r = cb(I, downstream);
|
|
144
|
+
if (r) onDone.add(r);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return found;
|
|
149
|
+
}
|
|
150
|
+
for (let ctx = this; ctx; ctx = ctx.parent) queue(ctx, true);
|
|
151
|
+
this.traverse((ctx) => queue(ctx, false));
|
|
152
|
+
if (!LOOKUP.has(I)) LOOKUP.set(I, this);
|
|
153
|
+
listener(I, () => {
|
|
154
|
+
expects.forEach((f) => f());
|
|
155
|
+
expects.clear();
|
|
156
|
+
return null;
|
|
157
|
+
});
|
|
158
|
+
function flush() {
|
|
159
|
+
onDone.forEach((r) => r());
|
|
160
|
+
onDone.clear();
|
|
161
|
+
}
|
|
162
|
+
function remove() {
|
|
163
|
+
cleanup.delete(remove);
|
|
164
|
+
flush();
|
|
165
|
+
}
|
|
166
|
+
cleanup.set(remove, remove);
|
|
167
|
+
return remove;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Create a child context, optionally registering one or more States to it.
|
|
171
|
+
*
|
|
172
|
+
* @param inputs State, State class, or map of States / State classes to register.
|
|
173
|
+
*/
|
|
174
|
+
push(inputs) {
|
|
175
|
+
const next = new Context(this);
|
|
176
|
+
if (inputs) next.set(inputs);
|
|
177
|
+
return next;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Remove all States from this context.
|
|
181
|
+
*
|
|
182
|
+
* Will also run any cleanup callbacks registered when States were added.
|
|
183
|
+
*/
|
|
184
|
+
pop() {
|
|
185
|
+
this.inputs = {};
|
|
186
|
+
this.scope.forEach((x) => x.pop());
|
|
187
|
+
this.scope.clear();
|
|
188
|
+
this.cleanup.forEach((cb) => cb());
|
|
189
|
+
this.cleanup.clear();
|
|
190
|
+
this.consume.clear();
|
|
191
|
+
this.provide.clear();
|
|
192
|
+
if (this.parent) this.parent.scope.delete(this);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
Object.defineProperty(Context.prototype, "toString", { value() {
|
|
196
|
+
return `Context-${this.id}`;
|
|
197
|
+
} });
|
|
198
|
+
|
|
199
|
+
//#endregion
|
|
200
|
+
export { Context };
|
|
201
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","names":[],"sources":["../src/context.ts"],"sourcesContent":["import { listener } from \"./observable\";\nimport { event, State, uid } from \"./state\";\n\nconst LOOKUP = new WeakMap<State, Context>();\nlet ROOT: Context;\n\ntype Accept<T extends State = State> =\n | T\n | State.Type<T>\n | Record<string | number, T | State.Type<T>>;\n\ntype Expect<T extends State = State> = (\n state: T,\n downstream: boolean,\n) => (() => void) | void;\n\ndeclare namespace Context {\n export { Accept, Expect };\n}\n\nclass Context {\n static get root(): Context {\n return ROOT ??= new Context();\n }\n\n /** Get the context for a State. Adapters may override to provide framework context. */\n static get(state?: State): Context {\n return state && LOOKUP.get(state.is) || Context.root;\n }\n\n public id = uid();\n public parent?: Context;\n public scope = new Set<Context>();\n public consume = new Map<\n State.Extends,\n Set<[Expect, boolean | undefined]> | null\n >();\n public provide = new Map<State.Extends, Set<[State, boolean]> | null>();\n\n protected inputs: Record<string | number, State | State.Extends> = {};\n\n private cleanup = new Map<string | number | Function, () => void>();\n\n constructor(arg?: Context | Accept) {\n if (arg instanceof Context) {\n this.parent = arg;\n arg.scope.add(this);\n } else if (arg) {\n this.set(arg);\n }\n }\n\n private traverse(accept: (ctx: Context) => boolean | void) {\n const queue = [...this.scope];\n for (const ctx of queue)\n if (accept(ctx) !== false) for (const c of ctx.scope) queue.push(c);\n }\n\n private register<T extends State>(\n type: State.Extends<T>,\n value: [Expect<T>, boolean | undefined],\n asConsumer: true,\n ): () => void;\n\n private register<T extends State>(\n type: State.Extends<T>,\n value: [T, boolean],\n asConsumer?: false,\n ): () => void;\n\n private register(type: State.Extends, value: any, asConsumer?: boolean) {\n const mode = asConsumer ? \"consume\" : \"provide\";\n let set = this[mode].get(type) as Set<any>;\n if (!set) this[mode].set(type, (set = new Set()));\n set.add(value);\n\n for (let ctx = this.parent; ctx; ctx = ctx.parent) {\n if (ctx[mode].has(type)) break;\n ctx[mode].set(type, null);\n }\n\n return () => set.delete(value);\n }\n\n /** Find specified type upstream. Throws if not found. */\n public get<T extends State>(Type: State.Extends<T>, required?: true): T;\n\n /** Find specified type upstream. Returns undefined if not found. */\n public get<T extends State>(\n Type: State.Extends<T>,\n required?: boolean,\n ): T | undefined;\n\n /** Subscribe to a type becoming available in context. */\n public get<T extends State>(\n Type: State.Extends<T>,\n callback: Expect<T>,\n downstream?: boolean,\n ): () => void;\n\n /** Lookup excluding a specific instance (used by State.prototype.get for upstream-of-self semantics). */\n public get<T extends State>(\n Type: State.Extends<T>,\n arg: boolean | Expect<T> | undefined,\n downstream: boolean | undefined,\n skip: State,\n ): T | undefined | (() => void);\n\n public get<T extends State>(\n Type: State.Extends<T>,\n arg?: boolean | Expect<T>,\n downstream?: boolean,\n skip?: State,\n ) {\n let found: T | null | undefined;\n let priority = false;\n\n for (let ctx: Context | undefined = this; ctx; ctx = ctx.parent) {\n const entries = ctx.provide.get(Type);\n if (!entries) continue;\n\n for (const [state, explicit] of entries) {\n if (state === skip || found === state) continue;\n if (!found || (explicit && !priority)) {\n found = state as T;\n priority = explicit;\n continue;\n }\n if (!priority) {\n found = null;\n break;\n }\n if (explicit)\n throw new Error(\n `Did find ${Type} in context, but multiple were defined.`,\n );\n }\n if (found !== undefined) break;\n }\n\n if (typeof arg === \"function\") {\n if (found && !downstream) arg(found, false);\n\n if (downstream !== false) {\n this.traverse((ctx) => {\n const has = ctx.provide.get(Type);\n if (has) for (const x of has) if (x[0] !== skip) arg(x[0] as T, true);\n return has !== undefined;\n });\n }\n\n return this.register(Type, [arg as Expect, downstream], true);\n }\n\n if (found) return found;\n if (found === null) return null;\n if (arg !== false) throw new Error(`Could not find ${Type} in context.`);\n }\n\n /**\n * Register one or more States to this context.\n *\n * Context will add or remove States as needed to keep with provided input.\n *\n * @param inputs State, State class, or map of States / State classes to register.\n * @param forEach Optional callback to run for each State registered.\n */\n public set<T extends State>(\n inputs: Accept<T>,\n forEach?: (state: T) => (() => void) | void,\n ) {\n const init = new Set<() => void>();\n const { cleanup } = this;\n\n if (typeof inputs == \"function\" || inputs instanceof State)\n inputs = { [0]: inputs };\n\n for (const K of Object.keys({ ...this.inputs, ...inputs })) {\n const V = inputs[K];\n const E = this.inputs[K];\n\n if (E === V) continue;\n\n if (E) {\n cleanup.get(K)?.();\n cleanup.delete(K);\n }\n\n if (!V) continue;\n\n if (!(State.is(V) || V instanceof State)) {\n const as = K == \"0\" || K == String(V) ? V : `${V} (as '${K}')`;\n throw new Error(\n `Context can only include an instance or class of State but got ${as}.`,\n );\n }\n\n const state = State.is(V) ? new (V as State.Type)() : V.is;\n const remove = this.add(state, true);\n\n init.add(() => {\n event(state);\n const dispose = forEach && forEach(state as T);\n cleanup.set(K, () => {\n if (dispose) dispose();\n remove();\n if (state !== V) event(state, null);\n });\n });\n }\n\n for (const i of init) i();\n\n this.inputs = inputs;\n\n return this;\n }\n\n add(I: State, explicit = false) {\n const { cleanup, provide } = this;\n const root = this === Context.root;\n const TT: State.Extends[] = [];\n\n function conflict(T: State.Extends) {\n const entries = provide.get(T);\n if (entries)\n for (const entry of entries)\n if (!entry[1] && entry[0] !== I)\n return entries.delete(entry);\n }\n\n for (\n let T = I.constructor as State.Extends;\n T !== State;\n T = Object.getPrototypeOf(T)\n )\n if (explicit || !root || !conflict(T)) TT.push(T);\n\n const expects = new Map<Expect, () => void>();\n const onDone = new Set<() => void>();\n\n for (const T of TT) onDone.add(this.register(T, [I, explicit]));\n\n function queue(ctx: Context, downstream: boolean) {\n let found = false;\n for (const T of TT) {\n const list = ctx.consume.get(T);\n if (list !== undefined) found = true;\n if (list)\n for (const [cb, filter] of list)\n if (filter === downstream || filter == null)\n expects.set(cb, () => {\n const r = cb(I, downstream);\n if (r) onDone.add(r);\n });\n }\n return found;\n }\n\n for (let ctx: Context | undefined = this; ctx; ctx = ctx.parent) queue(ctx, true);\n this.traverse((ctx) => queue(ctx, false));\n\n if (!LOOKUP.has(I)) LOOKUP.set(I, this);\n\n listener(I, () => {\n expects.forEach((f) => f());\n expects.clear();\n return null;\n });\n\n function flush() {\n onDone.forEach((r) => r());\n onDone.clear();\n }\n\n function remove() {\n cleanup.delete(remove);\n flush();\n }\n\n cleanup.set(remove, remove);\n\n return remove;\n }\n\n /**\n * Create a child context, optionally registering one or more States to it.\n *\n * @param inputs State, State class, or map of States / State classes to register.\n */\n public push(inputs?: Accept) {\n const next = new Context(this);\n if (inputs) next.set(inputs);\n return next;\n }\n\n /**\n * Remove all States from this context.\n *\n * Will also run any cleanup callbacks registered when States were added.\n */\n public pop() {\n this.inputs = {};\n this.scope.forEach((x) => x.pop());\n this.scope.clear();\n this.cleanup.forEach((cb) => cb());\n this.cleanup.clear();\n this.consume.clear();\n this.provide.clear();\n if (this.parent) {\n this.parent.scope.delete(this);\n }\n }\n}\n\nObject.defineProperty(Context.prototype, \"toString\", {\n value() {\n return `Context-${this.id}`;\n },\n});\n\nexport { Context };\n"],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { State } from "../state.js";
|
|
2
|
+
|
|
3
|
+
//#region src/field/def.d.ts
|
|
4
|
+
declare namespace def {
|
|
5
|
+
/**
|
|
6
|
+
* Property initializer, will run upon instance creation.
|
|
7
|
+
* Optional returned callback will run when once upon first access.
|
|
8
|
+
*/
|
|
9
|
+
type Factory<T = any, M extends State = any> = (this: M, key: Extract<State.Field<M>, string>, thisArg: M, state: State.Values<M>) => def.Config<T> | (() => void) | void;
|
|
10
|
+
interface Config<T = any> extends State.Apply<T> {
|
|
11
|
+
destroy?: () => void;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
declare function def<T>(arg1: def.Factory<T>): T extends void ? unknown : T;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { def };
|
|
17
|
+
//# sourceMappingURL=def.d.ts.map
|