@dxos/app-framework 0.7.4 → 0.7.5-main.9cb18ac

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.
Files changed (128) hide show
  1. package/dist/lib/browser/chunk-QG25ZU2N.mjs +320 -0
  2. package/dist/lib/browser/chunk-QG25ZU2N.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-SPDTXTOV.mjs +163 -0
  4. package/dist/lib/browser/chunk-SPDTXTOV.mjs.map +7 -0
  5. package/dist/lib/browser/{chunk-653Y45CL.mjs → chunk-WBOXEHBE.mjs} +12 -2
  6. package/dist/lib/browser/chunk-WBOXEHBE.mjs.map +7 -0
  7. package/dist/lib/browser/index.mjs +224 -109
  8. package/dist/lib/browser/index.mjs.map +4 -4
  9. package/dist/lib/browser/meta.json +1 -1
  10. package/dist/lib/browser/{plugin-intent-LU4KL2RO.mjs → plugin-intent-T7Y3MJ5C.mjs} +14 -4
  11. package/dist/lib/browser/{plugin-settings-OM3G2QFY.mjs → plugin-settings-5U2L2NRU.mjs} +6 -2
  12. package/dist/lib/browser/{plugin-surface-LECZMKSQ.mjs → plugin-surface-OKPF3EQI.mjs} +4 -4
  13. package/dist/lib/node/{chunk-SOVLKUWI.cjs → chunk-BW3RNEVI.cjs} +51 -102
  14. package/dist/lib/node/chunk-BW3RNEVI.cjs.map +7 -0
  15. package/dist/lib/node/{chunk-JZ2JVKRY.cjs → chunk-FCMHRU3M.cjs} +17 -5
  16. package/dist/lib/node/chunk-FCMHRU3M.cjs.map +7 -0
  17. package/dist/lib/node/chunk-VWHAALIN.cjs +344 -0
  18. package/dist/lib/node/chunk-VWHAALIN.cjs.map +7 -0
  19. package/dist/lib/node/index.cjs +232 -114
  20. package/dist/lib/node/index.cjs.map +4 -4
  21. package/dist/lib/node/meta.json +1 -1
  22. package/dist/lib/node/plugin-intent-F3TQZIUR.cjs +53 -0
  23. package/dist/lib/node/plugin-intent-F3TQZIUR.cjs.map +7 -0
  24. package/dist/lib/node/{plugin-settings-OZ6IKAE5.cjs → plugin-settings-W6UHMH5M.cjs} +12 -8
  25. package/dist/lib/node/plugin-settings-W6UHMH5M.cjs.map +7 -0
  26. package/dist/lib/node/{plugin-surface-YWDRXQTD.cjs → plugin-surface-CCSIONYW.cjs} +15 -15
  27. package/dist/lib/node/plugin-surface-CCSIONYW.cjs.map +7 -0
  28. package/dist/lib/node-esm/{chunk-YNU7FTGJ.mjs → chunk-3T5UIJY3.mjs} +12 -2
  29. package/dist/lib/node-esm/chunk-3T5UIJY3.mjs.map +7 -0
  30. package/dist/lib/node-esm/chunk-4GX7V5ZE.mjs +164 -0
  31. package/dist/lib/node-esm/chunk-4GX7V5ZE.mjs.map +7 -0
  32. package/dist/lib/node-esm/chunk-CFOUYXQ6.mjs +321 -0
  33. package/dist/lib/node-esm/chunk-CFOUYXQ6.mjs.map +7 -0
  34. package/dist/lib/node-esm/index.mjs +224 -109
  35. package/dist/lib/node-esm/index.mjs.map +4 -4
  36. package/dist/lib/node-esm/meta.json +1 -1
  37. package/dist/lib/node-esm/{plugin-intent-V7ER24Y6.mjs → plugin-intent-W2HQC6LC.mjs} +14 -4
  38. package/dist/lib/node-esm/{plugin-settings-37UVWF2V.mjs → plugin-settings-H5RHNFVC.mjs} +6 -2
  39. package/dist/lib/node-esm/{plugin-surface-TEU42XQN.mjs → plugin-surface-V3YET3UL.mjs} +4 -4
  40. package/dist/types/src/plugins/common/layout.d.ts +145 -171
  41. package/dist/types/src/plugins/common/layout.d.ts.map +1 -1
  42. package/dist/types/src/plugins/common/navigation.d.ts +77 -30
  43. package/dist/types/src/plugins/common/navigation.d.ts.map +1 -1
  44. package/dist/types/src/plugins/plugin-host/HostPlugin.d.ts +2 -7
  45. package/dist/types/src/plugins/plugin-host/HostPlugin.d.ts.map +1 -1
  46. package/dist/types/src/plugins/plugin-host/index.d.ts +2 -0
  47. package/dist/types/src/plugins/plugin-host/index.d.ts.map +1 -1
  48. package/dist/types/src/plugins/plugin-host/plugin.d.ts +7 -1
  49. package/dist/types/src/plugins/plugin-host/plugin.d.ts.map +1 -1
  50. package/dist/types/src/plugins/plugin-intent/IntentContext.d.ts +7 -20
  51. package/dist/types/src/plugins/plugin-intent/IntentContext.d.ts.map +1 -1
  52. package/dist/types/src/plugins/plugin-intent/IntentPlugin.d.ts.map +1 -1
  53. package/dist/types/src/plugins/plugin-intent/index.d.ts +1 -0
  54. package/dist/types/src/plugins/plugin-intent/index.d.ts.map +1 -1
  55. package/dist/types/src/plugins/plugin-intent/intent-dispatcher.d.ts +107 -0
  56. package/dist/types/src/plugins/plugin-intent/intent-dispatcher.d.ts.map +1 -0
  57. package/dist/types/src/plugins/plugin-intent/intent-dispatcher.test.d.ts +2 -0
  58. package/dist/types/src/plugins/plugin-intent/intent-dispatcher.test.d.ts.map +1 -0
  59. package/dist/types/src/plugins/plugin-intent/intent.d.ts +65 -58
  60. package/dist/types/src/plugins/plugin-intent/intent.d.ts.map +1 -1
  61. package/dist/types/src/plugins/plugin-intent/meta.d.ts +1 -0
  62. package/dist/types/src/plugins/plugin-intent/meta.d.ts.map +1 -1
  63. package/dist/types/src/plugins/plugin-intent/provides.d.ts +6 -10
  64. package/dist/types/src/plugins/plugin-intent/provides.d.ts.map +1 -1
  65. package/dist/types/src/plugins/plugin-settings/provides.d.ts +15 -2
  66. package/dist/types/src/plugins/plugin-settings/provides.d.ts.map +1 -1
  67. package/dist/types/src/plugins/plugin-surface/Surface.d.ts +2 -57
  68. package/dist/types/src/plugins/plugin-surface/Surface.d.ts.map +1 -1
  69. package/dist/types/src/plugins/plugin-surface/SurfaceContext.d.ts +85 -0
  70. package/dist/types/src/plugins/plugin-surface/SurfaceContext.d.ts.map +1 -0
  71. package/dist/types/src/plugins/plugin-surface/SurfacePlugin.d.ts.map +1 -1
  72. package/dist/types/src/plugins/plugin-surface/index.d.ts +1 -1
  73. package/dist/types/src/plugins/plugin-surface/index.d.ts.map +1 -1
  74. package/dist/types/src/plugins/plugin-surface/provides.d.ts +5 -4
  75. package/dist/types/src/plugins/plugin-surface/provides.d.ts.map +1 -1
  76. package/dist/types/tsconfig.tsbuildinfo +1 -0
  77. package/package.json +14 -12
  78. package/src/plugins/common/layout.ts +125 -107
  79. package/src/plugins/common/navigation.ts +59 -30
  80. package/src/plugins/plugin-host/HostPlugin.tsx +2 -10
  81. package/src/plugins/plugin-host/PluginContainer.tsx +1 -1
  82. package/src/plugins/plugin-host/index.ts +4 -0
  83. package/src/plugins/plugin-host/plugin.ts +8 -1
  84. package/src/plugins/plugin-intent/IntentContext.tsx +13 -36
  85. package/src/plugins/plugin-intent/IntentPlugin.tsx +44 -120
  86. package/src/plugins/plugin-intent/index.ts +1 -0
  87. package/src/plugins/plugin-intent/intent-dispatcher.test.ts +279 -0
  88. package/src/plugins/plugin-intent/intent-dispatcher.ts +285 -0
  89. package/src/plugins/plugin-intent/intent.ts +126 -65
  90. package/src/plugins/plugin-intent/meta.ts +3 -1
  91. package/src/plugins/plugin-intent/provides.ts +8 -20
  92. package/src/plugins/plugin-settings/provides.ts +10 -5
  93. package/src/plugins/plugin-surface/Surface.tsx +25 -158
  94. package/src/plugins/plugin-surface/SurfaceContext.ts +112 -0
  95. package/src/plugins/plugin-surface/SurfacePlugin.tsx +19 -7
  96. package/src/plugins/plugin-surface/index.ts +1 -1
  97. package/src/plugins/plugin-surface/provides.ts +8 -7
  98. package/tsconfig.json +38 -1
  99. package/dist/lib/browser/chunk-653Y45CL.mjs.map +0 -7
  100. package/dist/lib/browser/chunk-FRXJ25VI.mjs +0 -214
  101. package/dist/lib/browser/chunk-FRXJ25VI.mjs.map +0 -7
  102. package/dist/lib/browser/chunk-YXM35XRE.mjs +0 -213
  103. package/dist/lib/browser/chunk-YXM35XRE.mjs.map +0 -7
  104. package/dist/lib/node/chunk-JZ2JVKRY.cjs.map +0 -7
  105. package/dist/lib/node/chunk-QSVP5HOW.cjs +0 -238
  106. package/dist/lib/node/chunk-QSVP5HOW.cjs.map +0 -7
  107. package/dist/lib/node/chunk-SOVLKUWI.cjs.map +0 -7
  108. package/dist/lib/node/plugin-intent-FVFR2LKB.cjs +0 -43
  109. package/dist/lib/node/plugin-intent-FVFR2LKB.cjs.map +0 -7
  110. package/dist/lib/node/plugin-settings-OZ6IKAE5.cjs.map +0 -7
  111. package/dist/lib/node/plugin-surface-YWDRXQTD.cjs.map +0 -7
  112. package/dist/lib/node-esm/chunk-2R4GVK7O.mjs +0 -215
  113. package/dist/lib/node-esm/chunk-2R4GVK7O.mjs.map +0 -7
  114. package/dist/lib/node-esm/chunk-YFMFQBB4.mjs +0 -214
  115. package/dist/lib/node-esm/chunk-YFMFQBB4.mjs.map +0 -7
  116. package/dist/lib/node-esm/chunk-YNU7FTGJ.mjs.map +0 -7
  117. package/dist/types/src/plugins/plugin-intent/helpers.d.ts +0 -6
  118. package/dist/types/src/plugins/plugin-intent/helpers.d.ts.map +0 -1
  119. package/dist/types/src/plugins/plugin-surface/SurfaceRootContext.d.ts +0 -39
  120. package/dist/types/src/plugins/plugin-surface/SurfaceRootContext.d.ts.map +0 -1
  121. package/src/plugins/plugin-intent/helpers.ts +0 -11
  122. package/src/plugins/plugin-surface/SurfaceRootContext.tsx +0 -60
  123. /package/dist/lib/browser/{plugin-intent-LU4KL2RO.mjs.map → plugin-intent-T7Y3MJ5C.mjs.map} +0 -0
  124. /package/dist/lib/browser/{plugin-settings-OM3G2QFY.mjs.map → plugin-settings-5U2L2NRU.mjs.map} +0 -0
  125. /package/dist/lib/browser/{plugin-surface-LECZMKSQ.mjs.map → plugin-surface-OKPF3EQI.mjs.map} +0 -0
  126. /package/dist/lib/node-esm/{plugin-intent-V7ER24Y6.mjs.map → plugin-intent-W2HQC6LC.mjs.map} +0 -0
  127. /package/dist/lib/node-esm/{plugin-settings-37UVWF2V.mjs.map → plugin-settings-H5RHNFVC.mjs.map} +0 -0
  128. /package/dist/lib/node-esm/{plugin-surface-TEU42XQN.mjs.map → plugin-surface-V3YET3UL.mjs.map} +0 -0
@@ -2,26 +2,20 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ import { Effect } from 'effect';
5
6
  import React from 'react';
6
7
 
7
8
  import { create } from '@dxos/live-object';
8
- import { log } from '@dxos/log';
9
9
 
10
- import { type IntentContext, type IntentExecution, IntentProvider } from './IntentContext';
11
- import { isUndoable } from './helpers';
12
- import type { Intent, IntentResolver } from './intent';
10
+ import { IntentProvider } from './IntentContext';
11
+ import { createDispatcher, type AnyIntentResolver, type IntentContext } from './intent-dispatcher';
13
12
  import IntentMeta from './meta';
14
- import {
15
- IntentAction,
16
- type IntentPluginProvides,
17
- type IntentResolverProvides,
18
- parseIntentResolverPlugin,
19
- } from './provides';
20
- import { filterPlugins, findPlugin } from '../helpers';
13
+ import { type IntentPluginProvides, type ResolverDefinitions, parseIntentResolverPlugin } from './provides';
14
+ import { filterPlugins } from '../helpers';
21
15
  import { type PluginDefinition } from '../plugin-host';
22
16
 
23
- const EXECUTION_LIMIT = 1000;
24
- const HISTORY_LIMIT = 100;
17
+ const defaultEffect = () => Effect.fail(new Error('Intent runtime not ready'));
18
+ const defaultPromise = () => Effect.runPromise(defaultEffect());
25
19
 
26
20
  /**
27
21
  * Allows plugins to register intent handlers and routes sent intents to the appropriate plugin.
@@ -29,119 +23,38 @@ const HISTORY_LIMIT = 100;
29
23
  */
30
24
  export const IntentPlugin = (): PluginDefinition<IntentPluginProvides> => {
31
25
  const state = create<IntentContext>({
32
- dispatch: async () => ({}),
33
- undo: async () => ({}),
34
- history: [],
26
+ dispatch: defaultEffect,
27
+ dispatchPromise: defaultPromise,
28
+ undo: defaultEffect,
29
+ undoPromise: defaultPromise,
35
30
  registerResolver: () => () => {},
36
31
  });
37
32
 
38
- const dynamicResolvers = new Set<{ plugin: string; resolver: IntentResolver }>();
39
-
40
33
  return {
41
34
  meta: IntentMeta,
42
- ready: async (plugins) => {
43
- // Dispatch intent to associated plugin.
44
- const dispatch = async (intent: Intent) => {
45
- log('dispatch', { action: intent.action, intent });
46
- if (intent.plugin) {
47
- for (const entry of dynamicResolvers) {
48
- if (entry.plugin === intent.plugin) {
49
- const result = await entry.resolver(intent, plugins);
50
- if (result) {
51
- return result;
52
- }
53
- }
54
- }
55
-
56
- const plugin = findPlugin<IntentResolverProvides>(plugins, intent.plugin);
57
- return plugin?.provides.intent.resolver(intent, plugins);
58
- }
59
-
60
- for (const entry of dynamicResolvers) {
61
- const result = await entry.resolver(intent, plugins);
62
- if (result) {
63
- return result;
64
- }
65
- }
66
-
67
- // Return resolved value from first plugin that handles the intent.
68
- for (const plugin of filterPlugins(plugins, parseIntentResolverPlugin)) {
69
- const result = await plugin.provides.intent.resolver(intent, plugins);
70
- if (result) {
71
- return result;
72
- }
73
- }
74
-
75
- // https://vitejs.dev/guide/env-and-mode#env-variables
76
- // TODO(wittjosiah): How to handle this more generically?
77
- if (import.meta?.env?.DEV) {
78
- log.warn('No plugin found to handle intent', intent);
79
- }
80
- };
81
-
82
- // Sequentially dispatch array of invents.
83
- const dispatchChain = async (intentOrArray: Intent | Intent[], depth = 0) => {
84
- if (depth > EXECUTION_LIMIT) {
85
- return {
86
- error: new Error(
87
- `Intent execution limit exceeded (${EXECUTION_LIMIT} iterations). This is likely due to an infinite loop within intent resolvers.`,
88
- ),
89
- };
90
- }
91
-
92
- const executionResults: IntentExecution[] = [];
93
- const chain = Array.isArray(intentOrArray) ? intentOrArray : [intentOrArray];
94
- for (const intent of chain) {
95
- const { result: prevResult } = executionResults.at(-1) ?? {};
96
- const data = intent.data ? { result: prevResult?.data, ...intent.data } : prevResult?.data;
97
- const result = await dispatch({ ...intent, data });
98
-
99
- if (!result || result?.error) {
100
- break;
101
- }
102
-
103
- executionResults.push({ intent, result });
104
-
105
- // TODO(wittjosiah): How does undo work with returned intents?
106
- result?.intents?.forEach((intents) => {
107
- void dispatchChain(intents, depth + 1);
108
- });
109
- }
110
-
111
- state.history.push(executionResults);
112
- if (state.history.length > HISTORY_LIMIT) {
113
- state.history.splice(0, state.history.length - HISTORY_LIMIT);
114
- }
115
-
116
- if (isUndoable(executionResults)) {
117
- void dispatch({ action: IntentAction.SHOW_UNDO, data: { results: executionResults } });
118
- }
119
-
120
- return executionResults.at(-1)?.result;
121
- };
122
-
123
- const undo = async () => {
124
- const last = state.history.findLastIndex(isUndoable);
125
- const chain =
126
- last !== -1 &&
127
- state.history[last]?.map(({ intent, result }): Intent => {
128
- const data = result.undoable?.data ? { ...intent.data, ...result.undoable.data } : intent.data;
129
- return { ...intent, data, undo: true };
130
- });
131
- if (chain) {
132
- const result = await dispatchChain(chain);
133
- state.history = state.history.filter((_, index) => index !== last);
134
- return result;
135
- }
136
- };
137
-
138
- state.dispatch = dispatchChain;
35
+ ready: async ({ plugins }) => {
36
+ const resolvers = Object.fromEntries(
37
+ filterPlugins(plugins, parseIntentResolverPlugin).map((plugin): [string, AnyIntentResolver[]] => {
38
+ const resolvers = reduceResolvers(
39
+ plugin.provides.intent.resolvers({
40
+ plugins,
41
+ dispatch: (intent) => state.dispatch(intent),
42
+ dispatchPromise: (intent) => state.dispatchPromise(intent),
43
+ undo: () => state.undo(),
44
+ undoPromise: () => state.undoPromise(),
45
+ registerResolver: (id, resolver) => state.registerResolver(id, resolver),
46
+ }),
47
+ );
48
+ return [plugin.meta.id, resolvers];
49
+ }),
50
+ );
51
+ const { dispatch, dispatchPromise, undo, undoPromise, registerResolver } = createDispatcher(resolvers);
52
+
53
+ state.dispatch = dispatch;
54
+ state.dispatchPromise = dispatchPromise;
139
55
  state.undo = undo;
140
- state.registerResolver = (plugin, resolver) => {
141
- const entry = { plugin, resolver };
142
- dynamicResolvers.add(entry);
143
- return () => dynamicResolvers.delete(entry);
144
- };
56
+ state.undoPromise = undoPromise;
57
+ state.registerResolver = registerResolver;
145
58
  },
146
59
  provides: {
147
60
  intent: state,
@@ -149,3 +62,14 @@ export const IntentPlugin = (): PluginDefinition<IntentPluginProvides> => {
149
62
  },
150
63
  };
151
64
  };
65
+
66
+ const reduceResolvers = (
67
+ definitions: ResolverDefinitions,
68
+ resolvers: AnyIntentResolver[] = [],
69
+ ): AnyIntentResolver[] => {
70
+ if (Array.isArray(definitions)) {
71
+ return definitions.reduce((acc: AnyIntentResolver[], definition) => reduceResolvers(definition, acc), resolvers);
72
+ }
73
+
74
+ return [...resolvers, definitions];
75
+ };
@@ -5,6 +5,7 @@
5
5
  import { IntentPlugin } from './IntentPlugin';
6
6
 
7
7
  export * from './intent';
8
+ export * from './intent-dispatcher';
8
9
  export * from './provides';
9
10
 
10
11
  export * from './IntentContext';
@@ -0,0 +1,279 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { Effect, Fiber, pipe } from 'effect';
6
+ import { describe, expect, test } from 'vitest';
7
+
8
+ import { S } from '@dxos/echo-schema';
9
+
10
+ import { chain, createIntent } from './intent';
11
+ import { createDispatcher, createResolver } from './intent-dispatcher';
12
+
13
+ describe('Intent dispatcher', () => {
14
+ test('throws error if no resolver found', async () => {
15
+ const { dispatchPromise } = createDispatcher({});
16
+ const { data, error } = await dispatchPromise(createIntent(ToString, { value: 1 }));
17
+
18
+ expect(data).toBe(undefined);
19
+ expect(error).toBeInstanceOf(Error);
20
+ });
21
+
22
+ test('matches intent to resolver and executes', async () => {
23
+ const { dispatchPromise } = createDispatcher({ test: [toStringResolver] });
24
+ const { data, error } = await dispatchPromise(createIntent(ToString, { value: 1 }));
25
+
26
+ expect(error).toBe(undefined);
27
+ expect(data?.string).toBe('1');
28
+ });
29
+
30
+ test('update resolvers', async () => {
31
+ const { dispatchPromise, registerResolver } = createDispatcher({ test: [] });
32
+ const { error } = await dispatchPromise(createIntent(ToString, { value: 1 }));
33
+
34
+ expect(error).toBeInstanceOf(Error);
35
+
36
+ const removeResolver = registerResolver('test', toStringResolver);
37
+
38
+ const { data } = await dispatchPromise(createIntent(ToString, { value: 1 }));
39
+
40
+ expect(data?.string).toBe('1');
41
+
42
+ removeResolver();
43
+
44
+ {
45
+ const { data, error } = await dispatchPromise(createIntent(ToString, { value: 1 }));
46
+
47
+ expect(data).toBe(undefined);
48
+ expect(error).toBeInstanceOf(Error);
49
+ }
50
+ });
51
+
52
+ test('compose intent effects', async () => {
53
+ const { dispatch } = createDispatcher({ test: [computeResolver] });
54
+ const program = Effect.gen(function* () {
55
+ const a = yield* dispatch(createIntent(Compute, { value: 1 }));
56
+ const b = yield* dispatch(createIntent(Compute, { value: 2 }));
57
+ return b.data!.value - a.data!.value;
58
+ });
59
+
60
+ expect(await Effect.runPromise(program)).toBe(2);
61
+ });
62
+
63
+ test('concurrent intent effects', async () => {
64
+ const { dispatch } = createDispatcher({ test: [computeResolver] });
65
+ const program = Effect.gen(function* () {
66
+ const fiberA = yield* Effect.fork(dispatch(createIntent(Compute, { value: 5 })));
67
+ const fiberB = yield* Effect.fork(dispatch(createIntent(Compute, { value: 2 })));
68
+ const [a, b] = yield* Fiber.join(Fiber.zip(fiberA, fiberB));
69
+ return b.data!.value - a.data!.value;
70
+ });
71
+
72
+ expect(await Effect.runPromise(program)).toBe(-6);
73
+ });
74
+
75
+ test('mix & match intent effects with promises', async () => {
76
+ const { dispatch, dispatchPromise } = createDispatcher({ test: [toStringResolver, computeResolver] });
77
+ const program = Effect.gen(function* () {
78
+ const a = yield* dispatch(createIntent(Compute, { value: 2 }));
79
+ const b = yield* dispatch(createIntent(ToString, { value: a.data!.value }));
80
+ return b.data?.string;
81
+ });
82
+
83
+ expect(await Effect.runPromise(program)).toBe('4');
84
+
85
+ const a = await dispatchPromise(createIntent(Compute, { value: 2 }));
86
+ const b = await dispatchPromise(createIntent(ToString, { value: a.data!.value }));
87
+
88
+ expect(b.data?.string).toBe('4');
89
+ });
90
+
91
+ test('undo intent', async () => {
92
+ const { dispatch, undo } = createDispatcher({ test: [computeResolver] });
93
+ const program = Effect.gen(function* () {
94
+ const a = yield* dispatch(createIntent(Compute, { value: 2 }));
95
+
96
+ expect(a.data?.value).toBe(4);
97
+
98
+ const b = yield* undo();
99
+
100
+ expect(b?.data?.value).toBe(2);
101
+ });
102
+
103
+ await Effect.runPromise(program);
104
+ });
105
+
106
+ test('chain intents', async () => {
107
+ const { dispatch } = createDispatcher({ test: [computeResolver, toStringResolver, concatResolver] });
108
+ const intent = pipe(createIntent(Compute, { value: 1 }), chain(ToString, {}), chain(Concat, { plus: '!' }));
109
+
110
+ expect(intent.first.action).toBe(Compute._tag);
111
+ expect(intent.last.action).toBe(Concat._tag);
112
+ expect(intent.all.length).toBe(3);
113
+
114
+ const program = Effect.gen(function* () {
115
+ const { data } = yield* dispatch(intent);
116
+ return data?.string;
117
+ });
118
+
119
+ expect(await Effect.runPromise(program)).toBe('2!');
120
+ });
121
+
122
+ test('undo chained intent', async () => {
123
+ const { dispatch, undo } = createDispatcher({ test: [computeResolver, toStringResolver, concatResolver] });
124
+ const intent = pipe(createIntent(Compute, { value: 1 }), chain(Compute, {}), chain(Compute, {}));
125
+ const program = Effect.gen(function* () {
126
+ const a = yield* dispatch(intent);
127
+
128
+ expect(a.data?.value).toBe(8);
129
+
130
+ const b = yield* undo();
131
+
132
+ expect(b?.data?.value).toBe(1);
133
+ });
134
+
135
+ await Effect.runPromise(program);
136
+ });
137
+
138
+ test('filter resolvers by plugin', async () => {
139
+ const otherComputeResolver = createResolver(Compute, async (data) => ({ data: { value: data?.value * 3 } }));
140
+ const { dispatch } = createDispatcher({ test: [computeResolver], other: [otherComputeResolver] });
141
+ const program = Effect.gen(function* () {
142
+ const a = yield* dispatch(createIntent(Compute, { value: 1 }));
143
+
144
+ expect(a.data?.value).toBe(2);
145
+
146
+ const b = yield* dispatch(createIntent(Compute, { value: 1 }, { plugin: 'other' }));
147
+
148
+ expect(b.data?.value).toBe(3);
149
+ });
150
+
151
+ await Effect.runPromise(program);
152
+ });
153
+
154
+ test('filter resolvers by predicate', async () => {
155
+ const conditionalComputeResolver = createResolver(Compute, async (data) => ({ data: { value: data?.value * 3 } }), {
156
+ filter: (data): data is { value: number } => data?.value > 1,
157
+ });
158
+ const { dispatch } = createDispatcher({ test: [conditionalComputeResolver, computeResolver] });
159
+ const program = Effect.gen(function* () {
160
+ const a = yield* dispatch(createIntent(Compute, { value: 1 }));
161
+
162
+ expect(a.data?.value).toBe(2);
163
+
164
+ const b = yield* dispatch(createIntent(Compute, { value: 2 }));
165
+
166
+ expect(b.data?.value).toBe(6);
167
+ });
168
+
169
+ await Effect.runPromise(program);
170
+ });
171
+
172
+ test('hoist resolvers', async () => {
173
+ const hoistedComputeResolver = createResolver(Compute, async (data) => ({ data: { value: data?.value * 3 } }), {
174
+ disposition: 'hoist',
175
+ });
176
+ const { dispatchPromise } = createDispatcher({ test: [computeResolver, hoistedComputeResolver] });
177
+ const { data } = await dispatchPromise(createIntent(Compute, { value: 1 }));
178
+ expect(data?.value).toBe(3);
179
+ });
180
+
181
+ test('fallback resolvers', async () => {
182
+ const conditionalComputeResolver = createResolver(Compute, async (data) => ({ data: { value: data?.value * 2 } }), {
183
+ filter: (data): data is { value: number } => data?.value === 1,
184
+ });
185
+ const fallbackComputeResolver = createResolver(Compute, async (data) => ({ data: { value: data?.value * 3 } }), {
186
+ disposition: 'fallback',
187
+ });
188
+ const { dispatch } = createDispatcher({ test: [fallbackComputeResolver, conditionalComputeResolver] });
189
+ const program = Effect.gen(function* () {
190
+ const a = yield* dispatch(createIntent(Compute, { value: 1 }));
191
+
192
+ expect(a.data?.value).toBe(2);
193
+
194
+ const b = yield* dispatch(createIntent(Compute, { value: 2 }));
195
+
196
+ expect(b.data?.value).toBe(6);
197
+ });
198
+
199
+ await Effect.runPromise(program);
200
+ });
201
+
202
+ test('non-struct inputs & outputs', async () => {
203
+ const { dispatchPromise } = createDispatcher({ test: [addResolver] });
204
+ const { data } = await dispatchPromise(createIntent(Add, [1, 1]));
205
+ expect(data).toBe(2);
206
+ });
207
+
208
+ test('empty inputs & outputs', async () => {
209
+ const { dispatchPromise } = createDispatcher({ test: [sideEffectResolver] });
210
+ const { data } = await dispatchPromise(createIntent(SideEffect));
211
+ expect(data).toBe(undefined);
212
+ });
213
+
214
+ test.todo('follow up intents');
215
+ });
216
+
217
+ class ToString extends S.TaggedClass<ToString>()('ToString', {
218
+ input: S.Struct({
219
+ value: S.Number,
220
+ }),
221
+ output: S.Struct({
222
+ string: S.String,
223
+ }),
224
+ }) {}
225
+
226
+ const toStringResolver = createResolver(ToString, async (data) => {
227
+ return { data: { string: data.value.toString() } };
228
+ });
229
+
230
+ class Compute extends S.TaggedClass<Compute>()('Compute', {
231
+ input: S.Struct({
232
+ value: S.Number,
233
+ }),
234
+ output: S.Struct({
235
+ value: S.Number,
236
+ }),
237
+ }) {}
238
+
239
+ const computeResolver = createResolver(Compute, (data, undo) => {
240
+ return Effect.gen(function* () {
241
+ if (undo) {
242
+ return { data: { value: data.value / 2 } };
243
+ }
244
+
245
+ yield* Effect.sleep(data.value * 10);
246
+ const value = data.value * 2;
247
+ return { data: { value }, undoable: { message: 'test', data: { value } } };
248
+ });
249
+ });
250
+
251
+ class Concat extends S.TaggedClass<Concat>()('Concat', {
252
+ input: S.Struct({
253
+ string: S.String,
254
+ plus: S.String,
255
+ }),
256
+ output: S.Struct({
257
+ string: S.String,
258
+ }),
259
+ }) {}
260
+
261
+ const concatResolver = createResolver(Concat, async (data) => {
262
+ return { data: { string: data.string + data.plus } };
263
+ });
264
+
265
+ class Add extends S.TaggedClass<Add>()('Add', {
266
+ input: S.Tuple(S.Number, S.Number),
267
+ output: S.Number,
268
+ }) {}
269
+
270
+ const addResolver = createResolver(Add, async (data) => {
271
+ return { data: data[0] + data[1] };
272
+ });
273
+
274
+ class SideEffect extends S.TaggedClass<SideEffect>()('SideEffect', {
275
+ input: S.Void,
276
+ output: S.Void,
277
+ }) {}
278
+
279
+ const sideEffectResolver = createResolver(SideEffect, async () => {});