@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.
Files changed (114) hide show
  1. package/dist/lib/browser/{SheetContainer-VISF3VUB.mjs → SheetContainer-LG77O4RM.mjs} +11 -10
  2. package/dist/lib/browser/SheetContainer-LG77O4RM.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-ZLJ2GRE2.mjs → chunk-CHQAW4F4.mjs} +55 -33
  4. package/dist/lib/browser/chunk-CHQAW4F4.mjs.map +7 -0
  5. package/dist/lib/browser/{chunk-Z2XOOC2R.mjs → chunk-GSV5QNLD.mjs} +183 -159
  6. package/dist/lib/browser/chunk-GSV5QNLD.mjs.map +7 -0
  7. package/dist/lib/browser/graph-M4IQ76QX.mjs +33 -0
  8. package/dist/lib/browser/index.mjs +37 -15
  9. package/dist/lib/browser/index.mjs.map +4 -4
  10. package/dist/lib/browser/meta.json +1 -1
  11. package/dist/lib/node/{SheetContainer-2MEALQWW.cjs → SheetContainer-OZ7DHH4L.cjs} +18 -17
  12. package/dist/lib/node/SheetContainer-OZ7DHH4L.cjs.map +7 -0
  13. package/dist/lib/node/{chunk-6DQABRGJ.cjs → chunk-5FTFZL5W.cjs} +57 -35
  14. package/dist/lib/node/chunk-5FTFZL5W.cjs.map +7 -0
  15. package/dist/lib/node/{chunk-P5QYYEHQ.cjs → chunk-5XPK2V4A.cjs} +186 -158
  16. package/dist/lib/node/chunk-5XPK2V4A.cjs.map +7 -0
  17. package/dist/lib/node/graph-Q3N2X26H.cjs +55 -0
  18. package/dist/lib/node/graph-Q3N2X26H.cjs.map +7 -0
  19. package/dist/lib/node/index.cjs +38 -18
  20. package/dist/lib/node/index.cjs.map +4 -4
  21. package/dist/lib/node/meta.json +1 -1
  22. package/dist/lib/node-esm/{SheetContainer-RPSUSXWS.mjs → SheetContainer-4XS2G25Z.mjs} +11 -10
  23. package/dist/lib/node-esm/SheetContainer-4XS2G25Z.mjs.map +7 -0
  24. package/dist/lib/node-esm/{chunk-4MM7THJW.mjs → chunk-5WPZCXNS.mjs} +183 -159
  25. package/dist/lib/node-esm/chunk-5WPZCXNS.mjs.map +7 -0
  26. package/dist/lib/node-esm/{chunk-5RLTCIE2.mjs → chunk-KK3XL37M.mjs} +55 -33
  27. package/dist/lib/node-esm/chunk-KK3XL37M.mjs.map +7 -0
  28. package/dist/lib/node-esm/graph-SMPUMOV2.mjs +34 -0
  29. package/dist/lib/node-esm/index.mjs +37 -15
  30. package/dist/lib/node-esm/index.mjs.map +4 -4
  31. package/dist/lib/node-esm/meta.json +1 -1
  32. package/dist/types/src/SheetPlugin.d.ts.map +1 -1
  33. package/dist/types/src/components/CellEditor/CellEditor.stories.d.ts.map +1 -1
  34. package/dist/types/src/components/CellEditor/extension.d.ts.map +1 -1
  35. package/dist/types/src/components/Sheet/Sheet.d.ts.map +1 -1
  36. package/dist/types/src/components/Sheet/Sheet.stories.d.ts.map +1 -1
  37. package/dist/types/src/components/SheetContainer.d.ts.map +1 -1
  38. package/dist/types/src/components/index.d.ts.map +1 -1
  39. package/dist/types/src/extensions/compute.d.ts +2 -5
  40. package/dist/types/src/extensions/compute.d.ts.map +1 -1
  41. package/dist/types/src/extensions/compute.stories.d.ts.map +1 -1
  42. package/dist/types/src/graph/compute-graph-registry.d.ts +34 -0
  43. package/dist/types/src/graph/compute-graph-registry.d.ts.map +1 -0
  44. package/dist/types/src/graph/compute-graph.d.ts +13 -33
  45. package/dist/types/src/graph/compute-graph.d.ts.map +1 -1
  46. package/dist/types/src/graph/compute-node.d.ts.map +1 -1
  47. package/dist/types/src/graph/{async-function.d.ts → functions/async-function.d.ts} +6 -3
  48. package/dist/types/src/graph/functions/async-function.d.ts.map +1 -0
  49. package/dist/types/src/graph/functions/edge-function.d.ts +21 -0
  50. package/dist/types/src/graph/functions/edge-function.d.ts.map +1 -0
  51. package/dist/types/src/graph/functions/function-defs.d.ts.map +1 -0
  52. package/dist/types/src/graph/functions/index.d.ts +4 -0
  53. package/dist/types/src/graph/functions/index.d.ts.map +1 -0
  54. package/dist/types/src/graph/index.d.ts +2 -1
  55. package/dist/types/src/graph/index.d.ts.map +1 -1
  56. package/dist/types/src/graph/testing/index.d.ts +2 -1
  57. package/dist/types/src/graph/testing/index.d.ts.map +1 -1
  58. package/dist/types/src/graph/testing/test-builder.d.ts +15 -0
  59. package/dist/types/src/graph/testing/test-builder.d.ts.map +1 -0
  60. package/dist/types/src/graph/testing/test-plugin.d.ts +36 -0
  61. package/dist/types/src/graph/testing/test-plugin.d.ts.map +1 -0
  62. package/dist/types/src/hooks/useComputeGraph.d.ts.map +1 -1
  63. package/dist/types/src/model/sheet-model.d.ts.map +1 -1
  64. package/dist/types/src/model/sheet-model.test.d.ts +2 -0
  65. package/dist/types/src/model/sheet-model.test.d.ts.map +1 -0
  66. package/package.json +40 -39
  67. package/src/SheetPlugin.tsx +12 -10
  68. package/src/components/CellEditor/CellEditor.stories.tsx +1 -2
  69. package/src/components/CellEditor/extension.test.ts +0 -1
  70. package/src/components/CellEditor/extension.ts +4 -3
  71. package/src/components/Sheet/Sheet.stories.tsx +2 -2
  72. package/src/components/Sheet/Sheet.tsx +30 -14
  73. package/src/components/SheetContainer.tsx +11 -13
  74. package/src/extensions/compute.stories.tsx +9 -11
  75. package/src/extensions/compute.ts +66 -50
  76. package/src/graph/compute-graph-registry.ts +90 -0
  77. package/src/graph/compute-graph.stories.tsx +2 -2
  78. package/src/graph/compute-graph.test.ts +31 -71
  79. package/src/graph/compute-graph.ts +45 -116
  80. package/src/graph/compute-node.ts +1 -0
  81. package/src/graph/{async-function.ts → functions/async-function.ts} +10 -9
  82. package/src/graph/{edge-function.ts → functions/edge-function.ts} +13 -11
  83. package/src/graph/functions/index.ts +7 -0
  84. package/src/graph/hyperformula.test.ts +1 -2
  85. package/src/graph/index.ts +2 -1
  86. package/src/graph/testing/index.ts +2 -1
  87. package/src/graph/testing/test-builder.ts +54 -0
  88. package/src/graph/testing/{custom-function.ts → test-plugin.ts} +38 -12
  89. package/src/hooks/useComputeGraph.ts +8 -1
  90. package/src/model/sheet-model.test.ts +59 -0
  91. package/src/model/sheet-model.ts +4 -2
  92. package/dist/lib/browser/SheetContainer-VISF3VUB.mjs.map +0 -7
  93. package/dist/lib/browser/chunk-Z2XOOC2R.mjs.map +0 -7
  94. package/dist/lib/browser/chunk-ZLJ2GRE2.mjs.map +0 -7
  95. package/dist/lib/browser/graph-4XFKIHRL.mjs +0 -21
  96. package/dist/lib/node/SheetContainer-2MEALQWW.cjs.map +0 -7
  97. package/dist/lib/node/chunk-6DQABRGJ.cjs.map +0 -7
  98. package/dist/lib/node/chunk-P5QYYEHQ.cjs.map +0 -7
  99. package/dist/lib/node/graph-2LRDUXBZ.cjs +0 -43
  100. package/dist/lib/node/graph-2LRDUXBZ.cjs.map +0 -7
  101. package/dist/lib/node-esm/SheetContainer-RPSUSXWS.mjs.map +0 -7
  102. package/dist/lib/node-esm/chunk-4MM7THJW.mjs.map +0 -7
  103. package/dist/lib/node-esm/chunk-5RLTCIE2.mjs.map +0 -7
  104. package/dist/lib/node-esm/graph-WG5EKOMO.mjs +0 -22
  105. package/dist/types/src/graph/async-function.d.ts.map +0 -1
  106. package/dist/types/src/graph/edge-function.d.ts +0 -20
  107. package/dist/types/src/graph/edge-function.d.ts.map +0 -1
  108. package/dist/types/src/graph/function-defs.d.ts.map +0 -1
  109. package/dist/types/src/graph/testing/custom-function.d.ts +0 -23
  110. package/dist/types/src/graph/testing/custom-function.d.ts.map +0 -1
  111. /package/dist/lib/browser/{graph-4XFKIHRL.mjs.map → graph-M4IQ76QX.mjs.map} +0 -0
  112. /package/dist/lib/node-esm/{graph-WG5EKOMO.mjs.map → graph-SMPUMOV2.mjs.map} +0 -0
  113. /package/dist/types/src/graph/{function-defs.d.ts → functions/function-defs.d.ts} +0 -0
  114. /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 SpaceId, type Space, Filter, fullyQualifiedId } from '@dxos/client/echo';
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 { EdgeFunctionPlugin, EdgeFunctionPluginTranslations } from './edge-function';
24
- import { defaultFunctions, type FunctionDefinition } from './function-defs';
25
-
26
- //
27
- // NOTE: The package.json file defines the packaged #hyperformula module.
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) + 26 (object id) + 1 (separator).
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
- export type ComputeGraphPlugin = {
37
- plugin: FunctionPluginDefinition;
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
- export const defaultPlugins: ComputeGraphPlugin[] = [
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 PREFIX = '__';
61
- export const createSheetName = (id: string) => `${PREFIX}${id}`;
62
- export const getSheetId = (name: string): string | undefined =>
63
- name.startsWith(PREFIX) ? name.slice(PREFIX.length) : undefined;
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 _functions: FunctionType[] = [];
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._functions.map((fn) => ({ name: fn.binding! })) : []),
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
- async getOrCreateNode(name: string): Promise<ComputeNode> {
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
- // Sheet references.
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): What if not loaded?
145
+ // TODO(burdon): Cache map.
221
146
  const objects = this._hf
222
147
  .getSheetNames()
223
148
  .map((name) => {
224
- const id = getSheetId(name);
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 || obj.title === name) {
231
- return `${createSheetName(obj.id)}!`;
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
- // Functions.
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._functions.find((fn) => fn.binding === binding);
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 `${CUSTOM_FUNCTION}("${binding}")`;
176
+ return `${EDGE_FUNCTION_NAME}("${binding}")`;
248
177
  } else {
249
- return `${CUSTOM_FUNCTION}("${binding}", ${args})`;
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 === CUSTOM_FUNCTION || defaultFunctions.find((fn) => fn.name === 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._functions.find((fn) => fn.binding === binding);
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._functions.find((fn) => fullyQualifiedId(fn) === id);
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._functions = objects.filter(({ binding }) => binding);
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
- recalculationDelay: number;
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
- recalculationDelay: 200,
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.recalculationDelay);
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 FunctionPluginAsync extends FunctionPlugin {
163
+ export class AsyncFunctionPlugin extends FunctionPlugin {
166
164
  get context() {
167
165
  return this.config.context as FunctionContext;
168
166
  }
169
167
 
170
- runAsyncFunction(ast: ProcedureAst, state: InterpreterState, cb: AsyncFunction, options?: FunctionOptions) {
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, FunctionPluginAsync } from './async-function';
16
+ import { type AsyncFunction, AsyncFunctionPlugin } from './async-function';
17
17
 
18
- const EDGE_FUNCTION_TTL = 10_000;
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 FunctionPluginAsync {
26
- edge(ast: ProcedureAst, state: InterpreterState) {
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: EDGE_FUNCTION_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: EDGE_FUNCTION_TTL });
68
+ return this.runAsyncFunction(ast, state, handler(true), { ttl: FUNCTION_TTL });
67
69
  }
68
70
  }
69
71
 
70
72
  EdgeFunctionPlugin.implementedFunctions = {
71
- EDGE: {
72
- method: 'edge',
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
- EDGE: 'EDGE',
95
+ [EDGE_FUNCTION_NAME]: 'Remote function',
94
96
  },
95
97
  enUS: {
96
- EDGE: 'EDGE',
98
+ [EDGE_FUNCTION_NAME]: 'Remote function',
97
99
  },
98
100
  };
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './async-function';
6
+ export * from './edge-function';
7
+ export * from './function-defs';
@@ -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
  });
@@ -3,5 +3,6 @@
3
3
  //
4
4
 
5
5
  export * from './compute-graph';
6
+ export * from './compute-graph-registry';
6
7
  export * from './compute-node';
7
- export * from './function-defs';
8
+ export * from './functions';
@@ -2,4 +2,5 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export * from './custom-function';
5
+ export * from './test-builder';
6
+ export * from './test-plugin';
@@ -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 AsyncFunction, FunctionPluginAsync } from '../async-function';
12
- import { type ComputeGraphPlugin } from '../compute-graph';
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 CustomPlugin extends FunctionPluginAsync {
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
- CustomPlugin.implementedFunctions = {
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 CustomPluginTranslations = {
82
+ export const TestPluginTranslations = {
59
83
  enGB: {
60
- TEST: 'TEST',
61
- CRYPTO: 'CRYPTO',
84
+ TEST: 'Returns input value',
85
+ RANDOM: 'Random number',
86
+ CRYPTO: 'Crypto token value',
62
87
  },
63
88
  enUS: {
64
- TEST: 'TEST',
65
- CRYPTO: 'CRYPTO',
89
+ TEST: 'Returns input value',
90
+ RANDOM: 'Random number',
91
+ CRYPTO: 'Crypto token value',
66
92
  },
67
93
  };
68
94
 
69
- export const testPlugins: ComputeGraphPlugin[] = [
95
+ export const testFunctionPlugins: ComputeGraphPlugin[] = [
70
96
  {
71
- plugin: CustomPlugin,
72
- translations: CustomPluginTranslations,
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 () => space && registry.getOrCreateGraph(space), [space, registry]);
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
+ });