@dxos/app-framework 0.7.4 → 0.7.5-main.937ce75
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/lib/browser/chunk-QG25ZU2N.mjs +320 -0
- package/dist/lib/browser/chunk-QG25ZU2N.mjs.map +7 -0
- package/dist/lib/browser/chunk-SPDTXTOV.mjs +163 -0
- package/dist/lib/browser/chunk-SPDTXTOV.mjs.map +7 -0
- package/dist/lib/browser/{chunk-653Y45CL.mjs → chunk-WBOXEHBE.mjs} +12 -2
- package/dist/lib/browser/chunk-WBOXEHBE.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +224 -109
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/{plugin-intent-LU4KL2RO.mjs → plugin-intent-T7Y3MJ5C.mjs} +14 -4
- package/dist/lib/browser/{plugin-settings-OM3G2QFY.mjs → plugin-settings-5U2L2NRU.mjs} +6 -2
- package/dist/lib/browser/{plugin-surface-LECZMKSQ.mjs → plugin-surface-OKPF3EQI.mjs} +4 -4
- package/dist/lib/node/{chunk-SOVLKUWI.cjs → chunk-BW3RNEVI.cjs} +51 -102
- package/dist/lib/node/chunk-BW3RNEVI.cjs.map +7 -0
- package/dist/lib/node/{chunk-JZ2JVKRY.cjs → chunk-FCMHRU3M.cjs} +17 -5
- package/dist/lib/node/chunk-FCMHRU3M.cjs.map +7 -0
- package/dist/lib/node/chunk-VWHAALIN.cjs +344 -0
- package/dist/lib/node/chunk-VWHAALIN.cjs.map +7 -0
- package/dist/lib/node/index.cjs +232 -114
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/plugin-intent-F3TQZIUR.cjs +53 -0
- package/dist/lib/node/plugin-intent-F3TQZIUR.cjs.map +7 -0
- package/dist/lib/node/{plugin-settings-OZ6IKAE5.cjs → plugin-settings-W6UHMH5M.cjs} +12 -8
- package/dist/lib/node/plugin-settings-W6UHMH5M.cjs.map +7 -0
- package/dist/lib/node/{plugin-surface-YWDRXQTD.cjs → plugin-surface-CCSIONYW.cjs} +15 -15
- package/dist/lib/node/plugin-surface-CCSIONYW.cjs.map +7 -0
- package/dist/lib/node-esm/{chunk-YNU7FTGJ.mjs → chunk-3T5UIJY3.mjs} +12 -2
- package/dist/lib/node-esm/chunk-3T5UIJY3.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-4GX7V5ZE.mjs +164 -0
- package/dist/lib/node-esm/chunk-4GX7V5ZE.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-CFOUYXQ6.mjs +321 -0
- package/dist/lib/node-esm/chunk-CFOUYXQ6.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +224 -109
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/{plugin-intent-V7ER24Y6.mjs → plugin-intent-W2HQC6LC.mjs} +14 -4
- package/dist/lib/node-esm/{plugin-settings-37UVWF2V.mjs → plugin-settings-H5RHNFVC.mjs} +6 -2
- package/dist/lib/node-esm/{plugin-surface-TEU42XQN.mjs → plugin-surface-V3YET3UL.mjs} +4 -4
- package/dist/types/src/plugins/common/layout.d.ts +145 -171
- package/dist/types/src/plugins/common/layout.d.ts.map +1 -1
- package/dist/types/src/plugins/common/navigation.d.ts +77 -30
- package/dist/types/src/plugins/common/navigation.d.ts.map +1 -1
- package/dist/types/src/plugins/plugin-host/HostPlugin.d.ts +2 -7
- package/dist/types/src/plugins/plugin-host/HostPlugin.d.ts.map +1 -1
- package/dist/types/src/plugins/plugin-host/index.d.ts +2 -0
- package/dist/types/src/plugins/plugin-host/index.d.ts.map +1 -1
- package/dist/types/src/plugins/plugin-host/plugin.d.ts +7 -1
- package/dist/types/src/plugins/plugin-host/plugin.d.ts.map +1 -1
- package/dist/types/src/plugins/plugin-intent/IntentContext.d.ts +7 -20
- package/dist/types/src/plugins/plugin-intent/IntentContext.d.ts.map +1 -1
- package/dist/types/src/plugins/plugin-intent/IntentPlugin.d.ts.map +1 -1
- package/dist/types/src/plugins/plugin-intent/index.d.ts +1 -0
- package/dist/types/src/plugins/plugin-intent/index.d.ts.map +1 -1
- package/dist/types/src/plugins/plugin-intent/intent-dispatcher.d.ts +107 -0
- package/dist/types/src/plugins/plugin-intent/intent-dispatcher.d.ts.map +1 -0
- package/dist/types/src/plugins/plugin-intent/intent-dispatcher.test.d.ts +2 -0
- package/dist/types/src/plugins/plugin-intent/intent-dispatcher.test.d.ts.map +1 -0
- package/dist/types/src/plugins/plugin-intent/intent.d.ts +65 -58
- package/dist/types/src/plugins/plugin-intent/intent.d.ts.map +1 -1
- package/dist/types/src/plugins/plugin-intent/meta.d.ts +1 -0
- package/dist/types/src/plugins/plugin-intent/meta.d.ts.map +1 -1
- package/dist/types/src/plugins/plugin-intent/provides.d.ts +6 -10
- package/dist/types/src/plugins/plugin-intent/provides.d.ts.map +1 -1
- package/dist/types/src/plugins/plugin-settings/provides.d.ts +15 -2
- package/dist/types/src/plugins/plugin-settings/provides.d.ts.map +1 -1
- package/dist/types/src/plugins/plugin-surface/Surface.d.ts +2 -57
- package/dist/types/src/plugins/plugin-surface/Surface.d.ts.map +1 -1
- package/dist/types/src/plugins/plugin-surface/SurfaceContext.d.ts +85 -0
- package/dist/types/src/plugins/plugin-surface/SurfaceContext.d.ts.map +1 -0
- package/dist/types/src/plugins/plugin-surface/SurfacePlugin.d.ts.map +1 -1
- package/dist/types/src/plugins/plugin-surface/index.d.ts +1 -1
- package/dist/types/src/plugins/plugin-surface/index.d.ts.map +1 -1
- package/dist/types/src/plugins/plugin-surface/provides.d.ts +5 -4
- package/dist/types/src/plugins/plugin-surface/provides.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +14 -12
- package/src/plugins/common/layout.ts +125 -107
- package/src/plugins/common/navigation.ts +59 -30
- package/src/plugins/plugin-host/HostPlugin.tsx +2 -10
- package/src/plugins/plugin-host/PluginContainer.tsx +1 -1
- package/src/plugins/plugin-host/index.ts +4 -0
- package/src/plugins/plugin-host/plugin.ts +8 -1
- package/src/plugins/plugin-intent/IntentContext.tsx +13 -36
- package/src/plugins/plugin-intent/IntentPlugin.tsx +44 -120
- package/src/plugins/plugin-intent/index.ts +1 -0
- package/src/plugins/plugin-intent/intent-dispatcher.test.ts +279 -0
- package/src/plugins/plugin-intent/intent-dispatcher.ts +285 -0
- package/src/plugins/plugin-intent/intent.ts +126 -65
- package/src/plugins/plugin-intent/meta.ts +3 -1
- package/src/plugins/plugin-intent/provides.ts +8 -20
- package/src/plugins/plugin-settings/provides.ts +10 -5
- package/src/plugins/plugin-surface/Surface.tsx +25 -158
- package/src/plugins/plugin-surface/SurfaceContext.ts +112 -0
- package/src/plugins/plugin-surface/SurfacePlugin.tsx +19 -7
- package/src/plugins/plugin-surface/index.ts +1 -1
- package/src/plugins/plugin-surface/provides.ts +8 -7
- package/tsconfig.json +38 -1
- package/dist/lib/browser/chunk-653Y45CL.mjs.map +0 -7
- package/dist/lib/browser/chunk-FRXJ25VI.mjs +0 -214
- package/dist/lib/browser/chunk-FRXJ25VI.mjs.map +0 -7
- package/dist/lib/browser/chunk-YXM35XRE.mjs +0 -213
- package/dist/lib/browser/chunk-YXM35XRE.mjs.map +0 -7
- package/dist/lib/node/chunk-JZ2JVKRY.cjs.map +0 -7
- package/dist/lib/node/chunk-QSVP5HOW.cjs +0 -238
- package/dist/lib/node/chunk-QSVP5HOW.cjs.map +0 -7
- package/dist/lib/node/chunk-SOVLKUWI.cjs.map +0 -7
- package/dist/lib/node/plugin-intent-FVFR2LKB.cjs +0 -43
- package/dist/lib/node/plugin-intent-FVFR2LKB.cjs.map +0 -7
- package/dist/lib/node/plugin-settings-OZ6IKAE5.cjs.map +0 -7
- package/dist/lib/node/plugin-surface-YWDRXQTD.cjs.map +0 -7
- package/dist/lib/node-esm/chunk-2R4GVK7O.mjs +0 -215
- package/dist/lib/node-esm/chunk-2R4GVK7O.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-YFMFQBB4.mjs +0 -214
- package/dist/lib/node-esm/chunk-YFMFQBB4.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-YNU7FTGJ.mjs.map +0 -7
- package/dist/types/src/plugins/plugin-intent/helpers.d.ts +0 -6
- package/dist/types/src/plugins/plugin-intent/helpers.d.ts.map +0 -1
- package/dist/types/src/plugins/plugin-surface/SurfaceRootContext.d.ts +0 -39
- package/dist/types/src/plugins/plugin-surface/SurfaceRootContext.d.ts.map +0 -1
- package/src/plugins/plugin-intent/helpers.ts +0 -11
- package/src/plugins/plugin-surface/SurfaceRootContext.tsx +0 -60
- /package/dist/lib/browser/{plugin-intent-LU4KL2RO.mjs.map → plugin-intent-T7Y3MJ5C.mjs.map} +0 -0
- /package/dist/lib/browser/{plugin-settings-OM3G2QFY.mjs.map → plugin-settings-5U2L2NRU.mjs.map} +0 -0
- /package/dist/lib/browser/{plugin-surface-LECZMKSQ.mjs.map → plugin-surface-OKPF3EQI.mjs.map} +0 -0
- /package/dist/lib/node-esm/{plugin-intent-V7ER24Y6.mjs.map → plugin-intent-W2HQC6LC.mjs.map} +0 -0
- /package/dist/lib/node-esm/{plugin-settings-37UVWF2V.mjs.map → plugin-settings-H5RHNFVC.mjs.map} +0 -0
- /package/dist/lib/node-esm/{plugin-surface-TEU42XQN.mjs.map → plugin-surface-V3YET3UL.mjs.map} +0 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Effect, Ref } from 'effect';
|
|
6
|
+
|
|
7
|
+
import { type MaybePromise, pick } from '@dxos/util';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
createIntent,
|
|
11
|
+
IntentAction,
|
|
12
|
+
type AnyIntent,
|
|
13
|
+
type AnyIntentChain,
|
|
14
|
+
type Intent,
|
|
15
|
+
type IntentChain,
|
|
16
|
+
type IntentData,
|
|
17
|
+
type IntentParams,
|
|
18
|
+
type IntentResultData,
|
|
19
|
+
type IntentSchema,
|
|
20
|
+
type Label,
|
|
21
|
+
} from './intent';
|
|
22
|
+
|
|
23
|
+
const EXECUTION_LIMIT = 100;
|
|
24
|
+
const HISTORY_LIMIT = 100;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The return value of an intent effect.
|
|
28
|
+
*/
|
|
29
|
+
export type IntentEffectResult<Fields extends IntentParams> = {
|
|
30
|
+
/**
|
|
31
|
+
* The output of the action that was performed.
|
|
32
|
+
*
|
|
33
|
+
* If the intent is apart of a chain of intents, the data will be passed to the next intent.
|
|
34
|
+
*/
|
|
35
|
+
data?: IntentResultData<Fields>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* If provided, the action will be undoable.
|
|
39
|
+
*/
|
|
40
|
+
undoable?: {
|
|
41
|
+
/**
|
|
42
|
+
* Message to display to the user when indicating that the action can be undone.
|
|
43
|
+
*/
|
|
44
|
+
message: Label;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Will be merged with the original intent data when firing the undo intent.
|
|
48
|
+
*/
|
|
49
|
+
data?: Partial<IntentData<Fields>>;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* An error that occurred while performing the action.
|
|
54
|
+
*
|
|
55
|
+
* If the intent is apart of a chain of intents and an error occurs, the chain will be aborted.
|
|
56
|
+
*/
|
|
57
|
+
error?: Error;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Other intent chains to be triggered.
|
|
61
|
+
*/
|
|
62
|
+
intents?: AnyIntentChain[];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type AnyIntentEffectResult = IntentEffectResult<any>;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The result of an intent dispatcher.
|
|
69
|
+
*/
|
|
70
|
+
export type IntentDispatcherResult<Fields extends IntentParams> = Pick<IntentEffectResult<Fields>, 'data' | 'error'>;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Determines the priority of the effect when multiple intent resolvers are matched.
|
|
74
|
+
*
|
|
75
|
+
* - `static` - The effect is selected in the order it was resolved.
|
|
76
|
+
* - `hoist` - The effect is selected before `static` effects.
|
|
77
|
+
* - `fallback` - The effect is selected after `static` effects.
|
|
78
|
+
*/
|
|
79
|
+
export type IntentDisposition = 'static' | 'hoist' | 'fallback';
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The implementation of an intent effect.
|
|
83
|
+
*/
|
|
84
|
+
export type IntentEffectDefinition<Fields extends IntentParams> = (
|
|
85
|
+
data: IntentData<Fields>,
|
|
86
|
+
undo: boolean,
|
|
87
|
+
) => MaybePromise<IntentEffectResult<Fields> | void> | Effect.Effect<IntentEffectResult<Fields> | void>;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Intent resolver to match intents to their effects.
|
|
91
|
+
*/
|
|
92
|
+
export type IntentResolver<Tag extends string, Fields extends IntentParams> = {
|
|
93
|
+
action: Tag;
|
|
94
|
+
disposition?: IntentDisposition;
|
|
95
|
+
// TODO(wittjosiah): Would be nice to make this a guard for intents with optional data.
|
|
96
|
+
filter?: (data: IntentData<Fields>) => boolean;
|
|
97
|
+
effect: IntentEffectDefinition<Fields>;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export type AnyIntentResolver = IntentResolver<any, any>;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Creates an intent resolver to match intents to their effects.
|
|
104
|
+
* @param schema Schema of the intent. Must be a tagged class with input and output schemas.
|
|
105
|
+
* @param effect Effect to be performed when the intent is resolved.
|
|
106
|
+
* @param params.disposition Determines the priority of the resolver when multiple are resolved.
|
|
107
|
+
* @param params.filter Optional filter to determine if the resolver should be used.
|
|
108
|
+
*/
|
|
109
|
+
export const createResolver = <Tag extends string, Fields extends IntentParams>(
|
|
110
|
+
schema: IntentSchema<Tag, Fields>,
|
|
111
|
+
effect: IntentEffectDefinition<Fields>,
|
|
112
|
+
params: Pick<IntentResolver<Tag, Fields>, 'disposition' | 'filter'> = {},
|
|
113
|
+
): IntentResolver<Tag, Fields> => ({
|
|
114
|
+
action: schema._tag,
|
|
115
|
+
effect,
|
|
116
|
+
...params,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Invokes intents and returns the result.
|
|
121
|
+
*/
|
|
122
|
+
export type PromiseIntentDispatcher = <Fields extends IntentParams>(
|
|
123
|
+
intent: IntentChain<any, any, any, Fields>,
|
|
124
|
+
) => Promise<IntentDispatcherResult<Fields>>;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Creates an effect for intents.
|
|
128
|
+
*/
|
|
129
|
+
export type IntentDispatcher = <Fields extends IntentParams>(
|
|
130
|
+
intent: IntentChain<any, any, any, Fields>,
|
|
131
|
+
depth?: number,
|
|
132
|
+
) => Effect.Effect<IntentDispatcherResult<Fields>, Error>;
|
|
133
|
+
|
|
134
|
+
type IntentResult<Tag extends string, Fields extends IntentParams> = IntentEffectResult<Fields> & {
|
|
135
|
+
_intent: Intent<Tag, Fields>;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
type AnyIntentResult = IntentResult<any, any>;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Invokes the most recent undoable intent with undo flags.
|
|
142
|
+
*/
|
|
143
|
+
export type PromiseIntentUndo = () => Promise<IntentDispatcherResult<any> | undefined>;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Creates an effect which undoes the last intent.
|
|
147
|
+
*/
|
|
148
|
+
export type IntentUndo = () => Effect.Effect<IntentDispatcherResult<any> | undefined, Error>;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Check if a chain of results is undoable.
|
|
152
|
+
*/
|
|
153
|
+
const isUndoable = (historyEntry: AnyIntentResult[]): boolean =>
|
|
154
|
+
historyEntry.length > 0 && historyEntry.every(({ undoable }) => !!undoable);
|
|
155
|
+
|
|
156
|
+
export type IntentContext = {
|
|
157
|
+
dispatch: IntentDispatcher;
|
|
158
|
+
dispatchPromise: PromiseIntentDispatcher;
|
|
159
|
+
undo: IntentUndo;
|
|
160
|
+
undoPromise: PromiseIntentUndo;
|
|
161
|
+
registerResolver: (id: string, resolver: AnyIntentResolver) => () => void;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Sets of an intent dispatcher.
|
|
166
|
+
*
|
|
167
|
+
* @param resolvers An array of available intent resolvers.
|
|
168
|
+
* @param params.historyLimit The maximum number of intent results to keep in history.
|
|
169
|
+
* @param params.executionLimit The maximum recursion depth of intent chains.
|
|
170
|
+
*/
|
|
171
|
+
export const createDispatcher = (
|
|
172
|
+
resolvers: Record<string, AnyIntentResolver[]>,
|
|
173
|
+
{ executionLimit = EXECUTION_LIMIT, historyLimit = HISTORY_LIMIT } = {},
|
|
174
|
+
): IntentContext => {
|
|
175
|
+
const historyRef = Effect.runSync(Ref.make<AnyIntentResult[][]>([]));
|
|
176
|
+
|
|
177
|
+
const handleIntent = (intent: AnyIntent) => {
|
|
178
|
+
return Effect.gen(function* () {
|
|
179
|
+
const candidates = Object.entries(resolvers)
|
|
180
|
+
.filter(([id, _]) => (intent.plugin ? id === intent.plugin : true))
|
|
181
|
+
.flatMap(([_, resolvers]) => resolvers)
|
|
182
|
+
.filter((r) => r.action === intent.action)
|
|
183
|
+
.filter((r) => !r.filter || r.filter(intent.data))
|
|
184
|
+
.toSorted(({ disposition: a = 'static' }, { disposition: b = 'static' }) => {
|
|
185
|
+
return a === b ? 0 : a === 'hoist' || b === 'fallback' ? -1 : b === 'hoist' || a === 'fallback' ? 1 : 0;
|
|
186
|
+
});
|
|
187
|
+
if (candidates.length === 0) {
|
|
188
|
+
return {
|
|
189
|
+
_intent: intent,
|
|
190
|
+
error: new Error(`No resolver found for action: ${intent.action}`),
|
|
191
|
+
} satisfies AnyIntentResult;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const effect = candidates[0].effect(intent.data, intent.undo ?? false);
|
|
195
|
+
const result = Effect.isEffect(effect) ? yield* effect : yield* Effect.promise(async () => effect);
|
|
196
|
+
return { _intent: intent, ...result } satisfies AnyIntentResult;
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const dispatch: IntentDispatcher = (intentChain, depth = 0) => {
|
|
201
|
+
return Effect.gen(function* () {
|
|
202
|
+
if (depth > executionLimit) {
|
|
203
|
+
yield* Effect.fail(
|
|
204
|
+
new Error('Intent execution limit exceeded. This is likely due to an infinite loop within intent resolvers.'),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const resultsRef = yield* Ref.make<AnyIntentResult[]>([]);
|
|
209
|
+
for (const intent of intentChain.all) {
|
|
210
|
+
const { data: prev } = (yield* resultsRef.get)[0] ?? {};
|
|
211
|
+
const result = yield* handleIntent({ ...intent, data: { ...intent.data, ...prev } });
|
|
212
|
+
yield* Ref.update(resultsRef, (results) => [result, ...results]);
|
|
213
|
+
if (result.error) {
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
if (result.intents) {
|
|
217
|
+
for (const intent of result.intents) {
|
|
218
|
+
// Returned intents are dispatched but not yielded into results,
|
|
219
|
+
// as such they cannot be undone.
|
|
220
|
+
// TODO(wittjosiah): Use higher execution concurrency?
|
|
221
|
+
yield* dispatch(intent, depth + 1);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const results = yield* resultsRef.get;
|
|
227
|
+
const result = results[0];
|
|
228
|
+
if (result) {
|
|
229
|
+
yield* Ref.update(historyRef, (history) => {
|
|
230
|
+
const next = [...history, results];
|
|
231
|
+
if (next.length > historyLimit) {
|
|
232
|
+
next.splice(0, next.length - historyLimit);
|
|
233
|
+
}
|
|
234
|
+
return next;
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
if (result.undoable && isUndoable(results)) {
|
|
238
|
+
// TODO(wittjosiah): Is there a better way to handle showing undo for chains?
|
|
239
|
+
yield* dispatch(createIntent(IntentAction.ShowUndo, { message: result.undoable.message }));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return pick(result, ['data', 'error']);
|
|
243
|
+
} else {
|
|
244
|
+
return { data: {}, error: new Error('No results') };
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const dispatchPromise: PromiseIntentDispatcher = (intentChain) => {
|
|
250
|
+
const program = dispatch(intentChain);
|
|
251
|
+
return Effect.runPromise(program);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const undo: IntentUndo = () => {
|
|
255
|
+
return Effect.gen(function* () {
|
|
256
|
+
const history = yield* historyRef.get;
|
|
257
|
+
const last = history.findLastIndex(isUndoable);
|
|
258
|
+
const result = last !== -1 ? history[last] : undefined;
|
|
259
|
+
if (result) {
|
|
260
|
+
const all = result.map(({ _intent, undoable }): AnyIntent => {
|
|
261
|
+
const data = _intent.data;
|
|
262
|
+
const undoData = undoable?.data ?? {};
|
|
263
|
+
return { ..._intent, data: { ...data, ...undoData }, undo: true } satisfies AnyIntent;
|
|
264
|
+
});
|
|
265
|
+
const intent = { first: all[0], last: all.at(-1)!, all } satisfies AnyIntentChain;
|
|
266
|
+
yield* Ref.update(historyRef, (h) => h.filter((_, index) => index !== last));
|
|
267
|
+
return yield* dispatch(intent);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const undoPromise: PromiseIntentUndo = () => {
|
|
273
|
+
const program = undo();
|
|
274
|
+
return Effect.runPromise(program);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const registerResolver = (id: string, resolver: AnyIntentResolver) => {
|
|
278
|
+
resolvers[id] = [...(resolvers[id] ?? []), resolver];
|
|
279
|
+
return () => {
|
|
280
|
+
resolvers[id] = resolvers[id].filter((r) => r !== resolver);
|
|
281
|
+
};
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
return { dispatch, dispatchPromise, undo, undoPromise, registerResolver };
|
|
285
|
+
};
|
|
@@ -2,100 +2,161 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import { S } from '@dxos/echo-schema';
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import { INTENT_PLUGIN } from './meta';
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export type IntentData<T extends Record<string, any> = Record<string, any>> = T & {
|
|
13
|
-
/**
|
|
14
|
-
* The data from the result of the previous intent.
|
|
15
|
-
*/
|
|
16
|
-
// TODO(burdon): Chainable types? (see Effect hooks).
|
|
17
|
-
result?: any;
|
|
9
|
+
export type IntentParams = {
|
|
10
|
+
readonly input: S.Schema.All;
|
|
11
|
+
readonly output: S.Schema.All;
|
|
18
12
|
};
|
|
19
13
|
|
|
14
|
+
export type IntentData<Fields extends IntentParams> =
|
|
15
|
+
S.Schema.Type<S.Struct<Fields>> extends { readonly input: any } ? S.Schema.Type<S.Struct<Fields>>['input'] : any;
|
|
16
|
+
|
|
17
|
+
export type IntentResultData<Fields extends IntentParams> =
|
|
18
|
+
S.Schema.Type<S.Struct<Fields>> extends { readonly output: any } ? S.Schema.Type<S.Struct<Fields>>['output'] : any;
|
|
19
|
+
|
|
20
|
+
export type IntentSchema<Tag extends string, Fields extends IntentParams> = S.TaggedClass<any, Tag, Fields>;
|
|
21
|
+
|
|
20
22
|
/**
|
|
21
23
|
* An intent is an abstract description of an operation to be performed.
|
|
22
24
|
* Intents allow actions to be performed across plugins.
|
|
23
25
|
*/
|
|
24
|
-
export type Intent = {
|
|
25
|
-
|
|
26
|
-
* Plugin ID.
|
|
27
|
-
* If specified, the intent will be sent explicitly to the plugin.
|
|
28
|
-
* Otherwise, the intent will be sent to all plugins, in order and the first to resolve a non-null value will be used.
|
|
29
|
-
*/
|
|
30
|
-
plugin?: string;
|
|
26
|
+
export type Intent<Tag extends string, Fields extends IntentParams> = {
|
|
27
|
+
_schema: IntentSchema<Tag, Fields>;
|
|
31
28
|
|
|
32
29
|
/**
|
|
33
30
|
* The action to perform.
|
|
34
31
|
*/
|
|
35
|
-
action:
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Whether or not the intent is being undone.
|
|
39
|
-
*/
|
|
40
|
-
undo?: boolean;
|
|
32
|
+
action: Tag;
|
|
41
33
|
|
|
42
34
|
/**
|
|
43
35
|
* Any data needed to perform the desired action.
|
|
44
36
|
*/
|
|
45
|
-
|
|
46
|
-
data?: IntentData;
|
|
47
|
-
};
|
|
37
|
+
data: IntentData<Fields>;
|
|
48
38
|
|
|
49
|
-
export type IntentResult = {
|
|
50
39
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
40
|
+
* Plugin ID.
|
|
41
|
+
* If specified, the intent will be sent explicitly to the plugin.
|
|
42
|
+
* Otherwise, the intent will be sent to all plugins, in order and the first to resolve a non-null value will be used.
|
|
54
43
|
*/
|
|
55
|
-
|
|
44
|
+
plugin?: string;
|
|
56
45
|
|
|
57
46
|
/**
|
|
58
|
-
*
|
|
47
|
+
* Whether or not the intent is being undone.
|
|
59
48
|
*/
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
* Message to display to the user when indicating that the action can be undone.
|
|
63
|
-
*/
|
|
64
|
-
message: string;
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Will be merged with the original intent data when firing the undo intent.
|
|
68
|
-
*/
|
|
69
|
-
data?: IntentData;
|
|
70
|
-
};
|
|
49
|
+
undo?: boolean;
|
|
50
|
+
};
|
|
71
51
|
|
|
72
|
-
|
|
73
|
-
* An error that occurred while performing the action.
|
|
74
|
-
*
|
|
75
|
-
* If the intent is apart of a chain of intents and an error occurs, the chain will be aborted.
|
|
76
|
-
*/
|
|
77
|
-
error?: Error;
|
|
52
|
+
export type AnyIntent = Intent<any, any>;
|
|
78
53
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Chain of intents to be executed together.
|
|
56
|
+
* The result of each intent is merged into the next intent's input data.
|
|
57
|
+
*/
|
|
58
|
+
export type IntentChain<
|
|
59
|
+
FirstTag extends string,
|
|
60
|
+
LastTag extends string,
|
|
61
|
+
FirstFields extends IntentParams,
|
|
62
|
+
LastFields extends IntentParams,
|
|
63
|
+
> = {
|
|
64
|
+
first: Intent<FirstTag, FirstFields>;
|
|
65
|
+
last: Intent<LastTag, LastFields>;
|
|
66
|
+
all: AnyIntent[];
|
|
83
67
|
};
|
|
84
68
|
|
|
69
|
+
export type AnyIntentChain = IntentChain<any, any, any, any>;
|
|
70
|
+
|
|
85
71
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* @
|
|
72
|
+
* Creates a typed intent.
|
|
73
|
+
* @param schema Schema of the intent. Must be a tagged class with input and output schemas.
|
|
74
|
+
* @param data Data fulfilling the input schema of the intent.
|
|
75
|
+
* @param params.plugin Optional plugin ID to send the intent to.
|
|
76
|
+
* @param params.undo Optional flag to indicate that the intent is being undone. Generally not set manually.
|
|
90
77
|
*/
|
|
91
|
-
|
|
92
|
-
|
|
78
|
+
export const createIntent = <Tag extends string, Fields extends IntentParams>(
|
|
79
|
+
schema: IntentSchema<Tag, Fields>,
|
|
80
|
+
data: IntentData<Fields> = {},
|
|
81
|
+
params: Pick<AnyIntent, 'plugin' | 'undo'> = {},
|
|
82
|
+
): IntentChain<Tag, Tag, Fields, Fields> => {
|
|
83
|
+
// The output of validateSync breaks proxy objects so this is just used for validation.
|
|
84
|
+
// TODO(wittjosiah): Is there a better way to make theses types align?
|
|
85
|
+
const _ = S.validateSync(schema.fields.input as S.Schema<any, any, unknown>)(data);
|
|
86
|
+
const intent = {
|
|
87
|
+
...params,
|
|
88
|
+
_schema: schema,
|
|
89
|
+
action: schema._tag,
|
|
90
|
+
data,
|
|
91
|
+
} satisfies Intent<Tag, Fields>;
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
first: intent,
|
|
95
|
+
last: intent,
|
|
96
|
+
all: [intent],
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// TODO(wittjosiah): Add a function for mapping the output of one intent to the input of another.
|
|
93
101
|
|
|
94
102
|
/**
|
|
95
|
-
*
|
|
96
|
-
* If the intent is not handled, nothing should be returned.
|
|
103
|
+
* Chain two intents together.
|
|
97
104
|
*
|
|
98
|
-
*
|
|
105
|
+
* NOTE: Chaining of intents depends on the data inputs and outputs being structs.
|
|
99
106
|
*/
|
|
100
|
-
|
|
101
|
-
|
|
107
|
+
export const chain =
|
|
108
|
+
<
|
|
109
|
+
FirstTag extends string,
|
|
110
|
+
NextTag extends string,
|
|
111
|
+
FirstFields extends IntentParams,
|
|
112
|
+
LastFields extends IntentParams,
|
|
113
|
+
NextFields extends IntentParams,
|
|
114
|
+
>(
|
|
115
|
+
schema: IntentSchema<NextTag, NextFields>,
|
|
116
|
+
data: Omit<IntentData<NextFields>, keyof IntentResultData<LastFields>> = {},
|
|
117
|
+
params: Pick<AnyIntent, 'plugin' | 'undo'> = {},
|
|
118
|
+
) =>
|
|
119
|
+
(
|
|
120
|
+
intent: IntentChain<FirstTag, any, FirstFields, LastFields>,
|
|
121
|
+
): IntentChain<FirstTag, NextTag, FirstFields, NextFields> => {
|
|
122
|
+
const intents = 'all' in intent ? intent.all : [intent];
|
|
123
|
+
const first = intents[0];
|
|
124
|
+
const last = {
|
|
125
|
+
...params,
|
|
126
|
+
_schema: schema,
|
|
127
|
+
action: schema._tag,
|
|
128
|
+
data,
|
|
129
|
+
} satisfies Intent<NextTag, NextFields>;
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
first,
|
|
133
|
+
last,
|
|
134
|
+
all: [...intents, last],
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
//
|
|
139
|
+
// Intents
|
|
140
|
+
//
|
|
141
|
+
|
|
142
|
+
// NOTE: Should maintain compatibility with `i18next` (and @dxos/react-ui).
|
|
143
|
+
// TODO(wittjosiah): Making this immutable breaks type compatibility.
|
|
144
|
+
export const Label = S.Union(
|
|
145
|
+
S.String,
|
|
146
|
+
S.mutable(S.Tuple(S.String, S.mutable(S.Struct({ ns: S.String, count: S.optional(S.Number) })))),
|
|
147
|
+
);
|
|
148
|
+
export type Label = S.Schema.Type<typeof Label>;
|
|
149
|
+
|
|
150
|
+
export const INTENT_ACTION = `${INTENT_PLUGIN}/action`;
|
|
151
|
+
|
|
152
|
+
export namespace IntentAction {
|
|
153
|
+
/**
|
|
154
|
+
* Fired after an intent is dispatched if the intent is undoable.
|
|
155
|
+
*/
|
|
156
|
+
export class ShowUndo extends S.TaggedClass<ShowUndo>()(`${INTENT_ACTION}/show-undo`, {
|
|
157
|
+
input: S.Struct({
|
|
158
|
+
message: Label,
|
|
159
|
+
}),
|
|
160
|
+
output: S.Void,
|
|
161
|
+
}) {}
|
|
162
|
+
}
|
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type IntentContext } from './
|
|
6
|
-
import { type
|
|
7
|
-
|
|
5
|
+
import { type IntentContext, type AnyIntentResolver } from './intent-dispatcher';
|
|
6
|
+
import { type HostContext, type Plugin } from '../plugin-host';
|
|
7
|
+
|
|
8
|
+
type Context = HostContext & IntentContext;
|
|
9
|
+
|
|
10
|
+
export type ResolverDefinitions = AnyIntentResolver | AnyIntentResolver[] | ResolverDefinitions[];
|
|
8
11
|
|
|
9
12
|
export type IntentResolverProvides = {
|
|
10
13
|
intent: {
|
|
11
|
-
|
|
14
|
+
resolvers: (context: Context) => ResolverDefinitions;
|
|
12
15
|
};
|
|
13
16
|
};
|
|
14
17
|
|
|
@@ -20,19 +23,4 @@ export const parseIntentPlugin = (plugin: Plugin) =>
|
|
|
20
23
|
(plugin.provides as any).intent?.dispatch ? (plugin as Plugin<IntentPluginProvides>) : undefined;
|
|
21
24
|
|
|
22
25
|
export const parseIntentResolverPlugin = (plugin: Plugin) =>
|
|
23
|
-
(plugin.provides as any).intent?.
|
|
24
|
-
|
|
25
|
-
//
|
|
26
|
-
// Intents
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
const INTENT_PLUGIN = 'dxos.org/plugin/intent';
|
|
30
|
-
|
|
31
|
-
const INTENT_ACTION = `${INTENT_PLUGIN}/action`;
|
|
32
|
-
|
|
33
|
-
export enum IntentAction {
|
|
34
|
-
/**
|
|
35
|
-
* Fired after an intent is dispatched if the intent is undoable.
|
|
36
|
-
*/
|
|
37
|
-
SHOW_UNDO = `${INTENT_ACTION}/show-undo`,
|
|
38
|
-
}
|
|
26
|
+
(plugin.provides as any).intent?.resolvers ? (plugin as Plugin<IntentResolverProvides>) : undefined;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { S } from '@dxos/echo-schema';
|
|
5
6
|
import { type SettingsStoreFactory, type SettingsValue } from '@dxos/local-storage';
|
|
6
7
|
|
|
7
8
|
import { type Plugin } from '../plugin-host';
|
|
@@ -20,10 +21,14 @@ export const parseSettingsPlugin = (plugin: Plugin) => {
|
|
|
20
21
|
: undefined;
|
|
21
22
|
};
|
|
22
23
|
|
|
23
|
-
const SETTINGS_PLUGIN = 'dxos.org/plugin/settings';
|
|
24
|
+
export const SETTINGS_PLUGIN = 'dxos.org/plugin/settings';
|
|
25
|
+
export const SETTINGS_ACTION = `${SETTINGS_PLUGIN}/action`;
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
export namespace SettingsAction {
|
|
28
|
+
export class Open extends S.TaggedClass<Open>()(`${SETTINGS_ACTION}/open`, {
|
|
29
|
+
input: S.Struct({
|
|
30
|
+
plugin: S.optional(S.String),
|
|
31
|
+
}),
|
|
32
|
+
output: S.Void,
|
|
33
|
+
}) {}
|
|
29
34
|
}
|