@player-ui/context-plugin 0.16.0--canary.891.38194

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/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "sideEffects": false,
3
+ "files": [
4
+ "dist",
5
+ "src",
6
+ "types"
7
+ ],
8
+ "name": "@player-ui/context-plugin",
9
+ "version": "0.16.0--canary.891.38194",
10
+ "main": "dist/cjs/index.cjs",
11
+ "peerDependencies": {
12
+ "@player-ui/player": "0.16.0--canary.891.38194"
13
+ },
14
+ "devDependencies": {
15
+ "@player-ui/common-types-plugin": "workspace:^",
16
+ "@player-ui/reference-assets-plugin": "workspace:^"
17
+ },
18
+ "module": "dist/index.legacy-esm.js",
19
+ "types": "types/index.d.ts",
20
+ "bundle": "dist/ContextPlugin.native.js",
21
+ "exports": {
22
+ "./package.json": "./package.json",
23
+ "./dist/index.css": "./dist/index.css",
24
+ ".": {
25
+ "types": "./types/index.d.ts",
26
+ "import": "./dist/index.mjs",
27
+ "default": "./dist/cjs/index.cjs"
28
+ }
29
+ },
30
+ "dependencies": {
31
+ "tapable-ts": "^0.2.3",
32
+ "tslib": "^2.6.2"
33
+ }
34
+ }
@@ -0,0 +1,27 @@
1
+ import { test, expect } from "vitest";
2
+ import { ContextHistory } from "../history";
3
+ import type { FrozenContextSnapshot } from "../types";
4
+
5
+ const snap = (endedAt: number): FrozenContextSnapshot =>
6
+ Object.freeze({
7
+ endedAt,
8
+ entries: Object.freeze([]),
9
+ });
10
+
11
+ test("push appends in order", () => {
12
+ const h = new ContextHistory();
13
+ h.push(snap(1));
14
+ h.push(snap(2));
15
+ h.push(snap(3));
16
+
17
+ expect(h.size()).toBe(3);
18
+ expect(h.entries().map((s) => s.endedAt)).toEqual([1, 2, 3]);
19
+ });
20
+
21
+ test("clear empties the stack", () => {
22
+ const h = new ContextHistory();
23
+ h.push(snap(1));
24
+ h.clear();
25
+ expect(h.size()).toBe(0);
26
+ expect(h.entries()).toEqual([]);
27
+ });
@@ -0,0 +1,22 @@
1
+ import { test, expect } from "vitest";
2
+ import { defineContextKey, resolveContextKeySymbol } from "../key";
3
+
4
+ test("two keys with the same name share Symbol.for identity", () => {
5
+ const a = defineContextKey<string>("form-state", "Current form state");
6
+ const b = defineContextKey<string>("form-state", "Different description");
7
+
8
+ expect(a.symbol).toBe(b.symbol);
9
+ expect(a.description).toBe("Current form state");
10
+ expect(b.description).toBe("Different description");
11
+ });
12
+
13
+ test("resolveContextKeySymbol matches defineContextKey identity", () => {
14
+ const key = defineContextKey("simulation", "Simulator context");
15
+ expect(resolveContextKeySymbol("simulation")).toBe(key.symbol);
16
+ });
17
+
18
+ test("distinct names produce distinct symbols", () => {
19
+ const a = defineContextKey("alpha", "A");
20
+ const b = defineContextKey("beta", "B");
21
+ expect(a.symbol).not.toBe(b.symbol);
22
+ });
@@ -0,0 +1,376 @@
1
+ import { test, expect, vitest } from "vitest";
2
+ import { Player } from "@player-ui/player";
3
+ import { ContextPlugin } from "../plugin";
4
+ import { ContextPluginSymbol } from "../symbols";
5
+ import { defineContextKey } from "../key";
6
+ import { getContextPlugin } from "../utils";
7
+
8
+ const minimalFlow = {
9
+ id: "flow-alpha",
10
+ views: [{ id: "view-1", type: "info" }],
11
+ navigation: {
12
+ BEGIN: "FLOW_1",
13
+ FLOW_1: {
14
+ startState: "VIEW_1",
15
+ VIEW_1: {
16
+ ref: "view-1",
17
+ state_type: "VIEW",
18
+ transitions: { "*": "END_Done" },
19
+ },
20
+ END_Done: { state_type: "END", outcome: "done" },
21
+ },
22
+ },
23
+ };
24
+
25
+ test("findPlugin returns the registered ContextPlugin via its symbol", () => {
26
+ const plugin = new ContextPlugin();
27
+ const player = new Player({ plugins: [plugin] });
28
+ expect(player.findPlugin<ContextPlugin>(ContextPluginSymbol)).toBe(plugin);
29
+ });
30
+
31
+ test("set / get / has round-trip", () => {
32
+ const plugin = new ContextPlugin();
33
+ new Player({ plugins: [plugin] });
34
+ const key = defineContextKey<string>("greeting", "Greeting string");
35
+
36
+ expect(plugin.has(key)).toBe(false);
37
+ plugin.set(key, "hello");
38
+ expect(plugin.get(key)).toBe("hello");
39
+ expect(plugin.has(key)).toBe(true);
40
+ });
41
+
42
+ test("transform aggregates from other context entries on get", () => {
43
+ const plugin = new ContextPlugin();
44
+ new Player({ plugins: [plugin] });
45
+
46
+ const first = defineContextKey<string>("first", "First name");
47
+ const last = defineContextKey<string>("last", "Last name");
48
+ const full = defineContextKey<string>("full", "Full name");
49
+
50
+ plugin.set(first, "Ada");
51
+ plugin.set(last, "Lovelace");
52
+ plugin.registerTransform(full, {
53
+ sources: [first, last],
54
+ compute: (read) => `${read(first) ?? ""} ${read(last) ?? ""}`.trim(),
55
+ });
56
+
57
+ expect(plugin.get(full)).toBe("Ada Lovelace");
58
+ });
59
+
60
+ test("literal value takes precedence over registered transform", () => {
61
+ const plugin = new ContextPlugin();
62
+ new Player({ plugins: [plugin] });
63
+ const src = defineContextKey<number>("src", "Source");
64
+ const target = defineContextKey<number>("target", "Target");
65
+
66
+ plugin.set(src, 10);
67
+ const compute = vitest.fn().mockReturnValue(999);
68
+ plugin.registerTransform(target, { sources: [src], compute });
69
+ expect(plugin.get(target)).toBe(999);
70
+
71
+ plugin.set(target, 5);
72
+ compute.mockClear();
73
+ expect(plugin.get(target)).toBe(5);
74
+ expect(compute).not.toHaveBeenCalled();
75
+ });
76
+
77
+ test("per-key subscriber fires on direct set", () => {
78
+ const plugin = new ContextPlugin();
79
+ new Player({ plugins: [plugin] });
80
+ const key = defineContextKey<number>("c", "Counter");
81
+ const handler = vitest.fn();
82
+
83
+ plugin.subscribe(key, handler);
84
+ plugin.set(key, 1);
85
+ plugin.set(key, 2);
86
+
87
+ expect(handler).toHaveBeenCalledTimes(2);
88
+ expect(handler).toHaveBeenNthCalledWith(1, 1, key);
89
+ expect(handler).toHaveBeenNthCalledWith(2, 2, key);
90
+ });
91
+
92
+ test("subscriber on transform target fires when a source updates", () => {
93
+ const plugin = new ContextPlugin();
94
+ new Player({ plugins: [plugin] });
95
+ const src = defineContextKey<number>("src", "Source");
96
+ const target = defineContextKey<number>("target", "Target");
97
+
98
+ plugin.registerTransform(target, {
99
+ sources: [src],
100
+ compute: (read) => (read(src) ?? 0) + 1,
101
+ });
102
+ const handler = vitest.fn();
103
+ plugin.subscribe(target, handler);
104
+
105
+ plugin.set(src, 41);
106
+ expect(handler).toHaveBeenCalledTimes(1);
107
+ expect(handler).toHaveBeenCalledWith(42, target);
108
+ });
109
+
110
+ test("subscribeAll receives every literal set and every dependent invalidation", () => {
111
+ const plugin = new ContextPlugin();
112
+ new Player({ plugins: [plugin] });
113
+ const src = defineContextKey<number>("src", "Source");
114
+ const target = defineContextKey<number>("target", "Target");
115
+
116
+ plugin.registerTransform(target, {
117
+ sources: [src],
118
+ compute: (read) => (read(src) ?? 0) * 2,
119
+ });
120
+ const handler = vitest.fn();
121
+ plugin.subscribeAll(handler);
122
+
123
+ plugin.set(src, 3);
124
+ expect(handler).toHaveBeenCalledTimes(2);
125
+ expect(handler).toHaveBeenNthCalledWith(1, 3, src);
126
+ expect(handler).toHaveBeenNthCalledWith(2, 6, target);
127
+ });
128
+
129
+ test("unsubscribe stops a per-key handler", () => {
130
+ const plugin = new ContextPlugin();
131
+ new Player({ plugins: [plugin] });
132
+ const key = defineContextKey<number>("k", "K");
133
+ const handler = vitest.fn();
134
+
135
+ const token = plugin.subscribe(key, handler);
136
+ plugin.set(key, 1);
137
+ plugin.unsubscribe(token);
138
+ plugin.set(key, 2);
139
+
140
+ expect(handler).toHaveBeenCalledTimes(1);
141
+ });
142
+
143
+ test("unsubscribe stops a global handler", () => {
144
+ const plugin = new ContextPlugin();
145
+ new Player({ plugins: [plugin] });
146
+ const key = defineContextKey<number>("k", "K");
147
+ const handler = vitest.fn();
148
+
149
+ const token = plugin.subscribeAll(handler);
150
+ plugin.set(key, 1);
151
+ plugin.unsubscribe(token);
152
+ plugin.set(key, 2);
153
+
154
+ expect(handler).toHaveBeenCalledTimes(1);
155
+ });
156
+
157
+ test("two ContextPlugin instances share state via singleton aliasing", () => {
158
+ const a = new ContextPlugin();
159
+ const b = new ContextPlugin();
160
+ new Player({ plugins: [a, b] });
161
+ const key = defineContextKey<string>("shared", "Shared");
162
+
163
+ a.set(key, "from-a");
164
+ expect(b.get(key)).toBe("from-a");
165
+ });
166
+
167
+ test("flow end freezes the store, pushes to history, then rotates", () => {
168
+ const plugin = new ContextPlugin();
169
+ const player = new Player({ plugins: [plugin] });
170
+ const key = defineContextKey<string>("phase", "Phase");
171
+
172
+ player.start(minimalFlow as any);
173
+ plugin.set(key, "active");
174
+ expect(plugin.get(key)).toBe("active");
175
+
176
+ player.hooks.onEnd.call();
177
+
178
+ expect(plugin.history()).toHaveLength(1);
179
+ const snapshot = plugin.history()[0];
180
+ expect(snapshot.flowId).toBe("flow-alpha");
181
+ expect(snapshot.entries.map((e) => e.value)).toEqual(["active"]);
182
+ expect(Object.isFrozen(snapshot)).toBe(true);
183
+
184
+ expect(plugin.has(key)).toBe(false);
185
+ expect(plugin.get(key)).toBeUndefined();
186
+ });
187
+
188
+ test("transforms and subscribers persist across flow rotation", () => {
189
+ const plugin = new ContextPlugin();
190
+ const player = new Player({ plugins: [plugin] });
191
+ const src = defineContextKey<number>("src", "Source");
192
+ const target = defineContextKey<number>("target", "Target");
193
+
194
+ plugin.registerTransform(target, {
195
+ sources: [src],
196
+ compute: (read) => (read(src) ?? 0) + 100,
197
+ });
198
+ const handler = vitest.fn();
199
+ plugin.subscribe(target, handler);
200
+
201
+ player.start(minimalFlow as any);
202
+ plugin.set(src, 1);
203
+ player.hooks.onEnd.call();
204
+
205
+ player.start(minimalFlow as any);
206
+ plugin.set(src, 2);
207
+
208
+ expect(handler).toHaveBeenCalledTimes(2);
209
+ expect(handler).toHaveBeenNthCalledWith(1, 101, target);
210
+ expect(handler).toHaveBeenNthCalledWith(2, 102, target);
211
+ });
212
+
213
+ test("re-registering a transform for the same key silently replaces", () => {
214
+ const plugin = new ContextPlugin();
215
+ new Player({ plugins: [plugin] });
216
+ const target = defineContextKey<string>("t", "T");
217
+
218
+ plugin.registerTransform(target, { sources: [], compute: () => "v1" });
219
+ plugin.registerTransform(target, { sources: [], compute: () => "v2" });
220
+
221
+ expect(plugin.get(target)).toBe("v2");
222
+ });
223
+
224
+ test("list exposes registered descriptors with hasValue / hasTransform flags", () => {
225
+ const plugin = new ContextPlugin();
226
+ new Player({ plugins: [plugin] });
227
+ const literal = defineContextKey<number>("lit", "Literal");
228
+ const derived = defineContextKey<number>("der", "Derived");
229
+
230
+ plugin.set(literal, 1);
231
+ plugin.registerTransform(derived, { sources: [], compute: () => 2 });
232
+
233
+ const items = plugin.list();
234
+ const byDesc = Object.fromEntries(items.map((d) => [d.description, d]));
235
+ expect(byDesc.Literal).toMatchObject({ hasValue: true, hasTransform: false });
236
+ expect(byDesc.Derived).toMatchObject({
237
+ hasValue: false,
238
+ hasTransform: true,
239
+ });
240
+ });
241
+
242
+ test("name-based bridge API round-trips set/get/has/subscribe", () => {
243
+ const plugin = new ContextPlugin();
244
+ new Player({ plugins: [plugin] });
245
+
246
+ expect(plugin.hasByName("formState")).toBe(false);
247
+ plugin.setByName("formState", "Current form state", { name: "Ada" });
248
+ expect(plugin.getByName("formState")).toEqual({ name: "Ada" });
249
+ expect(plugin.hasByName("formState")).toBe(true);
250
+
251
+ const handler = vitest.fn();
252
+ plugin.subscribeByName("counter", "A counter", handler);
253
+ plugin.setByName("counter", "A counter", 7);
254
+
255
+ expect(handler).toHaveBeenCalledWith(7, "counter");
256
+ });
257
+
258
+ test("subscribeAllByName surfaces the resolved key name and description", () => {
259
+ const plugin = new ContextPlugin();
260
+ new Player({ plugins: [plugin] });
261
+
262
+ const handler = vitest.fn();
263
+ plugin.subscribeAllByName(handler);
264
+
265
+ plugin.setByName("flag", "A flag", true);
266
+
267
+ expect(handler).toHaveBeenCalledWith(true, "flag", "A flag");
268
+ });
269
+
270
+ test("a function-valued context entry round-trips through get and is callable", () => {
271
+ const plugin = new ContextPlugin();
272
+ new Player({ plugins: [plugin] });
273
+
274
+ const addKey = defineContextKey<(a: number, b: number) => number>(
275
+ "math.add",
276
+ "Add two numbers",
277
+ );
278
+
279
+ expect(plugin.has(addKey)).toBe(false);
280
+ plugin.set(addKey, (a, b) => a + b);
281
+ expect(plugin.has(addKey)).toBe(true);
282
+ expect(plugin.get(addKey)!(2, 3)).toBe(5);
283
+ });
284
+
285
+ test("setting a function entry twice replaces the prior implementation", () => {
286
+ const plugin = new ContextPlugin();
287
+ new Player({ plugins: [plugin] });
288
+ const key = defineContextKey<() => string>("greet", "Greet");
289
+
290
+ plugin.set(key, () => "v1");
291
+ plugin.set(key, () => "v2");
292
+ expect(plugin.get(key)!()).toBe("v2");
293
+ });
294
+
295
+ test("function entries appear in list() like any other entry", () => {
296
+ const plugin = new ContextPlugin();
297
+ new Player({ plugins: [plugin] });
298
+ const a = defineContextKey<() => void>("a", "Action A");
299
+ const b = defineContextKey<() => void>("b", "Action B");
300
+
301
+ plugin.set(a, () => undefined);
302
+ plugin.set(b, () => undefined);
303
+
304
+ const descriptions = plugin.list().map((d) => d.description);
305
+ expect(descriptions).toEqual(
306
+ expect.arrayContaining(["Action A", "Action B"]),
307
+ );
308
+ });
309
+
310
+ test("getByName resolves a function entry so native consumers can invoke it", () => {
311
+ const plugin = new ContextPlugin();
312
+ new Player({ plugins: [plugin] });
313
+ const key = defineContextKey<(msg: string) => string>(
314
+ "echo",
315
+ "Echo the input",
316
+ );
317
+ plugin.set(key, (msg) => `said: ${msg}`);
318
+
319
+ const echo = plugin.getByName("echo") as (msg: string) => string;
320
+ expect(echo("hello")).toBe("said: hello");
321
+ });
322
+
323
+ test("singleton aliasing shares function entries across ContextPlugin instances", () => {
324
+ const a = new ContextPlugin();
325
+ const b = new ContextPlugin();
326
+ new Player({ plugins: [a, b] });
327
+ const key = defineContextKey<() => string>("shared", "Shared");
328
+
329
+ a.set(key, () => "from-a");
330
+ expect(b.get(key)!()).toBe("from-a");
331
+ });
332
+
333
+ test("flow-end freeze replaces a function entry with a throwing tombstone", () => {
334
+ const plugin = new ContextPlugin();
335
+ const player = new Player({ plugins: [plugin] });
336
+ const actionKey = defineContextKey<() => string>("do.thing", "Do the thing");
337
+
338
+ player.start(minimalFlow as any);
339
+ plugin.set(actionKey, () => "live");
340
+ expect(plugin.get(actionKey)!()).toBe("live");
341
+
342
+ // End the flow so the active store is frozen into a history snapshot.
343
+ player.hooks.onEnd.call();
344
+
345
+ const [snapshot] = plugin.history();
346
+ // Read the frozen entry by key — the same typed access as live context.
347
+ const frozen = snapshot.get(actionKey);
348
+ // The capability is preserved (still callable) but poisoned post-flow.
349
+ expect(typeof frozen).toBe("function");
350
+ expect(() => frozen!()).toThrowError(/no longer valid/);
351
+ });
352
+
353
+ test("snapshot.get returns undefined for a key absent when frozen", () => {
354
+ const plugin = new ContextPlugin();
355
+ const player = new Player({ plugins: [plugin] });
356
+ const present = defineContextKey<string>("present", "Present");
357
+ const absent = defineContextKey<string>("absent", "Absent");
358
+
359
+ player.start(minimalFlow as any);
360
+ plugin.set(present, "here");
361
+ player.hooks.onEnd.call();
362
+
363
+ const [snapshot] = plugin.history();
364
+ expect(snapshot.get(present)).toBe("here");
365
+ expect(snapshot.get(absent)).toBeUndefined();
366
+ });
367
+
368
+ test("getContextPlugin returns the existing plugin or registers a new one", () => {
369
+ const existing = new ContextPlugin();
370
+ const player = new Player({ plugins: [existing] });
371
+ expect(getContextPlugin(player)).toBe(existing);
372
+
373
+ const fresh = new Player();
374
+ const created = getContextPlugin(fresh);
375
+ expect(fresh.findPlugin<ContextPlugin>(ContextPluginSymbol)).toBe(created);
376
+ });