@platforma-sdk/ui-vue 1.40.6 → 1.41.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.
Files changed (94) hide show
  1. package/.turbo/turbo-build.log +49 -22
  2. package/.turbo/turbo-type-check.log +1 -1
  3. package/CHANGELOG.md +20 -0
  4. package/dist/AgGridVue/useAgGridOptions.js +54 -53
  5. package/dist/AgGridVue/useAgGridOptions.js.map +1 -1
  6. package/dist/components/BlockLayout.vue.d.ts.map +1 -1
  7. package/dist/components/BlockLayout.vue.js +7 -50
  8. package/dist/components/BlockLayout.vue.js.map +1 -1
  9. package/dist/components/BlockLayout.vue2.js +53 -2
  10. package/dist/components/BlockLayout.vue2.js.map +1 -1
  11. package/dist/components/BlockLayout.vue3.js +9 -0
  12. package/dist/components/BlockLayout.vue3.js.map +1 -0
  13. package/dist/components/NotFound.vue.d.ts.map +1 -1
  14. package/dist/components/NotFound.vue.js +12 -14
  15. package/dist/components/NotFound.vue.js.map +1 -1
  16. package/dist/components/PlAgDataTable/PlAgRowCount.vue.js +7 -6
  17. package/dist/components/PlAgDataTable/PlAgRowCount.vue.js.map +1 -1
  18. package/dist/components/PlAgDataTable/sources/table-source-v2.js +12 -12
  19. package/dist/components/PlAgRowNumCheckbox/PlAgRowNumCheckbox.vue.js +10 -9
  20. package/dist/components/PlAgRowNumCheckbox/PlAgRowNumCheckbox.vue.js.map +1 -1
  21. package/dist/components/PlAgRowNumHeader.vue.js +3 -2
  22. package/dist/components/PlAgRowNumHeader.vue.js.map +1 -1
  23. package/dist/components/PlAnnotations/components/DynamicForm.vue2.js +10 -10
  24. package/dist/components/PlAnnotations/components/PlAnnotationCreateDialog.vue.d.ts +1 -1
  25. package/dist/components/PlAnnotations/components/PlAnnotationCreateDialog.vue.d.ts.map +1 -1
  26. package/dist/components/PlMultiSequenceAlignment/data.js +51 -51
  27. package/dist/composition/fileContent.js +16 -16
  28. package/dist/defineApp.d.ts +19 -6
  29. package/dist/defineApp.d.ts.map +1 -1
  30. package/dist/defineApp.js +50 -30
  31. package/dist/defineApp.js.map +1 -1
  32. package/dist/internal/UpdateSerializer.d.ts +26 -0
  33. package/dist/internal/UpdateSerializer.d.ts.map +1 -0
  34. package/dist/internal/UpdateSerializer.js +65 -0
  35. package/dist/internal/UpdateSerializer.js.map +1 -0
  36. package/dist/internal/createAppModel.d.ts +1 -1
  37. package/dist/internal/createAppModel.d.ts.map +1 -1
  38. package/dist/internal/createAppModel.js +43 -51
  39. package/dist/internal/createAppModel.js.map +1 -1
  40. package/dist/internal/{createApp.d.ts → createAppV1.d.ts} +4 -4
  41. package/dist/internal/createAppV1.d.ts.map +1 -0
  42. package/dist/internal/{createApp.js → createAppV1.js} +17 -18
  43. package/dist/internal/createAppV1.js.map +1 -0
  44. package/dist/internal/createAppV2.d.ts +56 -0
  45. package/dist/internal/createAppV2.d.ts.map +1 -0
  46. package/dist/internal/createAppV2.js +158 -0
  47. package/dist/internal/createAppV2.js.map +1 -0
  48. package/dist/internal/test-helpers/BlockMock.d.ts +28 -0
  49. package/dist/internal/test-helpers/BlockMock.d.ts.map +1 -0
  50. package/dist/internal/test-helpers/createMockApi.d.ts +4 -0
  51. package/dist/internal/test-helpers/createMockApi.d.ts.map +1 -0
  52. package/dist/internal/test-helpers/utils.d.ts +4 -0
  53. package/dist/internal/test-helpers/utils.d.ts.map +1 -0
  54. package/dist/{types.static-test.d.ts → internal/v1.static-test.d.ts} +3 -3
  55. package/dist/internal/v1.static-test.d.ts.map +1 -0
  56. package/dist/internal/v2.static-test.d.ts +7 -0
  57. package/dist/internal/v2.static-test.d.ts.map +1 -0
  58. package/dist/lib/model/common/dist/index.js +214 -199
  59. package/dist/lib/model/common/dist/index.js.map +1 -1
  60. package/dist/lib/ui/uikit/dist/lib/model/common/dist/index.js +8 -8
  61. package/dist/lib/ui/uikit/dist/sdk/model/dist/index.js +64 -43
  62. package/dist/lib/ui/uikit/dist/sdk/model/dist/index.js.map +1 -1
  63. package/dist/lib/util/helpers/dist/index.js +67 -56
  64. package/dist/lib/util/helpers/dist/index.js.map +1 -1
  65. package/dist/node_modules/.pnpm/fast-json-patch@3.1.1/node_modules/fast-json-patch/index.js +29 -0
  66. package/dist/node_modules/.pnpm/fast-json-patch@3.1.1/node_modules/fast-json-patch/index.js.map +1 -0
  67. package/dist/node_modules/.pnpm/fast-json-patch@3.1.1/node_modules/fast-json-patch/module/core.js +208 -0
  68. package/dist/node_modules/.pnpm/fast-json-patch@3.1.1/node_modules/fast-json-patch/module/core.js.map +1 -0
  69. package/dist/node_modules/.pnpm/fast-json-patch@3.1.1/node_modules/fast-json-patch/module/duplex.js +95 -0
  70. package/dist/node_modules/.pnpm/fast-json-patch@3.1.1/node_modules/fast-json-patch/module/duplex.js.map +1 -0
  71. package/dist/node_modules/.pnpm/fast-json-patch@3.1.1/node_modules/fast-json-patch/module/helpers.js +112 -0
  72. package/dist/node_modules/.pnpm/fast-json-patch@3.1.1/node_modules/fast-json-patch/module/helpers.js.map +1 -0
  73. package/dist/sdk/model/dist/index.js +151 -131
  74. package/dist/sdk/model/dist/index.js.map +1 -1
  75. package/dist/types.d.ts +2 -2
  76. package/dist/types.d.ts.map +1 -1
  77. package/package.json +9 -6
  78. package/src/components/BlockLayout.vue +9 -1
  79. package/src/components/NotFound.vue +1 -3
  80. package/src/defineApp.ts +134 -36
  81. package/src/internal/UpdateSerializer.ts +112 -0
  82. package/src/internal/createAppModel.ts +7 -20
  83. package/src/internal/{createApp.ts → createAppV1.ts} +10 -6
  84. package/src/internal/createAppV2.test.ts +158 -0
  85. package/src/internal/createAppV2.ts +309 -0
  86. package/src/internal/test-helpers/BlockMock.ts +144 -0
  87. package/src/internal/test-helpers/createMockApi.ts +92 -0
  88. package/src/internal/test-helpers/utils.ts +65 -0
  89. package/src/{types.static-test.ts → internal/v1.static-test.ts} +5 -9
  90. package/src/internal/v2.static-test.ts +98 -0
  91. package/src/types.ts +2 -2
  92. package/dist/internal/createApp.d.ts.map +0 -1
  93. package/dist/internal/createApp.js.map +0 -1
  94. package/dist/types.static-test.d.ts.map +0 -1
@@ -0,0 +1,158 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { createAppV2 } from './createAppV2';
3
+ import { unwrapResult, type ValueOrErrors } from '@platforma-sdk/model';
4
+ import { BlockStateMock } from './test-helpers/BlockMock';
5
+ import { BlockMock } from './test-helpers/BlockMock';
6
+ import { delay } from '@milaboratories/helpers';
7
+ import { createMockApi } from './test-helpers/createMockApi';
8
+ import { watch } from 'vue';
9
+ import { patchPoolingDelay } from './createAppV2';
10
+
11
+ type Args = {
12
+ x: number;
13
+ y: number;
14
+ };
15
+
16
+ type UiState = {
17
+ label: string;
18
+ delay?: number;
19
+ };
20
+
21
+ const defaultArgs = () => ({
22
+ x: 0,
23
+ y: 0,
24
+ });
25
+
26
+ type Outputs = {
27
+ sum: ValueOrErrors<number>;
28
+ };
29
+
30
+ const defaultOutputs = (): Outputs => {
31
+ return {
32
+ sum: {
33
+ ok: true,
34
+ value: 0,
35
+ },
36
+ };
37
+ };
38
+
39
+ const defaultState = (): BlockStateMock<Args, Outputs, UiState, `/${string}`> => {
40
+ return new BlockStateMock(defaultArgs(), defaultOutputs(), { label: '' }, '/');
41
+ };
42
+
43
+ class BlockSum extends BlockMock<Args, Outputs, UiState, `/${string}`> {
44
+ async process(): Promise<void> {
45
+ const { args, author } = this.state;
46
+ this.state.setState({ author, outputs: { sum: { ok: true, value: args.x + args.y } } });
47
+ }
48
+ }
49
+
50
+ export const platforma = createMockApi<Args, Outputs, UiState>(new BlockSum(defaultState()));
51
+
52
+ describe('createApp', { timeout: 20_000 }, () => {
53
+ beforeEach(() => {
54
+ // Mock window.addEventListener to prevent actual event listeners
55
+ vi.stubGlobal('window', {
56
+ addEventListener: vi.fn(),
57
+ });
58
+ });
59
+
60
+ it('should create an app with reactive snapshot', async () => {
61
+ const initialState = await platforma.loadBlockState().then(unwrapResult);
62
+
63
+ const app = createAppV2(initialState, platforma, { debug: true, debounceSpan: 10 });
64
+
65
+ expect(app.model.args).toEqual({ x: 0, y: 0 });
66
+ expect(app.model.ui).toEqual({ label: '' });
67
+ expect(app.snapshot.navigationState.href).toBe('/');
68
+
69
+ let watchCountShallow = 0;
70
+
71
+ watch(() => app.model.args, () => {
72
+ watchCountShallow++;
73
+ });
74
+
75
+ app.model.args.x = 1;
76
+ app.model.args.y = 2;
77
+
78
+ const t1 = performance.now();
79
+ await app.allSettled();
80
+ await delay(100);
81
+ const t2 = performance.now();
82
+ console.log('allSettled', t2 - t1);
83
+
84
+ expect(app.model.args).toEqual(app.snapshot.args);
85
+ expect(app.model.args).toEqual({ x: 1, y: 2 });
86
+ expect(app.model.outputs.sum).toEqual(3);
87
+
88
+ app.model.args.x = 3;
89
+ app.model.args.y = 3;
90
+
91
+ const t3 = performance.now();
92
+ await app.allSettled();
93
+ await delay(patchPoolingDelay + 10);
94
+ const t4 = performance.now();
95
+ console.log('allSettled', t4 - t3);
96
+
97
+ expect(watchCountShallow).toBe(0); // no changes
98
+
99
+ expect(app.model.args).toEqual(app.snapshot.args);
100
+ expect(app.model.args).toEqual({ x: 3, y: 3 });
101
+ expect(app.model.outputs.sum).toEqual(6);
102
+
103
+ app.closedRef = true;
104
+ });
105
+
106
+ it('states should be synchronized', async () => {
107
+ const sharedBlock = new BlockSum(defaultState());
108
+ const platforma1 = createMockApi<Args, Outputs, UiState>(sharedBlock);
109
+ const platforma2 = createMockApi<Args, Outputs, UiState>(sharedBlock);
110
+
111
+ const initialState1 = await platforma1.loadBlockState().then(unwrapResult);
112
+ const initialState2 = await platforma2.loadBlockState().then(unwrapResult);
113
+
114
+ const app1 = createAppV2(initialState1, platforma1, { appId: 'app1', debug: true, debounceSpan: 10 });
115
+ const app2 = createAppV2(initialState2, platforma2, { appId: 'app2', debug: true, debounceSpan: 10 });
116
+
117
+ app1.model.args.x = 1;
118
+ app1.model.args.y = 2;
119
+
120
+ await app1.allSettled();
121
+ await app2.allSettled();
122
+
123
+ expect(app1.model.args).toEqual(app2.model.args);
124
+ expect(app1.model.args).toEqual({ x: 1, y: 2 });
125
+ expect(app1.model.outputs.sum).toEqual(3);
126
+ expect(app2.model.outputs.sum).toEqual(3);
127
+
128
+ app2.model.args.x = 3;
129
+ app2.model.args.y = 4;
130
+
131
+ await Promise.all([app1.allSettled(), app2.allSettled()]);
132
+ await delay(patchPoolingDelay + 10);
133
+
134
+ expect(app1.model.args).toEqual(app2.model.args);
135
+ expect(app1.model.args).toEqual({ x: 3, y: 4 });
136
+ expect(app1.model.outputs.sum).toEqual(7);
137
+ expect(app2.model.outputs.sum).toEqual(7);
138
+
139
+ app1.model.args.x = 5;
140
+ app1.model.args.y = 5;
141
+ app1.model.ui.delay = 20;
142
+ await delay(5);
143
+ app2.model.args.x = 7;
144
+ app2.model.args.y = 7;
145
+
146
+ await Promise.all([app1.allSettled(), app2.allSettled()]);
147
+ await delay(patchPoolingDelay + 10);
148
+
149
+ expect(app1.model.args).toEqual(app1.snapshot.args);
150
+ expect(app1.model.args).toEqual(app2.model.args);
151
+ expect(app1.model.args).toEqual({ x: 5, y: 5 });
152
+ expect(app1.model.outputs.sum).toEqual(10);
153
+ expect(app2.model.outputs.sum).toEqual(10);
154
+
155
+ app1.closedRef = true;
156
+ app2.closedRef = true;
157
+ });
158
+ });
@@ -0,0 +1,309 @@
1
+ import { deepClone, delay, uniqueId } from '@milaboratories/helpers';
2
+ import type { Mutable } from '@milaboratories/helpers';
3
+ import type { NavigationState, BlockOutputsBase, BlockState, PlatformaV2, ValueWithUTag, AuthorMarker } from '@platforma-sdk/model';
4
+ import { hasAbortError, unwrapResult } from '@platforma-sdk/model';
5
+ import type { Ref } from 'vue';
6
+ import { reactive, computed, ref } from 'vue';
7
+ import type { StateModelOptions, UnwrapOutputs, OutputValues, OutputErrors, AppSettings } from '../types';
8
+ import { createModel } from '../createModel';
9
+ import { createAppModel } from './createAppModel';
10
+ import { parseQuery } from '../urls';
11
+ import { MultiError, unwrapValueOrErrors } from '../utils';
12
+ import { applyPatch } from 'fast-json-patch';
13
+ import { UpdateSerializer } from './UpdateSerializer';
14
+
15
+ export const patchPoolingDelay = 100;
16
+
17
+ export const createNextAuthorMarker = (marker: AuthorMarker | undefined): AuthorMarker => ({
18
+ authorId: marker?.authorId ?? uniqueId(),
19
+ localVersion: (marker?.localVersion ?? 0) + 1,
20
+ });
21
+
22
+ /**
23
+ * Creates an application instance with reactive state management, outputs, and methods for state updates and navigation.
24
+ *
25
+ * @template Args - The type of arguments used in the application.
26
+ * @template Outputs - The type of block outputs extending `BlockOutputsBase`.
27
+ * @template UiState - The type of the UI state.
28
+ * @template Href - The type of navigation href, defaulting to a string starting with `/`.
29
+ *
30
+ * @param state - Initial state of the application, including args, outputs, UI state, and navigation state.
31
+ * @param platforma - A platform interface for interacting with block states.
32
+ * @param settings - Application settings, such as debug flags.
33
+ *
34
+ * @returns A reactive application object with methods, getters, and state.
35
+ */
36
+ export function createAppV2<
37
+ Args = unknown,
38
+ Outputs extends BlockOutputsBase = BlockOutputsBase,
39
+ UiState = unknown,
40
+ Href extends `/${string}` = `/${string}`,
41
+ >(
42
+ state: ValueWithUTag<BlockState<Args, Outputs, UiState, Href>>,
43
+ platforma: PlatformaV2<Args, Outputs, UiState, Href>,
44
+ settings: AppSettings,
45
+ ) {
46
+ type AppModel = {
47
+ args: Args;
48
+ ui: UiState;
49
+ };
50
+
51
+ const debug = (msg: string, ...rest: unknown[]) => {
52
+ if (settings.debug) {
53
+ console.log(`%c>>> %c${msg}`, 'color: orange; font-weight: bold', 'color: orange', settings.appId, ...rest);
54
+ }
55
+ };
56
+
57
+ const error = (msg: string, ...rest: unknown[]) => {
58
+ console.error(`%c>>> %c${msg}`, 'color: red; font-weight: bold', 'color: red', settings.appId, ...rest);
59
+ };
60
+
61
+ const data = {
62
+ isExternalSnapshot: false,
63
+ author: {
64
+ authorId: uniqueId(),
65
+ localVersion: 0,
66
+ },
67
+ };
68
+
69
+ const nextAuthorMarker = () => {
70
+ data.author = createNextAuthorMarker(data.author);
71
+ debug('nextAuthorMarker', data.author);
72
+ return data.author;
73
+ };
74
+
75
+ const closedRef = ref(false);
76
+
77
+ const uTagRef = ref(state.uTag);
78
+
79
+ const debounceSpan = settings.debounceSpan ?? 200;
80
+
81
+ const setArgsQueue = new UpdateSerializer({ debounceSpan });
82
+ const setUiStateQueue = new UpdateSerializer({ debounceSpan });
83
+ const setArgsAndUiStateQueue = new UpdateSerializer({ debounceSpan });
84
+ const setNavigationStateQueue = new UpdateSerializer({ debounceSpan });
85
+ /**
86
+ * Reactive snapshot of the application state, including args, outputs, UI state, and navigation state.
87
+ */
88
+ const snapshot = ref<{
89
+ args: Args;
90
+ outputs: Partial<Outputs>;
91
+ ui: UiState;
92
+ navigationState: NavigationState<Href>;
93
+ }>(state.value) as Ref<{
94
+ args: Args;
95
+ outputs: Partial<Outputs>;
96
+ ui: UiState;
97
+ navigationState: NavigationState<Href>;
98
+ }>;
99
+
100
+ const setBlockArgs = async (args: Args) => {
101
+ return platforma.setBlockArgs(args, nextAuthorMarker());
102
+ };
103
+
104
+ const setBlockUiState = async (ui: UiState) => {
105
+ return platforma.setBlockUiState(ui, nextAuthorMarker());
106
+ };
107
+
108
+ const setBlockArgsAndUiState = async (args: Args, ui: UiState) => {
109
+ return platforma.setBlockArgsAndUiState(args, ui, nextAuthorMarker());
110
+ };
111
+
112
+ const setNavigationState = async (state: NavigationState<Href>) => {
113
+ return platforma.setNavigationState(state);
114
+ };
115
+
116
+ (async () => {
117
+ window.addEventListener('beforeunload', () => {
118
+ closedRef.value = true;
119
+ platforma.dispose().then(unwrapResult).catch((err) => {
120
+ error('error in dispose', err);
121
+ });
122
+ });
123
+
124
+ while (!closedRef.value) {
125
+ try {
126
+ const patches = await platforma.getPatches(uTagRef.value).then(unwrapResult);
127
+
128
+ debug('patches', JSON.stringify(patches, null, 2));
129
+ debug('uTagRef.value', uTagRef.value);
130
+ debug('patches.uTag', patches.uTag);
131
+ debug('patches.author', patches.author);
132
+ debug('data.author', data.author);
133
+
134
+ uTagRef.value = patches.uTag;
135
+
136
+ if (patches.value.length === 0) {
137
+ await new Promise((resolve) => setTimeout(resolve, patchPoolingDelay));
138
+ continue;
139
+ }
140
+
141
+ const isAuthorChanged = data.author?.authorId !== patches.author?.authorId;
142
+
143
+ // Immutable behavior, apply external changes to the snapshot
144
+ if (isAuthorChanged || data.isExternalSnapshot) {
145
+ debug('got external changes, applying them to the snapshot', JSON.stringify(snapshot.value, null, 2));
146
+ snapshot.value = applyPatch(snapshot.value, patches.value, false, false).newDocument;
147
+ data.isExternalSnapshot = isAuthorChanged;
148
+ } else {
149
+ // Mutable behavior
150
+ snapshot.value = applyPatch(snapshot.value, patches.value).newDocument;
151
+ }
152
+
153
+ await new Promise((resolve) => setTimeout(resolve, patchPoolingDelay));
154
+ } catch (err) {
155
+ if (hasAbortError(err)) {
156
+ debug('patches loop aborted');
157
+ closedRef.value = true;
158
+ } else {
159
+ error('error in patches loop', err);
160
+ await new Promise((resolve) => setTimeout(resolve, 1000));
161
+ }
162
+ }
163
+ }
164
+ })();
165
+
166
+ const outputs = computed<OutputValues<Outputs>>(() => {
167
+ const entries = Object.entries(snapshot.value.outputs as Partial<Readonly<Outputs>>).map(([k, vOrErr]) => [k, vOrErr.ok && vOrErr.value !== undefined ? vOrErr.value : undefined]);
168
+ return Object.fromEntries(entries);
169
+ });
170
+
171
+ const outputErrors = computed<OutputErrors<Outputs>>(() => {
172
+ const entries = Object.entries(snapshot.value.outputs as Partial<Readonly<Outputs>>).map(([k, vOrErr]) => [k, vOrErr && !vOrErr.ok ? new MultiError(vOrErr.errors) : undefined]);
173
+ return Object.fromEntries(entries);
174
+ });
175
+
176
+ const appModel = createAppModel(
177
+ {
178
+ get() {
179
+ return { args: snapshot.value.args, ui: snapshot.value.ui } as AppModel;
180
+ },
181
+ autoSave: true,
182
+ onSave(newData: AppModel) {
183
+ debug('onSave', newData);
184
+ setArgsAndUiStateQueue.run(() => setBlockArgsAndUiState(newData.args, newData.ui).then(unwrapResult));
185
+ },
186
+ },
187
+ {
188
+ outputs,
189
+ outputErrors,
190
+ },
191
+ settings,
192
+ );
193
+
194
+ const cloneArgs = () => deepClone(appModel.model.args) as Args;
195
+ const cloneUiState = () => deepClone(appModel.model.ui) as UiState;
196
+ const cloneNavigationState = () => deepClone(snapshot.value.navigationState) as Mutable<NavigationState<Href>>;
197
+
198
+ const methods = {
199
+ cloneArgs,
200
+ cloneUiState,
201
+ cloneNavigationState,
202
+ createArgsModel<T extends Args = Args>(options: StateModelOptions<Args, T> = {}) {
203
+ return createModel<T, Args>({
204
+ get() {
205
+ if (options.transform) {
206
+ return options.transform(snapshot.value.args as Args);
207
+ }
208
+
209
+ return snapshot.value.args as T;
210
+ },
211
+ validate: options.validate,
212
+ autoSave: true,
213
+ onSave(newArgs) {
214
+ setArgsQueue.run(() => setBlockArgs(newArgs).then(unwrapResult));
215
+ },
216
+ });
217
+ },
218
+ /**
219
+ * defaultUiState is temporarily here, remove it after implementing initialUiState
220
+ */
221
+ createUiModel<T extends UiState = UiState>(options: StateModelOptions<UiState, T> = {}, defaultUiState: () => UiState) {
222
+ return createModel<T, UiState>({
223
+ get() {
224
+ if (options.transform) {
225
+ return options.transform(snapshot.value.ui as UiState);
226
+ }
227
+
228
+ return (snapshot.value.ui ?? defaultUiState()) as T;
229
+ },
230
+ validate: options.validate,
231
+ autoSave: true,
232
+ onSave(newData) {
233
+ setUiStateQueue.run(() => setBlockUiState(newData).then(unwrapResult));
234
+ },
235
+ });
236
+ },
237
+ /**
238
+ * Retrieves the unwrapped values of outputs for the given keys.
239
+ *
240
+ * @template K - Keys of the outputs to unwrap.
241
+ * @param keys - List of output names.
242
+ * @throws Error if the outputs contain errors.
243
+ * @returns An object with unwrapped output values.
244
+ */
245
+ unwrapOutputs<K extends keyof Outputs>(...keys: K[]): UnwrapOutputs<Outputs, K> {
246
+ const outputs = snapshot.value.outputs as Partial<Readonly<Outputs>>;
247
+ const entries = keys.map((key) => [key, unwrapValueOrErrors(outputs[key])]);
248
+ return Object.fromEntries(entries);
249
+ },
250
+ /**
251
+ * Updates the arguments state by applying a callback.
252
+ *
253
+ * @param cb - Callback to modify the current arguments.
254
+ * @returns A promise resolving after the update is applied.
255
+ */
256
+ updateArgs(cb: (args: Args) => void): Promise<boolean> {
257
+ const newArgs = cloneArgs();
258
+ cb(newArgs);
259
+ debug('updateArgs', newArgs);
260
+ appModel.model.args = newArgs;
261
+ return setArgsQueue.run(() => setBlockArgs(newArgs).then(unwrapResult));
262
+ },
263
+ /**
264
+ * Updates the UI state by applying a callback.
265
+ *
266
+ * @param cb - Callback to modify the current UI state.
267
+ * @returns A promise resolving after the update is applied.
268
+ * @todo Make it mutable since there is already an initial one
269
+ */
270
+ updateUiState(cb: (args: UiState) => UiState): Promise<boolean> {
271
+ const newUiState = cb(cloneUiState());
272
+ debug('updateUiState', newUiState);
273
+ appModel.model.ui = newUiState;
274
+ return setUiStateQueue.run(() => setBlockUiState(newUiState).then(unwrapResult));
275
+ },
276
+ /**
277
+ * Navigates to a specific href by updating the navigation state.
278
+ *
279
+ * @param href - The target href to navigate to.
280
+ * @returns A promise resolving after the navigation state is updated.
281
+ */
282
+ navigateTo(href: Href) {
283
+ const newState = cloneNavigationState();
284
+ newState.href = href;
285
+ return setNavigationStateQueue.run(() => setNavigationState(newState).then(unwrapResult));
286
+ },
287
+ async allSettled() {
288
+ await delay(0);
289
+ return setArgsAndUiStateQueue.allSettled();
290
+ },
291
+ };
292
+
293
+ const getters = {
294
+ closedRef,
295
+ snapshot,
296
+ queryParams: computed(() => parseQuery<Href>(snapshot.value.navigationState.href as Href)),
297
+ href: computed(() => snapshot.value.navigationState.href),
298
+ hasErrors: computed(() => Object.values(snapshot.value.outputs as Partial<Readonly<Outputs>>).some((v) => !v?.ok)),
299
+ };
300
+
301
+ return reactive(Object.assign(appModel, methods, getters));
302
+ }
303
+
304
+ export type BaseAppV2<
305
+ Args = unknown,
306
+ Outputs extends BlockOutputsBase = BlockOutputsBase,
307
+ UiState = unknown,
308
+ Href extends `/${string}` = `/${string}`,
309
+ > = ReturnType<typeof createAppV2<Args, Outputs, UiState, Href>>;
@@ -0,0 +1,144 @@
1
+ import { deepClone, delay, uniqueId } from '@milaboratories/helpers';
2
+ import {
3
+ type BlockOutputsBase,
4
+ type BlockState,
5
+ type NavigationState,
6
+ type AuthorMarker,
7
+ type ValueWithUTagAndAuthor,
8
+ wrapAsyncCallback,
9
+ type ResultOrError,
10
+ unwrapResult,
11
+ } from '@platforma-sdk/model';
12
+ import { compare, type Operation } from 'fast-json-patch';
13
+ import type { BlockApiV2, ValueWithUTag } from '@platforma-sdk/model';
14
+
15
+ export class BlockStateMock<
16
+ Args = unknown,
17
+ Outputs extends BlockOutputsBase = BlockOutputsBase,
18
+ UiState = unknown,
19
+ Href extends `/${string}` = `/${string}`,
20
+ > {
21
+ constructor(
22
+ public args: Args,
23
+ public outputs: Outputs,
24
+ public ui: UiState,
25
+ public href: Href,
26
+ public author: AuthorMarker = { authorId: 'test', localVersion: 0 },
27
+ public uTag: string = uniqueId(),
28
+ ) {}
29
+
30
+ setState(_state: Partial<BlockStateMock<Args, Outputs, UiState, Href>>) {
31
+ const state = deepClone(_state);
32
+ this.args = state.args ?? this.args;
33
+ this.outputs = state.outputs ?? this.outputs;
34
+ this.ui = state.ui ?? this.ui;
35
+ this.href = state.href ?? this.href;
36
+ this.author = state.author ?? this.author;
37
+ console.log('set author', state.author);
38
+ this.uTag = uniqueId();
39
+ }
40
+ }
41
+
42
+ export abstract class BlockMock<
43
+ Args = unknown,
44
+ Outputs extends BlockOutputsBase = BlockOutputsBase,
45
+ UiState = unknown,
46
+ Href extends `/${string}` = `/${string}`,
47
+ > implements BlockApiV2<Args, Outputs, UiState, Href> {
48
+ #previousState: {
49
+ uTag: string;
50
+ value: BlockState<Args, Outputs, UiState, Href>;
51
+ } | undefined;
52
+
53
+ constructor(
54
+ public state: BlockStateMock<Args, Outputs, UiState, Href>,
55
+ ) {
56
+ this.loadBlockState().then(unwrapResult).then(({ uTag, value }) => {
57
+ this.#previousState = { value, uTag };
58
+ });
59
+ }
60
+
61
+ get uTag(): string {
62
+ return this.state.uTag;
63
+ }
64
+
65
+ async setBlockArgs(args: Args, author?: AuthorMarker) {
66
+ return wrapAsyncCallback(() => {
67
+ this.state.setState({ args, author });
68
+ return this.doUpdate();
69
+ });
70
+ }
71
+
72
+ async setBlockUiState(ui: UiState, author?: AuthorMarker) {
73
+ return wrapAsyncCallback(async () => {
74
+ this.state.setState({ ui, author });
75
+ return this.doUpdate();
76
+ });
77
+ }
78
+
79
+ async setBlockArgsAndUiState(args: Args, ui: UiState, author?: AuthorMarker) {
80
+ return wrapAsyncCallback(async () => {
81
+ await delay(0);
82
+ if (typeof ui === 'object' && ui !== null && 'delay' in ui && ui.delay && typeof ui.delay === 'number') {
83
+ console.log('DELAY', ui.delay);
84
+ await delay(ui.delay);
85
+ }
86
+ this.state.setState({ args, ui, author });
87
+ return this.doUpdate();
88
+ });
89
+ }
90
+
91
+ async setNavigationState(navigationState: NavigationState<Href>) {
92
+ return wrapAsyncCallback(() => {
93
+ this.state.setState({ href: navigationState.href });
94
+ return this.doUpdate();
95
+ });
96
+ }
97
+
98
+ async loadBlockState(): Promise<ResultOrError<ValueWithUTag<BlockState<Args, Outputs, UiState, Href>>>> {
99
+ return wrapAsyncCallback(async () => {
100
+ return deepClone({
101
+ value: {
102
+ args: this.state.args,
103
+ ui: this.state.ui,
104
+ outputs: this.state.outputs,
105
+ navigationState: {
106
+ href: this.state.href,
107
+ },
108
+ author: this.state.author,
109
+ },
110
+ uTag: this.state.uTag,
111
+ });
112
+ });
113
+ }
114
+
115
+ async getPatches(uTag: string): Promise<ResultOrError<ValueWithUTagAndAuthor<Operation[]>>> {
116
+ return wrapAsyncCallback(async () => {
117
+ while (uTag === this.state.uTag) {
118
+ await delay(0);
119
+ }
120
+
121
+ if (this.#previousState?.uTag !== uTag) {
122
+ console.log('uTag mismatch, resetting previous state');
123
+ this.#previousState = undefined;
124
+ }
125
+
126
+ const currentState = await this.loadBlockState().then(unwrapResult).then(({ uTag, value }) => ({ uTag, value }));
127
+ const patches = compare(this.#previousState?.value ?? {}, currentState.value);
128
+ this.#previousState = currentState;
129
+ return {
130
+ uTag: this.state.uTag,
131
+ value: patches,
132
+ author: this.state.author,
133
+ };
134
+ });
135
+ }
136
+
137
+ private async doUpdate() {
138
+ await this.process();
139
+ }
140
+
141
+ abstract process(): Promise<void>;
142
+ }
143
+
144
+ export type InferState<B> = B extends BlockMock<infer S> ? S : never;
@@ -0,0 +1,92 @@
1
+ import type {
2
+ ValueWithUTag,
3
+ ValueWithUTagAndAuthor,
4
+ BlockState,
5
+ PlatformaV2,
6
+ BlockOutputsBase,
7
+ ImportFileHandle,
8
+ FileLike,
9
+ ListFilesResult,
10
+ LocalImportFileHandle,
11
+ NavigationState,
12
+ OpenDialogOps,
13
+ OpenMultipleFilesResponse,
14
+ OpenSingleFileResponse,
15
+ StorageHandle,
16
+ ResultOrError,
17
+ AuthorMarker,
18
+ } from '@platforma-sdk/model';
19
+ import type { BlockMock } from './BlockMock';
20
+ import type { Operation } from 'fast-json-patch';
21
+ import { delay } from '@milaboratories/helpers';
22
+ import { getLsFilesResult } from './utils';
23
+
24
+ export function createMockApi<
25
+ Args,
26
+ Outputs extends BlockOutputsBase,
27
+ UiState = unknown,
28
+ Href extends `/${string}` = `/${string}`,
29
+ >(block: BlockMock<Args, Outputs, UiState, Href>): PlatformaV2<Args, Outputs, UiState, Href> {
30
+ return {
31
+ apiVersion: 2,
32
+ sdkInfo: {
33
+ sdkVersion: 'dev',
34
+ },
35
+ loadBlockState: async function (): Promise<ResultOrError<ValueWithUTag<BlockState<Args, Outputs, UiState, Href>>>> {
36
+ return block.loadBlockState();
37
+ },
38
+ getPatches: async function (uTag: string): Promise<ResultOrError<ValueWithUTagAndAuthor<Operation[]>>> {
39
+ return block.getPatches(uTag);
40
+ },
41
+ async setBlockArgs(value: Args, author?: AuthorMarker): Promise<ResultOrError<void>> {
42
+ return block.setBlockArgs(value, author);
43
+ },
44
+ async setBlockUiState(value: UiState, author?: AuthorMarker): Promise<ResultOrError<void>> {
45
+ return block.setBlockUiState(value, author);
46
+ },
47
+ async setBlockArgsAndUiState(args: Args, uiState: UiState, author?: AuthorMarker): Promise<ResultOrError<void>> {
48
+ return block.setBlockArgsAndUiState(args, uiState, author);
49
+ },
50
+ async setNavigationState(navigationState: NavigationState<Href>): Promise<ResultOrError<void>> {
51
+ return block.setNavigationState(navigationState);
52
+ },
53
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
+ blobDriver: undefined as any,
55
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
56
+ logDriver: undefined as any,
57
+
58
+ lsDriver: {
59
+ async getStorageList() {
60
+ return [
61
+ {
62
+ name: 'local',
63
+ handle: 'local://test',
64
+ initialFullPath: '/',
65
+ isInitialPathHome: false,
66
+ },
67
+ ];
68
+ },
69
+ async listFiles(_storage: StorageHandle, fullPath: string): Promise<ListFilesResult> {
70
+ await delay(10);
71
+ return getLsFilesResult(fullPath);
72
+ },
73
+ async getLocalFileContent(_file: LocalImportFileHandle): Promise<Uint8Array> {
74
+ return Uint8Array.of(0, 1);
75
+ },
76
+ async getLocalFileSize(_file: LocalImportFileHandle): Promise<number> {
77
+ return 3;
78
+ },
79
+ async showOpenMultipleFilesDialog(_ops: OpenDialogOps): Promise<OpenMultipleFilesResponse> {
80
+ return {};
81
+ },
82
+ async showOpenSingleFileDialog(_ops: OpenDialogOps): Promise<OpenSingleFileResponse> {
83
+ return {};
84
+ },
85
+ async fileToImportHandle(_file: FileLike): Promise<ImportFileHandle> {
86
+ return '' as ImportFileHandle;
87
+ },
88
+ },
89
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
90
+ pFrameDriver: undefined as any,
91
+ };
92
+ }