@player-ui/player 0.0.1-next.1
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/dist/index.cjs.js +1394 -0
- package/dist/index.d.ts +434 -0
- package/dist/index.esm.js +1326 -0
- package/package.json +26 -0
- package/src/data.ts +247 -0
- package/src/index.ts +18 -0
- package/src/player.ts +497 -0
- package/src/plugins/flow-exp-plugin.ts +65 -0
- package/src/types.ts +114 -0
- package/src/utils/desc.d.ts +2 -0
- package/src/validation/binding-tracker.ts +239 -0
- package/src/validation/controller.ts +661 -0
- package/src/validation/index.ts +2 -0
- package/src/view/asset-transform.ts +147 -0
- package/src/view/controller.ts +148 -0
- package/src/view/index.ts +4 -0
- package/src/view/store.ts +94 -0
- package/src/view/types.ts +31 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { Node } from '@player-ui/view';
|
|
2
|
+
import { NodeType } from '@player-ui/view';
|
|
3
|
+
import { LocalStateStore } from './store';
|
|
4
|
+
import type { TransformRegistry } from './types';
|
|
5
|
+
import type { ViewController } from './controller';
|
|
6
|
+
import { PlayerPlugin } from '..';
|
|
7
|
+
|
|
8
|
+
/** Traverse up the nodes until the target is found */
|
|
9
|
+
function findUp(node: Node.Node, target: Node.Node): boolean {
|
|
10
|
+
if (node === target) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (node.parent) {
|
|
15
|
+
return findUp(node.parent, target);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A plugin to register custom transforms on certain asset types
|
|
23
|
+
* This allows users to embed stateful data into transforms.
|
|
24
|
+
*/
|
|
25
|
+
export class AssetTransformCorePlugin {
|
|
26
|
+
public readonly stateStore: Map<Node.Node, LocalStateStore>;
|
|
27
|
+
private readonly registry: TransformRegistry;
|
|
28
|
+
private beforeResolveSymbol: symbol;
|
|
29
|
+
private resolveSymbol: symbol;
|
|
30
|
+
private beforeResolveCountSymbol: symbol;
|
|
31
|
+
private resolveCountSymbol: symbol;
|
|
32
|
+
|
|
33
|
+
constructor(registry: TransformRegistry) {
|
|
34
|
+
this.registry = registry;
|
|
35
|
+
this.stateStore = new Map();
|
|
36
|
+
this.beforeResolveSymbol = Symbol('before resolve');
|
|
37
|
+
this.resolveSymbol = Symbol('resolve');
|
|
38
|
+
this.beforeResolveCountSymbol = Symbol('before resolve count');
|
|
39
|
+
this.resolveCountSymbol = Symbol('resolve count');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
apply(viewController: ViewController) {
|
|
43
|
+
viewController.hooks.view.tap('asset-transform', (view) => {
|
|
44
|
+
// Clear out everything when we create a new view
|
|
45
|
+
this.stateStore.clear();
|
|
46
|
+
|
|
47
|
+
view.hooks.resolver.tap('asset-transform', (resolver) => {
|
|
48
|
+
let lastUpdatedNode: Node.Node | undefined;
|
|
49
|
+
|
|
50
|
+
/** A function to update the state and trigger a view re-compute */
|
|
51
|
+
const updateState = (node: Node.Node) => {
|
|
52
|
+
lastUpdatedNode = node;
|
|
53
|
+
view.update(new Set());
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** Given a node and a transform step, fetch a local store */
|
|
57
|
+
const getStore = (node: Node.Node, stepKey: symbol) => {
|
|
58
|
+
let store: LocalStateStore;
|
|
59
|
+
const countKey =
|
|
60
|
+
stepKey === this.resolveSymbol
|
|
61
|
+
? this.resolveCountSymbol
|
|
62
|
+
: this.beforeResolveCountSymbol;
|
|
63
|
+
|
|
64
|
+
const storedState = this.stateStore.get(node);
|
|
65
|
+
|
|
66
|
+
if (storedState) {
|
|
67
|
+
store = storedState;
|
|
68
|
+
store.removeKey(countKey);
|
|
69
|
+
} else {
|
|
70
|
+
store = new LocalStateStore(() => {
|
|
71
|
+
updateState(node);
|
|
72
|
+
});
|
|
73
|
+
this.stateStore.set(node, store);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
useSharedState: (
|
|
78
|
+
key: string | symbol
|
|
79
|
+
): (<T>(initialState: T) => readonly [T, (value: T) => void]) => {
|
|
80
|
+
return store.useSharedState(key);
|
|
81
|
+
},
|
|
82
|
+
useLocalState: <T>(initialState: T) => {
|
|
83
|
+
return store.getLocalStateFunction<T>(
|
|
84
|
+
stepKey,
|
|
85
|
+
countKey
|
|
86
|
+
)(initialState);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
resolver.hooks.beforeResolve.tap('asset-transform', (node, options) => {
|
|
92
|
+
if (node && (node.type === 'asset' || node.type === 'view')) {
|
|
93
|
+
const transform = this.registry.get(node.value);
|
|
94
|
+
|
|
95
|
+
if (transform?.beforeResolve) {
|
|
96
|
+
const store = getStore(node, this.beforeResolveSymbol);
|
|
97
|
+
|
|
98
|
+
return transform.beforeResolve(node, options, store);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return node;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
resolver.hooks.afterUpdate.tap('asset-transform', () => {
|
|
106
|
+
lastUpdatedNode = undefined;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
resolver.hooks.skipResolve.tap('asset-transform', (skip, node) => {
|
|
110
|
+
if (!skip || !lastUpdatedNode) {
|
|
111
|
+
return skip;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const isParentOfUpdated = findUp(lastUpdatedNode, node);
|
|
115
|
+
const isChildOfUpdated = findUp(node, lastUpdatedNode);
|
|
116
|
+
|
|
117
|
+
return !isParentOfUpdated && !isChildOfUpdated;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
resolver.hooks.afterResolve.tap(
|
|
121
|
+
'asset-transform',
|
|
122
|
+
(value, node, options) => {
|
|
123
|
+
if (node.type !== NodeType.Asset && node.type !== NodeType.View) {
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const originalNode = resolver.getSourceNode(node);
|
|
128
|
+
|
|
129
|
+
if (!originalNode) {
|
|
130
|
+
return value;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const transform = this.registry.get(value);
|
|
134
|
+
|
|
135
|
+
if (transform?.resolve) {
|
|
136
|
+
const store = getStore(originalNode, this.resolveSymbol);
|
|
137
|
+
|
|
138
|
+
return transform?.resolve(value, options, store);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { SyncHook, SyncWaterfallHook } from 'tapable';
|
|
2
|
+
import type { Resolve } from '@player-ui/view';
|
|
3
|
+
import { ViewInstance } from '@player-ui/view';
|
|
4
|
+
import type { Logger } from '@player-ui/logger';
|
|
5
|
+
import type { FlowInstance, FlowController } from '@player-ui/flow';
|
|
6
|
+
import type { View, NavigationFlowViewState } from '@player-ui/types';
|
|
7
|
+
import { resolveDataRefsInString } from '@player-ui/string-resolver';
|
|
8
|
+
import { Registry } from '@player-ui/partial-match-registry';
|
|
9
|
+
import queueMicrotask from 'queue-microtask';
|
|
10
|
+
import type { DataController } from '../data';
|
|
11
|
+
import { AssetTransformCorePlugin } from './asset-transform';
|
|
12
|
+
import type { TransformRegistry } from './types';
|
|
13
|
+
import type { BindingInstance } from '..';
|
|
14
|
+
|
|
15
|
+
export interface ViewControllerOptions {
|
|
16
|
+
/** Where to get data from */
|
|
17
|
+
model: DataController;
|
|
18
|
+
|
|
19
|
+
/** Where to log data */
|
|
20
|
+
logger?: Logger;
|
|
21
|
+
|
|
22
|
+
/** A flow-controller instance to listen for view changes */
|
|
23
|
+
flowController: FlowController;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A controller to manage updating/switching views */
|
|
27
|
+
export class ViewController {
|
|
28
|
+
public readonly hooks = {
|
|
29
|
+
/** Do any processing before the `View` instance is created */
|
|
30
|
+
resolveView: new SyncWaterfallHook<View, string, NavigationFlowViewState>([
|
|
31
|
+
'view',
|
|
32
|
+
'viewRef',
|
|
33
|
+
'viewState',
|
|
34
|
+
]),
|
|
35
|
+
|
|
36
|
+
// The hook right before the View starts resolving. Attach anything custom here
|
|
37
|
+
view: new SyncHook<ViewInstance>(['view']),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
private readonly viewMap: Record<string, View>;
|
|
41
|
+
private readonly viewOptions: Resolve.ResolverOptions & ViewControllerOptions;
|
|
42
|
+
private pendingUpdate?: {
|
|
43
|
+
/** pending data binding changes */
|
|
44
|
+
changedBindings?: Set<BindingInstance>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
public currentView?: ViewInstance;
|
|
48
|
+
public transformRegistry: TransformRegistry = new Registry();
|
|
49
|
+
public optimizeUpdates = true;
|
|
50
|
+
|
|
51
|
+
constructor(
|
|
52
|
+
initialViews: View[],
|
|
53
|
+
options: Resolve.ResolverOptions & ViewControllerOptions
|
|
54
|
+
) {
|
|
55
|
+
this.viewOptions = options;
|
|
56
|
+
this.viewMap = initialViews.reduce<Record<string, View>>(
|
|
57
|
+
(viewMap, view) => ({
|
|
58
|
+
...viewMap,
|
|
59
|
+
[view.id]: view,
|
|
60
|
+
}),
|
|
61
|
+
{}
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
new AssetTransformCorePlugin(this.transformRegistry).apply(this);
|
|
65
|
+
|
|
66
|
+
options.flowController.hooks.flow.tap(
|
|
67
|
+
'viewController',
|
|
68
|
+
(flow: FlowInstance) => {
|
|
69
|
+
flow.hooks.transition.tap('viewController', (_oldState, newState) => {
|
|
70
|
+
if (newState.value.state_type === 'VIEW') {
|
|
71
|
+
this.onView(newState.value);
|
|
72
|
+
} else {
|
|
73
|
+
this.currentView = undefined;
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
options.model.hooks.onUpdate.tap('viewController', (updates) => {
|
|
80
|
+
if (this.currentView) {
|
|
81
|
+
if (this.optimizeUpdates) {
|
|
82
|
+
this.queueUpdate(new Set(updates.map((t) => t.binding)));
|
|
83
|
+
} else {
|
|
84
|
+
this.currentView.update();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private queueUpdate(bindings: Set<BindingInstance>) {
|
|
91
|
+
if (this.pendingUpdate?.changedBindings) {
|
|
92
|
+
this.pendingUpdate.changedBindings = new Set([
|
|
93
|
+
...this.pendingUpdate.changedBindings,
|
|
94
|
+
...bindings,
|
|
95
|
+
]);
|
|
96
|
+
} else {
|
|
97
|
+
this.pendingUpdate = { changedBindings: bindings };
|
|
98
|
+
queueMicrotask(() => {
|
|
99
|
+
const updates = this.pendingUpdate?.changedBindings;
|
|
100
|
+
this.pendingUpdate = undefined;
|
|
101
|
+
this.currentView?.update(updates);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private getViewForRef(viewRef: string): View | undefined {
|
|
107
|
+
// First look for a 1:1 viewRef -> id mapping (this is most common)
|
|
108
|
+
if (this.viewMap[viewRef]) {
|
|
109
|
+
return this.viewMap[viewRef];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// The view ids saved may also contain model refs, resolve those and try again
|
|
113
|
+
const matchingViewId = Object.keys(this.viewMap).find(
|
|
114
|
+
(possibleViewIdMatch) =>
|
|
115
|
+
viewRef ===
|
|
116
|
+
resolveDataRefsInString(possibleViewIdMatch, {
|
|
117
|
+
model: this.viewOptions.model,
|
|
118
|
+
evaluate: this.viewOptions.evaluator.evaluate,
|
|
119
|
+
})
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
if (matchingViewId && this.viewMap[matchingViewId]) {
|
|
123
|
+
return this.viewMap[matchingViewId];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public onView(state: NavigationFlowViewState) {
|
|
128
|
+
const viewId = state.ref;
|
|
129
|
+
|
|
130
|
+
const source = this.hooks.resolveView.call(
|
|
131
|
+
this.getViewForRef(viewId),
|
|
132
|
+
viewId,
|
|
133
|
+
state
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (!source) {
|
|
137
|
+
throw new Error(`No view with id ${viewId}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const view = new ViewInstance(source, this.viewOptions);
|
|
141
|
+
this.currentView = view;
|
|
142
|
+
|
|
143
|
+
// Give people a chance to attach their
|
|
144
|
+
// own listeners to the view before we resolve it
|
|
145
|
+
this.hooks.view.call(view);
|
|
146
|
+
view.update();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export interface Store {
|
|
2
|
+
useLocalState<T>(initialState: T): readonly [T, (value: T) => void];
|
|
3
|
+
useSharedState<T>(
|
|
4
|
+
key: string | symbol
|
|
5
|
+
): (initialState: T) => readonly [T, (value: T) => void];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface SharedStore {
|
|
9
|
+
getLocalStateFunction<T>(
|
|
10
|
+
key: string | symbol,
|
|
11
|
+
countKey: symbol
|
|
12
|
+
): (initialState: T) => readonly [T, (value: T) => void];
|
|
13
|
+
useSharedState<T>(
|
|
14
|
+
key: string | symbol
|
|
15
|
+
): (initialState: T) => readonly [T, (value: T) => void];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A store that holds on to state for a transform */
|
|
19
|
+
export class LocalStateStore implements SharedStore {
|
|
20
|
+
private state: Map<string | symbol, any>;
|
|
21
|
+
|
|
22
|
+
private updateCallback?: () => void;
|
|
23
|
+
|
|
24
|
+
constructor(onUpdate?: () => void) {
|
|
25
|
+
this.updateCallback = onUpdate;
|
|
26
|
+
|
|
27
|
+
this.state = new Map();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public removeKey(key: symbol | string) {
|
|
31
|
+
this.state.delete(key);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public reset() {
|
|
35
|
+
this.state.clear();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
useSharedState<T>(key: string | symbol) {
|
|
39
|
+
return (initialState: T) => {
|
|
40
|
+
if (!this.state.has(key)) {
|
|
41
|
+
this.state.set(key, initialState);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return [
|
|
45
|
+
this.state.get(key) as T,
|
|
46
|
+
(newState: T) => {
|
|
47
|
+
const current = this.state.get(key) as T;
|
|
48
|
+
|
|
49
|
+
this.state.set(key, newState);
|
|
50
|
+
|
|
51
|
+
if (current !== newState) {
|
|
52
|
+
this.updateCallback?.();
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
] as const;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getLocalStateFunction<T>(key: symbol, countKey: symbol) {
|
|
60
|
+
return (initialState: T) => {
|
|
61
|
+
// initialize if not already created
|
|
62
|
+
if (!this.state.has(key)) {
|
|
63
|
+
this.state.set(key, []);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!this.state.has(countKey)) {
|
|
67
|
+
this.state.set(countKey, 0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const localState = this.state.get(key);
|
|
71
|
+
const oldCount = this.state.get(countKey);
|
|
72
|
+
|
|
73
|
+
this.state.set(countKey, oldCount + 1);
|
|
74
|
+
|
|
75
|
+
if (localState.length <= oldCount) {
|
|
76
|
+
localState.push(initialState);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const value = localState[oldCount] as T;
|
|
80
|
+
|
|
81
|
+
return [
|
|
82
|
+
value,
|
|
83
|
+
(newState: T) => {
|
|
84
|
+
const oldValue = localState[oldCount] as T;
|
|
85
|
+
localState[oldCount] = newState;
|
|
86
|
+
|
|
87
|
+
if (oldValue !== newState) {
|
|
88
|
+
this.updateCallback?.();
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
] as const;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Asset } from '@player-ui/types';
|
|
2
|
+
import type { Resolve, Node } from '@player-ui/view';
|
|
3
|
+
import type { Registry } from '@player-ui/partial-match-registry';
|
|
4
|
+
import type { Store } from './store';
|
|
5
|
+
|
|
6
|
+
/** Transform function that is ran on the Asset before it's resolved */
|
|
7
|
+
export type BeforeTransformFunction<AuthoredAsset extends Asset = Asset> = (
|
|
8
|
+
asset: Node.Asset<AuthoredAsset> | Node.View<AuthoredAsset>,
|
|
9
|
+
options: Resolve.NodeResolveOptions,
|
|
10
|
+
store: Store
|
|
11
|
+
) => Node.Node;
|
|
12
|
+
|
|
13
|
+
/** Transform function that is ran on the Asset after it's resolved */
|
|
14
|
+
export type TransformFunction<
|
|
15
|
+
AuthoredAsset extends Asset = Asset,
|
|
16
|
+
TransformedAsset extends Asset = AuthoredAsset
|
|
17
|
+
> = (
|
|
18
|
+
asset: AuthoredAsset,
|
|
19
|
+
options: Resolve.NodeResolveOptions,
|
|
20
|
+
store: Store
|
|
21
|
+
) => TransformedAsset;
|
|
22
|
+
|
|
23
|
+
export interface TransformFunctions {
|
|
24
|
+
/** A function that is executed as an AST -> AST transform before resolving the node to a value */
|
|
25
|
+
beforeResolve?: BeforeTransformFunction<any>;
|
|
26
|
+
|
|
27
|
+
/** A function to resolve an AST to a value */
|
|
28
|
+
resolve?: TransformFunction<any>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type TransformRegistry = Registry<TransformFunctions>;
|