@dxos/plugin-sheet 0.6.12-main.f9d0246 → 0.6.12-staging.e11e696
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/{SheetContainer-VISF3VUB.mjs → SheetContainer-LG77O4RM.mjs} +11 -10
- package/dist/lib/browser/SheetContainer-LG77O4RM.mjs.map +7 -0
- package/dist/lib/browser/{chunk-ZLJ2GRE2.mjs → chunk-CHQAW4F4.mjs} +55 -33
- package/dist/lib/browser/chunk-CHQAW4F4.mjs.map +7 -0
- package/dist/lib/browser/{chunk-Z2XOOC2R.mjs → chunk-GSV5QNLD.mjs} +183 -159
- package/dist/lib/browser/chunk-GSV5QNLD.mjs.map +7 -0
- package/dist/lib/browser/graph-M4IQ76QX.mjs +33 -0
- package/dist/lib/browser/index.mjs +37 -15
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/{SheetContainer-2MEALQWW.cjs → SheetContainer-OZ7DHH4L.cjs} +18 -17
- package/dist/lib/node/SheetContainer-OZ7DHH4L.cjs.map +7 -0
- package/dist/lib/node/{chunk-6DQABRGJ.cjs → chunk-5FTFZL5W.cjs} +57 -35
- package/dist/lib/node/chunk-5FTFZL5W.cjs.map +7 -0
- package/dist/lib/node/{chunk-P5QYYEHQ.cjs → chunk-5XPK2V4A.cjs} +186 -158
- package/dist/lib/node/chunk-5XPK2V4A.cjs.map +7 -0
- package/dist/lib/node/graph-Q3N2X26H.cjs +55 -0
- package/dist/lib/node/graph-Q3N2X26H.cjs.map +7 -0
- package/dist/lib/node/index.cjs +38 -18
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/{SheetContainer-RPSUSXWS.mjs → SheetContainer-4XS2G25Z.mjs} +11 -10
- package/dist/lib/node-esm/SheetContainer-4XS2G25Z.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-4MM7THJW.mjs → chunk-5WPZCXNS.mjs} +183 -159
- package/dist/lib/node-esm/chunk-5WPZCXNS.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-5RLTCIE2.mjs → chunk-KK3XL37M.mjs} +55 -33
- package/dist/lib/node-esm/chunk-KK3XL37M.mjs.map +7 -0
- package/dist/lib/node-esm/graph-SMPUMOV2.mjs +34 -0
- package/dist/lib/node-esm/index.mjs +37 -15
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/SheetPlugin.d.ts.map +1 -1
- package/dist/types/src/components/CellEditor/CellEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/CellEditor/extension.d.ts.map +1 -1
- package/dist/types/src/components/Sheet/Sheet.d.ts.map +1 -1
- package/dist/types/src/components/Sheet/Sheet.stories.d.ts.map +1 -1
- package/dist/types/src/components/SheetContainer.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/extensions/compute.d.ts +2 -5
- package/dist/types/src/extensions/compute.d.ts.map +1 -1
- package/dist/types/src/extensions/compute.stories.d.ts.map +1 -1
- package/dist/types/src/graph/compute-graph-registry.d.ts +34 -0
- package/dist/types/src/graph/compute-graph-registry.d.ts.map +1 -0
- package/dist/types/src/graph/compute-graph.d.ts +13 -33
- package/dist/types/src/graph/compute-graph.d.ts.map +1 -1
- package/dist/types/src/graph/compute-node.d.ts.map +1 -1
- package/dist/types/src/graph/{async-function.d.ts → functions/async-function.d.ts} +6 -3
- package/dist/types/src/graph/functions/async-function.d.ts.map +1 -0
- package/dist/types/src/graph/functions/edge-function.d.ts +21 -0
- package/dist/types/src/graph/functions/edge-function.d.ts.map +1 -0
- package/dist/types/src/graph/functions/function-defs.d.ts.map +1 -0
- package/dist/types/src/graph/functions/index.d.ts +4 -0
- package/dist/types/src/graph/functions/index.d.ts.map +1 -0
- package/dist/types/src/graph/index.d.ts +2 -1
- package/dist/types/src/graph/index.d.ts.map +1 -1
- package/dist/types/src/graph/testing/index.d.ts +2 -1
- package/dist/types/src/graph/testing/index.d.ts.map +1 -1
- package/dist/types/src/graph/testing/test-builder.d.ts +15 -0
- package/dist/types/src/graph/testing/test-builder.d.ts.map +1 -0
- package/dist/types/src/graph/testing/test-plugin.d.ts +36 -0
- package/dist/types/src/graph/testing/test-plugin.d.ts.map +1 -0
- package/dist/types/src/hooks/useComputeGraph.d.ts.map +1 -1
- package/dist/types/src/model/sheet-model.d.ts.map +1 -1
- package/dist/types/src/model/sheet-model.test.d.ts +2 -0
- package/dist/types/src/model/sheet-model.test.d.ts.map +1 -0
- package/package.json +40 -39
- package/src/SheetPlugin.tsx +12 -10
- package/src/components/CellEditor/CellEditor.stories.tsx +1 -2
- package/src/components/CellEditor/extension.test.ts +0 -1
- package/src/components/CellEditor/extension.ts +4 -3
- package/src/components/Sheet/Sheet.stories.tsx +2 -2
- package/src/components/Sheet/Sheet.tsx +30 -14
- package/src/components/SheetContainer.tsx +11 -13
- package/src/extensions/compute.stories.tsx +9 -11
- package/src/extensions/compute.ts +66 -50
- package/src/graph/compute-graph-registry.ts +90 -0
- package/src/graph/compute-graph.stories.tsx +2 -2
- package/src/graph/compute-graph.test.ts +31 -71
- package/src/graph/compute-graph.ts +45 -116
- package/src/graph/compute-node.ts +1 -0
- package/src/graph/{async-function.ts → functions/async-function.ts} +10 -9
- package/src/graph/{edge-function.ts → functions/edge-function.ts} +13 -11
- package/src/graph/functions/index.ts +7 -0
- package/src/graph/hyperformula.test.ts +1 -2
- package/src/graph/index.ts +2 -1
- package/src/graph/testing/index.ts +2 -1
- package/src/graph/testing/test-builder.ts +54 -0
- package/src/graph/testing/{custom-function.ts → test-plugin.ts} +38 -12
- package/src/hooks/useComputeGraph.ts +8 -1
- package/src/model/sheet-model.test.ts +59 -0
- package/src/model/sheet-model.ts +4 -2
- package/dist/lib/browser/SheetContainer-VISF3VUB.mjs.map +0 -7
- package/dist/lib/browser/chunk-Z2XOOC2R.mjs.map +0 -7
- package/dist/lib/browser/chunk-ZLJ2GRE2.mjs.map +0 -7
- package/dist/lib/browser/graph-4XFKIHRL.mjs +0 -21
- package/dist/lib/node/SheetContainer-2MEALQWW.cjs.map +0 -7
- package/dist/lib/node/chunk-6DQABRGJ.cjs.map +0 -7
- package/dist/lib/node/chunk-P5QYYEHQ.cjs.map +0 -7
- package/dist/lib/node/graph-2LRDUXBZ.cjs +0 -43
- package/dist/lib/node/graph-2LRDUXBZ.cjs.map +0 -7
- package/dist/lib/node-esm/SheetContainer-RPSUSXWS.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-4MM7THJW.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-5RLTCIE2.mjs.map +0 -7
- package/dist/lib/node-esm/graph-WG5EKOMO.mjs +0 -22
- package/dist/types/src/graph/async-function.d.ts.map +0 -1
- package/dist/types/src/graph/edge-function.d.ts +0 -20
- package/dist/types/src/graph/edge-function.d.ts.map +0 -1
- package/dist/types/src/graph/function-defs.d.ts.map +0 -1
- package/dist/types/src/graph/testing/custom-function.d.ts +0 -23
- package/dist/types/src/graph/testing/custom-function.d.ts.map +0 -1
- /package/dist/lib/browser/{graph-4XFKIHRL.mjs.map → graph-M4IQ76QX.mjs.map} +0 -0
- /package/dist/lib/node-esm/{graph-WG5EKOMO.mjs.map → graph-SMPUMOV2.mjs.map} +0 -0
- /package/dist/types/src/graph/{function-defs.d.ts → functions/function-defs.d.ts} +0 -0
- /package/src/graph/{function-defs.ts → functions/function-defs.ts} +0 -0
|
@@ -2,116 +2,45 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type FunctionPluginDefinition } from 'hyperformula';
|
|
6
|
-
import { type ConfigParams } from 'hyperformula/typings/ConfigParams';
|
|
7
5
|
import { type Listeners } from 'hyperformula/typings/Emitter';
|
|
8
|
-
import { type FunctionTranslationsPackage } from 'hyperformula/typings/interpreter';
|
|
9
|
-
import defaultsDeep from 'lodash.defaultsdeep';
|
|
10
6
|
|
|
11
7
|
import { Event } from '@dxos/async';
|
|
12
|
-
import { type
|
|
8
|
+
import { type Space, Filter, fullyQualifiedId } from '@dxos/client/echo';
|
|
13
9
|
import { Resource } from '@dxos/context';
|
|
10
|
+
import { getTypename } from '@dxos/echo-schema';
|
|
14
11
|
import { invariant } from '@dxos/invariant';
|
|
15
12
|
import { PublicKey } from '@dxos/keys';
|
|
16
13
|
import { log } from '@dxos/log';
|
|
17
14
|
import { FunctionType } from '@dxos/plugin-script/types';
|
|
18
15
|
import { nonNullable } from '@dxos/util';
|
|
19
16
|
|
|
20
|
-
import { ExportedCellChange, HyperFormula } from '#hyperformula';
|
|
21
|
-
import { FunctionContext, type FunctionContextOptions } from './async-function';
|
|
17
|
+
import { ExportedCellChange, type HyperFormula } from '#hyperformula';
|
|
22
18
|
import { ComputeNode } from './compute-node';
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
import {
|
|
20
|
+
defaultFunctions,
|
|
21
|
+
FunctionContext,
|
|
22
|
+
type FunctionContextOptions,
|
|
23
|
+
type FunctionDefinition,
|
|
24
|
+
EDGE_FUNCTION_NAME,
|
|
25
|
+
} from './functions';
|
|
29
26
|
|
|
30
27
|
// TODO(wittjosiah): Factor out.
|
|
31
|
-
const OBJECT_ID_LENGTH = 60; // 33 (space id) +
|
|
32
|
-
|
|
33
|
-
// TODO(burdon): Change to "DX".
|
|
34
|
-
const CUSTOM_FUNCTION = 'ECHO';
|
|
28
|
+
const OBJECT_ID_LENGTH = 60; // 33 (space id) + 1 (separator) + 26 (object id).
|
|
35
29
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
translations: FunctionTranslationsPackage;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export type ComputeGraphOptions = {
|
|
42
|
-
plugins?: ComputeGraphPlugin[];
|
|
43
|
-
} & Partial<FunctionContextOptions> &
|
|
44
|
-
Partial<ConfigParams>;
|
|
45
|
-
|
|
46
|
-
export const defaultOptions: ComputeGraphOptions = {
|
|
47
|
-
licenseKey: 'gpl-v3',
|
|
48
|
-
};
|
|
30
|
+
// TODO(burdon): Factory.
|
|
31
|
+
// export type ComputeNodeGenerator = <T>(obj: T) => ComputeNode;
|
|
49
32
|
|
|
50
|
-
|
|
51
|
-
{
|
|
52
|
-
plugin: EdgeFunctionPlugin,
|
|
53
|
-
translations: EdgeFunctionPluginTranslations,
|
|
54
|
-
},
|
|
55
|
-
];
|
|
33
|
+
type ObjectRef = { type: string; id: string };
|
|
56
34
|
|
|
57
35
|
/**
|
|
58
36
|
* Marker for sheets that are managed by an ECHO object.
|
|
37
|
+
* Sheet ID: `dxos.org/type/SheetType@1234`
|
|
59
38
|
*/
|
|
60
|
-
const
|
|
61
|
-
export const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Manages a collection of ComputeGraph instances for each space.
|
|
67
|
-
*
|
|
68
|
-
* [ComputePlugin] => [ComputeGraphRegistry] => [ComputeGraph(Space)] => [ComputeNode(Object)]
|
|
69
|
-
*
|
|
70
|
-
* NOTE: The ComputeGraphRegistry manages the hierarchy of resources via its root Context.
|
|
71
|
-
*/
|
|
72
|
-
// TODO(burdon): Move graph into separate plugin; isolate HF deps.
|
|
73
|
-
export class ComputeGraphRegistry extends Resource {
|
|
74
|
-
private readonly _graphs = new Map<SpaceId, ComputeGraph>();
|
|
75
|
-
|
|
76
|
-
private readonly _options: ComputeGraphOptions;
|
|
77
|
-
|
|
78
|
-
constructor(options: ComputeGraphOptions = { plugins: defaultPlugins }) {
|
|
79
|
-
super();
|
|
80
|
-
this._options = defaultsDeep({}, options, defaultOptions);
|
|
81
|
-
this._options.plugins?.forEach(({ plugin, translations }) => {
|
|
82
|
-
HyperFormula.registerFunctionPlugin(plugin, translations);
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
getGraph(spaceId: SpaceId): ComputeGraph | undefined {
|
|
87
|
-
return this._graphs.get(spaceId);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async getOrCreateGraph(space: Space): Promise<ComputeGraph> {
|
|
91
|
-
let graph = this.getGraph(space.id);
|
|
92
|
-
if (!graph) {
|
|
93
|
-
log.info('create graph', { space: space.id });
|
|
94
|
-
graph = await this.createGraph(space);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return graph;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async createGraph(space: Space): Promise<ComputeGraph> {
|
|
101
|
-
invariant(!this._graphs.has(space.id), `ComputeGraph already exists for space: ${space.id}`);
|
|
102
|
-
const hf = HyperFormula.buildEmpty(this._options);
|
|
103
|
-
const graph = new ComputeGraph(hf, space, this._options);
|
|
104
|
-
this._graphs.set(space.id, graph);
|
|
105
|
-
await graph.open();
|
|
106
|
-
return graph;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
protected override async _close() {
|
|
110
|
-
for (const graph of this._graphs.values()) {
|
|
111
|
-
await graph.close();
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
39
|
+
export const createSheetName = ({ type, id }: ObjectRef) => `${type}@${id}`;
|
|
40
|
+
export const parseSheetName = (name: string): Partial<ObjectRef> => {
|
|
41
|
+
const [type, id] = name.split('@');
|
|
42
|
+
return id ? { type, id } : { id: type };
|
|
43
|
+
};
|
|
115
44
|
|
|
116
45
|
export type ComputeGraphEvent = 'functionsUpdated';
|
|
117
46
|
|
|
@@ -128,7 +57,7 @@ export class ComputeGraph extends Resource {
|
|
|
128
57
|
private readonly _nodes = new Map<number, ComputeNode>();
|
|
129
58
|
|
|
130
59
|
// Cached function objects.
|
|
131
|
-
private
|
|
60
|
+
private _remoteFunctions: FunctionType[] = [];
|
|
132
61
|
|
|
133
62
|
public readonly update = new Event<{ type: ComputeGraphEvent }>();
|
|
134
63
|
|
|
@@ -163,11 +92,6 @@ export class ComputeGraph extends Resource {
|
|
|
163
92
|
return this._hf;
|
|
164
93
|
}
|
|
165
94
|
|
|
166
|
-
// refresh() {
|
|
167
|
-
// log('refresh', { id: this.id });
|
|
168
|
-
// this.update.emit();
|
|
169
|
-
// }
|
|
170
|
-
|
|
171
95
|
getFunctions(
|
|
172
96
|
{ standard, echo }: { standard?: boolean; echo?: boolean } = { standard: true, echo: true },
|
|
173
97
|
): FunctionDefinition[] {
|
|
@@ -177,7 +101,7 @@ export class ComputeGraph extends Resource {
|
|
|
177
101
|
.getRegisteredFunctionNames()
|
|
178
102
|
.map((name) => defaultFunctions.find((fn) => fn.name === name) ?? { name })
|
|
179
103
|
: []),
|
|
180
|
-
...(echo ? this.
|
|
104
|
+
...(echo ? this._remoteFunctions.map((fn) => ({ name: fn.binding! })) : []),
|
|
181
105
|
];
|
|
182
106
|
}
|
|
183
107
|
|
|
@@ -189,19 +113,17 @@ export class ComputeGraph extends Resource {
|
|
|
189
113
|
// This would enable on-the-fly instantiation of new models when then are referenced.
|
|
190
114
|
// E.g., Cross-object reference would be stored as "ObjectId!A1"
|
|
191
115
|
// The graph would then load the object and create a ComputeNode (model) of the appropriate type.
|
|
192
|
-
|
|
116
|
+
getOrCreateNode(name: string): ComputeNode {
|
|
193
117
|
invariant(name.length);
|
|
194
118
|
if (!this._hf.doesSheetExist(name)) {
|
|
195
|
-
log.info('created node', { space: this._space?.id, name });
|
|
119
|
+
log.info('created node', { space: this._space?.id, sheet: name });
|
|
196
120
|
this._hf.addSheet(name);
|
|
197
|
-
// this.update.emit();
|
|
198
121
|
}
|
|
199
122
|
|
|
200
123
|
const sheetId = this._hf.getSheetId(name);
|
|
201
124
|
invariant(sheetId !== undefined);
|
|
202
125
|
|
|
203
126
|
const node = new ComputeNode(this, sheetId);
|
|
204
|
-
await node.open();
|
|
205
127
|
this._nodes.set(sheetId, node);
|
|
206
128
|
return node;
|
|
207
129
|
}
|
|
@@ -213,22 +135,27 @@ export class ComputeGraph extends Resource {
|
|
|
213
135
|
mapFormulaToNative(formula: string): string {
|
|
214
136
|
return (
|
|
215
137
|
formula
|
|
216
|
-
//
|
|
138
|
+
//
|
|
139
|
+
// Map cross-sheet references by name onto sheet stored by ECHO object/model.
|
|
140
|
+
// Example: "Test Sheet"!A0 => "dxos.org/type/SheetType@1234"!A0
|
|
217
141
|
// https://hyperformula.handsontable.com/guide/cell-references.html#cell-references
|
|
142
|
+
//
|
|
218
143
|
.replace(/['"]?([ \w]+)['"]?!/, (_match, name) => {
|
|
219
144
|
if (name) {
|
|
220
|
-
// TODO(burdon):
|
|
145
|
+
// TODO(burdon): Cache map.
|
|
221
146
|
const objects = this._hf
|
|
222
147
|
.getSheetNames()
|
|
223
148
|
.map((name) => {
|
|
224
|
-
const id =
|
|
225
|
-
return id ? this._space?.db.getObjectById(id) : undefined;
|
|
149
|
+
const { type, id } = parseSheetName(name);
|
|
150
|
+
return type && id ? this._space?.db.getObjectById(id) : undefined;
|
|
226
151
|
})
|
|
227
152
|
.filter(nonNullable);
|
|
228
153
|
|
|
229
154
|
for (const obj of objects) {
|
|
230
|
-
if (obj.name === name
|
|
231
|
-
|
|
155
|
+
if (obj.name === name) {
|
|
156
|
+
const type = getTypename(obj)!;
|
|
157
|
+
// NOTE: Names must be single quoted.
|
|
158
|
+
return `'${createSheetName({ type, id: obj.id })}'!`;
|
|
232
159
|
}
|
|
233
160
|
}
|
|
234
161
|
}
|
|
@@ -236,17 +163,19 @@ export class ComputeGraph extends Resource {
|
|
|
236
163
|
return `${name}!`;
|
|
237
164
|
})
|
|
238
165
|
|
|
239
|
-
//
|
|
166
|
+
//
|
|
167
|
+
// Map remote function references (i.e., to remote DX function invocation).
|
|
168
|
+
//
|
|
240
169
|
.replace(/(\w+)\((.*)\)/g, (match, binding, args) => {
|
|
241
|
-
const fn = this.
|
|
170
|
+
const fn = this._remoteFunctions.find((fn) => fn.binding === binding);
|
|
242
171
|
if (!fn) {
|
|
243
172
|
return match;
|
|
244
173
|
}
|
|
245
174
|
|
|
246
175
|
if (args.trim() === '') {
|
|
247
|
-
return `${
|
|
176
|
+
return `${EDGE_FUNCTION_NAME}("${binding}")`;
|
|
248
177
|
} else {
|
|
249
|
-
return `${
|
|
178
|
+
return `${EDGE_FUNCTION_NAME}("${binding}", ${args})`;
|
|
250
179
|
}
|
|
251
180
|
})
|
|
252
181
|
);
|
|
@@ -258,11 +187,11 @@ export class ComputeGraph extends Resource {
|
|
|
258
187
|
*/
|
|
259
188
|
mapFunctionBindingToId(formula: string) {
|
|
260
189
|
return formula.replace(/(\w+)\((.*)\)/g, (match, binding, args) => {
|
|
261
|
-
if (binding ===
|
|
190
|
+
if (binding === EDGE_FUNCTION_NAME || defaultFunctions.find((fn) => fn.name === binding)) {
|
|
262
191
|
return match;
|
|
263
192
|
}
|
|
264
193
|
|
|
265
|
-
const fn = this.
|
|
194
|
+
const fn = this._remoteFunctions.find((fn) => fn.binding === binding);
|
|
266
195
|
if (fn) {
|
|
267
196
|
const id = fullyQualifiedId(fn);
|
|
268
197
|
return `${id}(${args})`;
|
|
@@ -283,7 +212,7 @@ export class ComputeGraph extends Resource {
|
|
|
283
212
|
return match;
|
|
284
213
|
}
|
|
285
214
|
|
|
286
|
-
const fn = this.
|
|
215
|
+
const fn = this._remoteFunctions.find((fn) => fullyQualifiedId(fn) === id);
|
|
287
216
|
if (fn?.binding) {
|
|
288
217
|
return `${fn.binding}(${args})`;
|
|
289
218
|
} else {
|
|
@@ -297,7 +226,7 @@ export class ComputeGraph extends Resource {
|
|
|
297
226
|
// Subscribe to remote function definitions.
|
|
298
227
|
const query = this._space.db.query(Filter.schema(FunctionType));
|
|
299
228
|
const unsubscribe = query.subscribe(({ objects }) => {
|
|
300
|
-
this.
|
|
229
|
+
this._remoteFunctions = objects.filter(({ binding }) => binding);
|
|
301
230
|
this.update.emit({ type: 'functionsUpdated' });
|
|
302
231
|
});
|
|
303
232
|
|
|
@@ -55,6 +55,7 @@ export class ComputeNode extends Resource {
|
|
|
55
55
|
this._graph.hf.setCellContents({ sheet: this.sheetId, row: cell.row, col: cell.col }, [[mappedValue]]);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// TODO(burdon): Load data into sheet.
|
|
58
59
|
protected override async _open() {
|
|
59
60
|
// const unsubscribe = this._graph.update.on(this.update.emit);
|
|
60
61
|
// this._ctx.onDispose(unsubscribe);
|
|
@@ -14,13 +14,12 @@ import { log } from '@dxos/log';
|
|
|
14
14
|
|
|
15
15
|
import { CellError, ErrorType, EmptyValue, FunctionPlugin, type HyperFormula } from '#hyperformula';
|
|
16
16
|
|
|
17
|
-
// TODO(burdon): API gateways
|
|
17
|
+
// TODO(burdon): Create API gateways:
|
|
18
18
|
// https://publicapis.io
|
|
19
19
|
// https://api-ninjas.com/api/cryptoprice
|
|
20
20
|
// https://developers.google.com/apis-explorer
|
|
21
21
|
// https://publicapis.io/coin-desk-api
|
|
22
22
|
|
|
23
|
-
// TODO(burdon): Create wrapper.
|
|
24
23
|
export type AsyncFunction = (...args: any) => Promise<InterpreterValue>;
|
|
25
24
|
|
|
26
25
|
export type FunctionUpdateEvent = {
|
|
@@ -34,14 +33,14 @@ export type FunctionOptions = {
|
|
|
34
33
|
|
|
35
34
|
export type FunctionContextOptions = {
|
|
36
35
|
defaultTtl: number;
|
|
37
|
-
|
|
36
|
+
debounceDelay: number;
|
|
38
37
|
remoteFunctionUrl: string;
|
|
39
38
|
onUpdate?: (update: FunctionUpdateEvent) => void;
|
|
40
39
|
};
|
|
41
40
|
|
|
42
41
|
export const defaultFunctionContextOptions: FunctionContextOptions = {
|
|
43
42
|
defaultTtl: 5_000,
|
|
44
|
-
|
|
43
|
+
debounceDelay: 200,
|
|
45
44
|
remoteFunctionUrl: 'https://edge.dxos.workers.dev/functions', // TODO(burdon): Config.
|
|
46
45
|
};
|
|
47
46
|
|
|
@@ -84,11 +83,10 @@ export class FunctionContext {
|
|
|
84
83
|
) {
|
|
85
84
|
this._options = defaultsDeep(_options ?? {}, defaultFunctionContextOptions);
|
|
86
85
|
this._onUpdate = debounce((update) => {
|
|
87
|
-
// TODO(burdon): Better way to trigger recalculation?
|
|
88
|
-
// NOTE: rebuildAndRecalculate resets the undo history.
|
|
86
|
+
// TODO(burdon): Better way to trigger recalculation? (NOTE: rebuildAndRecalculate resets the undo history.)
|
|
89
87
|
this._hf.resumeEvaluation();
|
|
90
88
|
this._options.onUpdate?.(update);
|
|
91
|
-
}, this._options.
|
|
89
|
+
}, this._options.debounceDelay);
|
|
92
90
|
}
|
|
93
91
|
|
|
94
92
|
get space() {
|
|
@@ -162,12 +160,15 @@ export class FunctionContext {
|
|
|
162
160
|
/**
|
|
163
161
|
* Base class for async functions.
|
|
164
162
|
*/
|
|
165
|
-
export class
|
|
163
|
+
export class AsyncFunctionPlugin extends FunctionPlugin {
|
|
166
164
|
get context() {
|
|
167
165
|
return this.config.context as FunctionContext;
|
|
168
166
|
}
|
|
169
167
|
|
|
170
|
-
|
|
168
|
+
/**
|
|
169
|
+
* Immediately returns cached value then runs the async function.
|
|
170
|
+
*/
|
|
171
|
+
protected runAsyncFunction(ast: ProcedureAst, state: InterpreterState, cb: AsyncFunction, options?: FunctionOptions) {
|
|
171
172
|
const { procedureName } = ast;
|
|
172
173
|
const metadata = this.metadata(procedureName);
|
|
173
174
|
return this.runFunction(ast.args, state, metadata, (...args: any) => {
|
|
@@ -13,17 +13,19 @@ import { FunctionType } from '@dxos/plugin-script/types';
|
|
|
13
13
|
import { nonNullable } from '@dxos/util';
|
|
14
14
|
|
|
15
15
|
import { CellError, ErrorType, FunctionArgumentType } from '#hyperformula';
|
|
16
|
-
import { type AsyncFunction,
|
|
16
|
+
import { type AsyncFunction, AsyncFunctionPlugin } from './async-function';
|
|
17
17
|
|
|
18
|
-
const
|
|
18
|
+
export const EDGE_FUNCTION_NAME = 'DX';
|
|
19
|
+
|
|
20
|
+
const FUNCTION_TTL = 10_000;
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
|
-
* A hyperformula function plugin for calling EDGE functions.
|
|
23
|
+
* A hyperformula function plugin for calling remote (EDGE) functions.
|
|
22
24
|
*
|
|
23
25
|
* https://hyperformula.handsontable.com/guide/custom-functions.html#add-a-simple-custom-function
|
|
24
26
|
*/
|
|
25
|
-
export class EdgeFunctionPlugin extends
|
|
26
|
-
|
|
27
|
+
export class EdgeFunctionPlugin extends AsyncFunctionPlugin {
|
|
28
|
+
dx(ast: ProcedureAst, state: InterpreterState) {
|
|
27
29
|
const handler =
|
|
28
30
|
(subscribe = false): AsyncFunction =>
|
|
29
31
|
async (binding: string, ...args: any) => {
|
|
@@ -47,7 +49,7 @@ export class EdgeFunctionPlugin extends FunctionPluginAsync {
|
|
|
47
49
|
|
|
48
50
|
// TODO(wittjosiah): `ttl` should be 0 to force a recalculation when a new version is deployed.
|
|
49
51
|
// This needs a ttl to prevent a binding change from causing the function not to be found.
|
|
50
|
-
this.runAsyncFunction(ast, state, handler(false), { ttl:
|
|
52
|
+
this.runAsyncFunction(ast, state, handler(false), { ttl: FUNCTION_TTL });
|
|
51
53
|
});
|
|
52
54
|
|
|
53
55
|
this.context.createSubscription(ast.procedureName, unsubscribe);
|
|
@@ -63,13 +65,13 @@ export class EdgeFunctionPlugin extends FunctionPluginAsync {
|
|
|
63
65
|
return await result.text();
|
|
64
66
|
};
|
|
65
67
|
|
|
66
|
-
return this.runAsyncFunction(ast, state, handler(true), { ttl:
|
|
68
|
+
return this.runAsyncFunction(ast, state, handler(true), { ttl: FUNCTION_TTL });
|
|
67
69
|
}
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
EdgeFunctionPlugin.implementedFunctions = {
|
|
71
|
-
|
|
72
|
-
method: '
|
|
73
|
+
[EDGE_FUNCTION_NAME]: {
|
|
74
|
+
method: 'dx',
|
|
73
75
|
parameters: [
|
|
74
76
|
// Binding
|
|
75
77
|
{ argumentType: FunctionArgumentType.STRING },
|
|
@@ -90,9 +92,9 @@ EdgeFunctionPlugin.implementedFunctions = {
|
|
|
90
92
|
|
|
91
93
|
export const EdgeFunctionPluginTranslations = {
|
|
92
94
|
enGB: {
|
|
93
|
-
|
|
95
|
+
[EDGE_FUNCTION_NAME]: 'Remote function',
|
|
94
96
|
},
|
|
95
97
|
enUS: {
|
|
96
|
-
|
|
98
|
+
[EDGE_FUNCTION_NAME]: 'Remote function',
|
|
97
99
|
},
|
|
98
100
|
};
|
|
@@ -7,8 +7,7 @@ import { describe, test, expect } from 'vitest';
|
|
|
7
7
|
import { HyperFormula } from '#hyperformula';
|
|
8
8
|
|
|
9
9
|
describe('hyperformula', () => {
|
|
10
|
-
test('sanity', async () => {
|
|
11
|
-
// TODO(burdon): Throws "Cannot convert undefined or null to object" in vitest (without browser).
|
|
10
|
+
test('sanity test', async () => {
|
|
12
11
|
const hf = HyperFormula.buildEmpty({ licenseKey: 'gpl-v3' });
|
|
13
12
|
expect(hf).to.exist;
|
|
14
13
|
});
|
package/src/graph/index.ts
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Client, type ClientOptions } from '@dxos/client';
|
|
6
|
+
import { type Context, Resource } from '@dxos/context';
|
|
7
|
+
import { invariant } from '@dxos/invariant';
|
|
8
|
+
|
|
9
|
+
import { type ComputeGraphOptions, ComputeGraphRegistry } from '../compute-graph-registry';
|
|
10
|
+
|
|
11
|
+
export type TestBuilderOptions = ClientOptions & ComputeGraphOptions;
|
|
12
|
+
|
|
13
|
+
// TODO(burdon): Reconcile with @dxos/client/testing.
|
|
14
|
+
export class TestBuilder extends Resource {
|
|
15
|
+
private _client?: Client;
|
|
16
|
+
private _registry?: ComputeGraphRegistry;
|
|
17
|
+
|
|
18
|
+
constructor(private readonly _options: TestBuilderOptions = {}) {
|
|
19
|
+
super();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get ctx(): Context {
|
|
23
|
+
return this._ctx;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get client(): Client {
|
|
27
|
+
invariant(this._client);
|
|
28
|
+
return this._client;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get registry(): ComputeGraphRegistry {
|
|
32
|
+
invariant(this._registry);
|
|
33
|
+
return this._registry;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
override async _open() {
|
|
37
|
+
const client = new Client(this._options);
|
|
38
|
+
await client.initialize();
|
|
39
|
+
await client.halo.createIdentity();
|
|
40
|
+
this._client = client;
|
|
41
|
+
this._ctx.onDispose(async () => {
|
|
42
|
+
await client.destroy();
|
|
43
|
+
this._client = undefined;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const registry = new ComputeGraphRegistry(this._options);
|
|
47
|
+
await registry.open();
|
|
48
|
+
this._registry = registry;
|
|
49
|
+
this._ctx.onDispose(async () => {
|
|
50
|
+
await registry.close();
|
|
51
|
+
this._registry = undefined;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -8,15 +8,30 @@ import { type ProcedureAst } from 'hyperformula/typings/parser';
|
|
|
8
8
|
import { getDeep } from '@dxos/util';
|
|
9
9
|
|
|
10
10
|
import { FunctionArgumentType } from '#hyperformula';
|
|
11
|
-
import { type
|
|
12
|
-
import { type
|
|
11
|
+
import { type ComputeGraphPlugin } from '../compute-graph-registry';
|
|
12
|
+
import { type AsyncFunction, AsyncFunctionPlugin } from '../functions';
|
|
13
13
|
import { parseNumberString } from '../util';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
+
* Testing functions run locally (not run via EDGE).
|
|
16
17
|
* https://hyperformula.handsontable.com/guide/custom-functions.html#add-a-simple-custom-function
|
|
17
18
|
*/
|
|
18
|
-
export class
|
|
19
|
+
export class TestPlugin extends AsyncFunctionPlugin {
|
|
20
|
+
/**
|
|
21
|
+
* Simple local function returns input value.
|
|
22
|
+
*/
|
|
19
23
|
test(ast: ProcedureAst, state: InterpreterState) {
|
|
24
|
+
const handler: AsyncFunction = async (_value) => {
|
|
25
|
+
return _value;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return this.runAsyncFunction(ast, state, handler);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Simple local function returns random number.
|
|
33
|
+
*/
|
|
34
|
+
random(ast: ProcedureAst, state: InterpreterState) {
|
|
20
35
|
const handler: AsyncFunction = async () => {
|
|
21
36
|
return Math.random();
|
|
22
37
|
};
|
|
@@ -24,6 +39,9 @@ export class CustomPlugin extends FunctionPluginAsync {
|
|
|
24
39
|
return this.runAsyncFunction(ast, state, handler);
|
|
25
40
|
}
|
|
26
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Async HTTP function.
|
|
44
|
+
*/
|
|
27
45
|
crypto(ast: ProcedureAst, state: InterpreterState) {
|
|
28
46
|
const handler: AsyncFunction = async (_currency) => {
|
|
29
47
|
const currency = (_currency || 'USD').toUpperCase();
|
|
@@ -41,9 +59,15 @@ export class CustomPlugin extends FunctionPluginAsync {
|
|
|
41
59
|
}
|
|
42
60
|
}
|
|
43
61
|
|
|
44
|
-
|
|
62
|
+
TestPlugin.implementedFunctions = {
|
|
45
63
|
TEST: {
|
|
46
64
|
method: 'test',
|
|
65
|
+
parameters: [{ argumentType: FunctionArgumentType.NUMBER, optionalArg: false }],
|
|
66
|
+
isVolatile: true,
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
RANDOM: {
|
|
70
|
+
method: 'random',
|
|
47
71
|
parameters: [],
|
|
48
72
|
isVolatile: true,
|
|
49
73
|
},
|
|
@@ -55,20 +79,22 @@ CustomPlugin.implementedFunctions = {
|
|
|
55
79
|
},
|
|
56
80
|
};
|
|
57
81
|
|
|
58
|
-
export const
|
|
82
|
+
export const TestPluginTranslations = {
|
|
59
83
|
enGB: {
|
|
60
|
-
TEST: '
|
|
61
|
-
|
|
84
|
+
TEST: 'Returns input value',
|
|
85
|
+
RANDOM: 'Random number',
|
|
86
|
+
CRYPTO: 'Crypto token value',
|
|
62
87
|
},
|
|
63
88
|
enUS: {
|
|
64
|
-
TEST: '
|
|
65
|
-
|
|
89
|
+
TEST: 'Returns input value',
|
|
90
|
+
RANDOM: 'Random number',
|
|
91
|
+
CRYPTO: 'Crypto token value',
|
|
66
92
|
},
|
|
67
93
|
};
|
|
68
94
|
|
|
69
|
-
export const
|
|
95
|
+
export const testFunctionPlugins: ComputeGraphPlugin[] = [
|
|
70
96
|
{
|
|
71
|
-
plugin:
|
|
72
|
-
translations:
|
|
97
|
+
plugin: TestPlugin,
|
|
98
|
+
translations: TestPluginTranslations,
|
|
73
99
|
},
|
|
74
100
|
];
|
|
@@ -16,6 +16,13 @@ import { type ComputeGraph } from '../graph';
|
|
|
16
16
|
*/
|
|
17
17
|
export const useComputeGraph = (space?: Space): ComputeGraph | undefined => {
|
|
18
18
|
const { registry } = useContext(ComputeGraphContext) ?? raise(new Error('Missing ComputeGraphContext'));
|
|
19
|
-
const [graph] = useAsyncState(async () =>
|
|
19
|
+
const [graph] = useAsyncState(async () => {
|
|
20
|
+
if (space) {
|
|
21
|
+
const graph = registry.getOrCreateGraph(space);
|
|
22
|
+
await graph.open();
|
|
23
|
+
return graph;
|
|
24
|
+
}
|
|
25
|
+
}, [space, registry]);
|
|
26
|
+
|
|
20
27
|
return graph;
|
|
21
28
|
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { Trigger } from '@dxos/async';
|
|
8
|
+
import { FunctionType } from '@dxos/plugin-script/types';
|
|
9
|
+
|
|
10
|
+
import { SheetModel } from './sheet-model';
|
|
11
|
+
import { createSheet, addressFromA1Notation } from '../defs';
|
|
12
|
+
import { TestBuilder, testFunctionPlugins } from '../graph/testing';
|
|
13
|
+
import { type CellScalarValue } from '../types';
|
|
14
|
+
|
|
15
|
+
// TODO(burdon): GPT("prompt", inputs); e.g., with large text cells.
|
|
16
|
+
|
|
17
|
+
describe('SheetModel', () => {
|
|
18
|
+
let testBuilder: TestBuilder;
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
testBuilder = new TestBuilder({ types: [FunctionType], plugins: testFunctionPlugins });
|
|
21
|
+
await testBuilder.open();
|
|
22
|
+
});
|
|
23
|
+
afterEach(async () => {
|
|
24
|
+
await testBuilder.close();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('async function', async () => {
|
|
28
|
+
const space = await testBuilder.client.spaces.create();
|
|
29
|
+
const graph = testBuilder.registry.createGraph(space);
|
|
30
|
+
await graph.open();
|
|
31
|
+
|
|
32
|
+
// TODO(burdon): Create via factory.
|
|
33
|
+
const sheet = createSheet({ rows: 5, columns: 5 });
|
|
34
|
+
const model = new SheetModel(graph, sheet);
|
|
35
|
+
await model.open();
|
|
36
|
+
testBuilder.ctx.onDispose(() => model.close());
|
|
37
|
+
|
|
38
|
+
// Trigger waits for function invocation.
|
|
39
|
+
const trigger = new Trigger<CellScalarValue>();
|
|
40
|
+
model.setValue(addressFromA1Notation('A1'), '=TEST(100)');
|
|
41
|
+
model.update.once((update) => {
|
|
42
|
+
const { type } = update;
|
|
43
|
+
if (type === 'valuesUpdated') {
|
|
44
|
+
const value = model.getValue(addressFromA1Notation('A1'));
|
|
45
|
+
trigger.wake(value);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Initial value will be null.
|
|
50
|
+
const v1 = model.getValue(addressFromA1Notation('A1'));
|
|
51
|
+
expect(v1).to.be.null;
|
|
52
|
+
expect(graph.context.info.invocations.TEST).not.to.exist;
|
|
53
|
+
|
|
54
|
+
// Wait until async update triggered.
|
|
55
|
+
const v2 = await trigger.wait();
|
|
56
|
+
expect(v2).to.eq(100);
|
|
57
|
+
expect(graph.context.info.invocations.TEST).to.eq(1);
|
|
58
|
+
});
|
|
59
|
+
});
|