@dxos/plugin-sheet 0.6.12-main.c974201 → 0.6.12-main.f9d0246

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 (122) hide show
  1. package/dist/lib/browser/{SheetContainer-V4GCCZTX.mjs → SheetContainer-VISF3VUB.mjs} +6 -6
  2. package/dist/lib/browser/{SheetContainer-V4GCCZTX.mjs.map → SheetContainer-VISF3VUB.mjs.map} +3 -3
  3. package/dist/lib/browser/{chunk-T3NJFTD4.mjs → chunk-WZMOZKQZ.mjs} +2 -2
  4. package/dist/lib/browser/{chunk-T3NJFTD4.mjs.map → chunk-WZMOZKQZ.mjs.map} +3 -3
  5. package/dist/lib/browser/{chunk-6ZMQVB4Z.mjs → chunk-Z2XOOC2R.mjs} +81 -62
  6. package/dist/lib/browser/chunk-Z2XOOC2R.mjs.map +7 -0
  7. package/dist/lib/browser/{chunk-U2JHW3L6.mjs → chunk-ZLJ2GRE2.mjs} +173 -42
  8. package/dist/lib/browser/chunk-ZLJ2GRE2.mjs.map +7 -0
  9. package/dist/lib/browser/{graph-T27BOBOV.mjs → graph-4XFKIHRL.mjs} +4 -4
  10. package/dist/lib/browser/index.mjs +15 -13
  11. package/dist/lib/browser/index.mjs.map +3 -3
  12. package/dist/lib/browser/meta.json +1 -1
  13. package/dist/lib/browser/types.mjs +1 -1
  14. package/dist/lib/node/{SheetContainer-3ZY7MPWJ.cjs → SheetContainer-2MEALQWW.cjs} +14 -14
  15. package/dist/lib/node/{SheetContainer-3ZY7MPWJ.cjs.map → SheetContainer-2MEALQWW.cjs.map} +3 -3
  16. package/dist/lib/node/{chunk-OTTD7FBK.cjs → chunk-6DQABRGJ.cjs} +192 -60
  17. package/dist/lib/node/chunk-6DQABRGJ.cjs.map +7 -0
  18. package/dist/lib/node/{chunk-Q3HBHPRL.cjs → chunk-AOP42UAA.cjs} +5 -5
  19. package/dist/lib/node/{chunk-Q3HBHPRL.cjs.map → chunk-AOP42UAA.cjs.map} +3 -3
  20. package/dist/lib/node/{chunk-DD6FIXWC.cjs → chunk-P5QYYEHQ.cjs} +86 -67
  21. package/dist/lib/node/chunk-P5QYYEHQ.cjs.map +7 -0
  22. package/dist/lib/node/{graph-SPKGX7W4.cjs → graph-2LRDUXBZ.cjs} +14 -14
  23. package/dist/lib/node/graph-2LRDUXBZ.cjs.map +7 -0
  24. package/dist/lib/node/index.cjs +25 -24
  25. package/dist/lib/node/index.cjs.map +3 -3
  26. package/dist/lib/node/meta.json +1 -1
  27. package/dist/lib/node/types.cjs +8 -8
  28. package/dist/lib/node/types.cjs.map +1 -1
  29. package/dist/lib/node-esm/{SheetContainer-PXSJX6XK.mjs → SheetContainer-RPSUSXWS.mjs} +6 -6
  30. package/dist/lib/node-esm/{SheetContainer-PXSJX6XK.mjs.map → SheetContainer-RPSUSXWS.mjs.map} +3 -3
  31. package/dist/lib/node-esm/{chunk-D6KU5MI7.mjs → chunk-4MM7THJW.mjs} +81 -62
  32. package/dist/lib/node-esm/chunk-4MM7THJW.mjs.map +7 -0
  33. package/dist/lib/node-esm/{chunk-7HVSOTGA.mjs → chunk-5RLTCIE2.mjs} +173 -42
  34. package/dist/lib/node-esm/chunk-5RLTCIE2.mjs.map +7 -0
  35. package/dist/lib/node-esm/{chunk-BMNA27EX.mjs → chunk-RR2AO4SM.mjs} +2 -2
  36. package/dist/lib/node-esm/{chunk-BMNA27EX.mjs.map → chunk-RR2AO4SM.mjs.map} +3 -3
  37. package/dist/lib/node-esm/{graph-U67IO4UC.mjs → graph-WG5EKOMO.mjs} +4 -4
  38. package/dist/lib/node-esm/index.mjs +15 -13
  39. package/dist/lib/node-esm/index.mjs.map +3 -3
  40. package/dist/lib/node-esm/meta.json +1 -1
  41. package/dist/lib/node-esm/types.mjs +1 -1
  42. package/dist/types/src/SheetPlugin.d.ts.map +1 -1
  43. package/dist/types/src/components/GridSheet/GridSheet.d.ts +3 -3
  44. package/dist/types/src/components/GridSheet/GridSheet.d.ts.map +1 -1
  45. package/dist/types/src/components/GridSheet/GridSheet.stories.d.ts +1 -1
  46. package/dist/types/src/components/GridSheet/GridSheet.stories.d.ts.map +1 -1
  47. package/dist/types/src/components/GridSheet/util.d.ts +3 -2
  48. package/dist/types/src/components/GridSheet/util.d.ts.map +1 -1
  49. package/dist/types/src/components/Sheet/Sheet.stories.d.ts.map +1 -1
  50. package/dist/types/src/components/Sheet/sheet-context.d.ts +3 -3
  51. package/dist/types/src/components/Sheet/sheet-context.d.ts.map +1 -1
  52. package/dist/types/src/components/SheetContainer.d.ts +1 -1
  53. package/dist/types/src/components/index.d.ts +1 -1
  54. package/dist/types/src/defs/types.d.ts.map +1 -1
  55. package/dist/types/src/defs/util.d.ts +1 -1
  56. package/dist/types/src/defs/util.d.ts.map +1 -1
  57. package/dist/types/src/extensions/compute.d.ts +5 -1
  58. package/dist/types/src/extensions/compute.d.ts.map +1 -1
  59. package/dist/types/src/extensions/compute.stories.d.ts.map +1 -1
  60. package/dist/types/src/graph/async-function.d.ts +7 -1
  61. package/dist/types/src/graph/async-function.d.ts.map +1 -1
  62. package/dist/types/src/graph/compute-graph.d.ts +12 -9
  63. package/dist/types/src/graph/compute-graph.d.ts.map +1 -1
  64. package/dist/types/src/graph/compute-graph.stories.d.ts.map +1 -1
  65. package/dist/types/src/graph/compute-graph.test.d.ts +2 -0
  66. package/dist/types/src/graph/compute-graph.test.d.ts.map +1 -0
  67. package/dist/types/src/graph/compute-node.d.ts +9 -2
  68. package/dist/types/src/graph/compute-node.d.ts.map +1 -1
  69. package/dist/types/src/graph/edge-function.d.ts.map +1 -1
  70. package/dist/types/src/graph/{custom-function.d.ts → testing/custom-function.d.ts} +3 -1
  71. package/dist/types/src/graph/testing/custom-function.d.ts.map +1 -0
  72. package/dist/types/src/graph/testing/index.d.ts +2 -0
  73. package/dist/types/src/graph/testing/index.d.ts.map +1 -0
  74. package/dist/types/src/hooks/useComputeGraph.d.ts.map +1 -1
  75. package/dist/types/src/hooks/useSheetModel.d.ts +2 -2
  76. package/dist/types/src/hooks/useSheetModel.d.ts.map +1 -1
  77. package/dist/types/src/model/sheet-model.d.ts +3 -3
  78. package/dist/types/src/model/sheet-model.d.ts.map +1 -1
  79. package/dist/types/src/testing/testing.d.ts +4 -5
  80. package/dist/types/src/testing/testing.d.ts.map +1 -1
  81. package/dist/types/src/types.d.ts +4 -3
  82. package/dist/types/src/types.d.ts.map +1 -1
  83. package/package.json +33 -33
  84. package/src/SheetPlugin.tsx +9 -7
  85. package/src/components/CellEditor/CellEditor.stories.tsx +1 -1
  86. package/src/components/GridSheet/GridSheet.stories.tsx +5 -4
  87. package/src/components/GridSheet/GridSheet.tsx +6 -6
  88. package/src/components/GridSheet/util.ts +46 -27
  89. package/src/components/Sheet/Sheet.stories.tsx +21 -20
  90. package/src/components/Sheet/sheet-context.tsx +4 -4
  91. package/src/components/SheetContainer.tsx +2 -2
  92. package/src/defs/types.ts +1 -0
  93. package/src/defs/util.ts +19 -3
  94. package/src/extensions/compute.stories.tsx +18 -16
  95. package/src/extensions/compute.ts +72 -39
  96. package/src/graph/async-function.ts +13 -6
  97. package/src/graph/compute-graph.stories.tsx +4 -3
  98. package/src/graph/compute-graph.test.ts +127 -0
  99. package/src/graph/compute-graph.ts +64 -41
  100. package/src/graph/compute-node.ts +16 -5
  101. package/src/graph/edge-function.ts +1 -2
  102. package/src/graph/{custom-function.ts → testing/custom-function.ts} +10 -2
  103. package/src/graph/testing/index.ts +5 -0
  104. package/src/hooks/hooks.stories.tsx +3 -3
  105. package/src/hooks/useComputeGraph.ts +2 -1
  106. package/src/hooks/useSheetModel.ts +4 -7
  107. package/src/model/sheet-model.ts +44 -29
  108. package/src/testing/testing.tsx +17 -15
  109. package/src/types.ts +3 -3
  110. package/dist/lib/browser/chunk-6ZMQVB4Z.mjs.map +0 -7
  111. package/dist/lib/browser/chunk-U2JHW3L6.mjs.map +0 -7
  112. package/dist/lib/node/chunk-DD6FIXWC.cjs.map +0 -7
  113. package/dist/lib/node/chunk-OTTD7FBK.cjs.map +0 -7
  114. package/dist/lib/node/graph-SPKGX7W4.cjs.map +0 -7
  115. package/dist/lib/node-esm/chunk-7HVSOTGA.mjs.map +0 -7
  116. package/dist/lib/node-esm/chunk-D6KU5MI7.mjs.map +0 -7
  117. package/dist/types/src/graph/compute-graph.browser.test.d.ts +0 -2
  118. package/dist/types/src/graph/compute-graph.browser.test.d.ts.map +0 -1
  119. package/dist/types/src/graph/custom-function.d.ts.map +0 -1
  120. package/src/graph/compute-graph.browser.test.ts +0 -104
  121. /package/dist/lib/browser/{graph-T27BOBOV.mjs.map → graph-4XFKIHRL.mjs.map} +0 -0
  122. /package/dist/lib/node-esm/{graph-U67IO4UC.mjs.map → graph-WG5EKOMO.mjs.map} +0 -0
@@ -2,6 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import { type SimpleCellAddress } from 'hyperformula/typings/Cell';
5
6
  import { type InterpreterState } from 'hyperformula/typings/interpreter/InterpreterState';
6
7
  import { type InterpreterValue } from 'hyperformula/typings/interpreter/InterpreterValue';
7
8
  import { type ProcedureAst } from 'hyperformula/typings/parser';
@@ -22,6 +23,11 @@ import { CellError, ErrorType, EmptyValue, FunctionPlugin, type HyperFormula } f
22
23
  // TODO(burdon): Create wrapper.
23
24
  export type AsyncFunction = (...args: any) => Promise<InterpreterValue>;
24
25
 
26
+ export type FunctionUpdateEvent = {
27
+ name: string;
28
+ cell: SimpleCellAddress;
29
+ };
30
+
25
31
  export type FunctionOptions = {
26
32
  ttl?: number;
27
33
  };
@@ -30,6 +36,7 @@ export type FunctionContextOptions = {
30
36
  defaultTtl: number;
31
37
  recalculationDelay: number;
32
38
  remoteFunctionUrl: string;
39
+ onUpdate?: (update: FunctionUpdateEvent) => void;
33
40
  };
34
41
 
35
42
  export const defaultFunctionContextOptions: FunctionContextOptions = {
@@ -66,24 +73,24 @@ export class FunctionContext {
66
73
  private _invocations: Record<string, number> = {};
67
74
 
68
75
  private readonly _options: FunctionContextOptions;
69
- private readonly _onUpdate: () => void;
76
+
77
+ // Debounced update handler.
78
+ private readonly _onUpdate: (update: FunctionUpdateEvent) => void;
70
79
 
71
80
  constructor(
72
81
  private readonly _hf: HyperFormula,
73
82
  private readonly _space: Space | undefined,
74
- onUpdate: (context: FunctionContext) => void,
75
83
  _options?: Partial<FunctionContextOptions>,
76
84
  ) {
77
85
  this._options = defaultsDeep(_options ?? {}, defaultFunctionContextOptions);
78
- this._onUpdate = debounce(() => {
86
+ this._onUpdate = debounce((update) => {
79
87
  // TODO(burdon): Better way to trigger recalculation?
80
88
  // NOTE: rebuildAndRecalculate resets the undo history.
81
89
  this._hf.resumeEvaluation();
82
- onUpdate(this);
90
+ this._options.onUpdate?.(update);
83
91
  }, this._options.recalculationDelay);
84
92
  }
85
93
 
86
- // TODO(burdon): Remove?
87
94
  get space() {
88
95
  return this._space;
89
96
  }
@@ -136,7 +143,7 @@ export class FunctionContext {
136
143
  const value = await cb(...args);
137
144
  this._cache.set(invocationKey, { value, ts: Date.now() });
138
145
  log('set', { cell, value });
139
- this._onUpdate();
146
+ this._onUpdate({ name, cell });
140
147
  } catch (err) {
141
148
  // TODO(burdon): Show error to user.
142
149
  log.warn('failed', { cell, err });
@@ -13,9 +13,10 @@ import { Toolbar, Button, Input } from '@dxos/react-ui';
13
13
  import { SyntaxHighlighter } from '@dxos/react-ui-syntax-highlighter';
14
14
  import { withTheme } from '@dxos/storybook-utils';
15
15
 
16
+ import { testPlugins } from './testing';
16
17
  import { createSheet } from '../defs';
17
18
  import { useComputeGraph, useSheetModel } from '../hooks';
18
- import { withGraphDecorator } from '../testing';
19
+ import { withComputeGraphDecorator } from '../testing';
19
20
  import { SheetType } from '../types';
20
21
 
21
22
  const FUNCTION_NAME = 'TEST';
@@ -26,7 +27,7 @@ const Story = () => {
26
27
  const [sheet, setSheet] = useState<SheetType>();
27
28
  const [text, setText] = useState(`${FUNCTION_NAME}(100)`);
28
29
  const [result, setResult] = useState<any>();
29
- const model = useSheetModel(space, sheet);
30
+ const model = useSheetModel(graph, sheet);
30
31
  useEffect(() => {
31
32
  if (space) {
32
33
  const sheet = space.db.add(createSheet());
@@ -83,7 +84,7 @@ export default {
83
84
  title: 'plugin-sheet/functions',
84
85
  decorators: [
85
86
  withClientProvider({ types: [FunctionType, SheetType], createIdentity: true, createSpace: true }),
86
- withGraphDecorator,
87
+ withComputeGraphDecorator({ plugins: testPlugins }),
87
88
  withTheme,
88
89
  ],
89
90
  render: (args: any) => <Story {...args} />,
@@ -0,0 +1,127 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type CellValue } from 'hyperformula';
6
+ import { afterEach, beforeEach, describe, expect, test } from 'vitest';
7
+
8
+ import { Trigger } from '@dxos/async';
9
+ import { Client } from '@dxos/client';
10
+ import { create, fullyQualifiedId } from '@dxos/client/echo';
11
+ import { Context } from '@dxos/context';
12
+ import { type S } from '@dxos/echo-schema';
13
+ import { FunctionType } from '@dxos/plugin-script/types';
14
+
15
+ import { ComputeGraphRegistry } from './compute-graph';
16
+ import { testPlugins } from './testing';
17
+ import { addressFromA1Notation, createSheet } from '../defs';
18
+ import { SheetModel } from '../model';
19
+ import { type CellScalarValue } from '../types';
20
+
21
+ /**
22
+ * NOTE: Browser test required for hyperformula due to raw translation files.
23
+ */
24
+ describe('compute graph', () => {
25
+ let ctx: Context;
26
+ beforeEach(() => {
27
+ ctx = new Context();
28
+ });
29
+ afterEach(async () => {
30
+ await ctx.dispose();
31
+ });
32
+
33
+ // TODO(burdon): Replace with builder.
34
+ const createModel = async (types?: S.Schema<any>[]) => {
35
+ const client = new Client();
36
+ if (types) {
37
+ // TODO(burdon): Add to config.
38
+ client.addTypes(types);
39
+ }
40
+ await client.initialize();
41
+ await client.halo.createIdentity();
42
+ const space = await client.spaces.create();
43
+ ctx.onDispose(() => client.destroy());
44
+
45
+ const registry = new ComputeGraphRegistry({ plugins: testPlugins });
46
+ await registry.open();
47
+ ctx.onDispose(() => registry.close());
48
+
49
+ const graph = await registry.createGraph(space);
50
+
51
+ const sheet = createSheet({ rows: 5, columns: 5 });
52
+ const model = new SheetModel(graph, sheet);
53
+ await model.open();
54
+
55
+ return { space, graph, model };
56
+ };
57
+
58
+ test('map functions', async () => {
59
+ const { space, graph } = await createModel([FunctionType]);
60
+
61
+ // Create script.
62
+ const trigger = new Trigger();
63
+ graph.update.once(() => trigger.wake());
64
+ const fn = space.db.add(create(FunctionType, { version: 1, binding: 'TEST' }));
65
+ await trigger.wait();
66
+ expect(graph.getFunctions({ echo: true })).to.toHaveLength(1);
67
+
68
+ const id = graph.mapFunctionBindingToId('TEST()');
69
+ expect(id).to.eq(`${fullyQualifiedId(fn)}()`);
70
+ });
71
+
72
+ test('cross-node references', async () => {
73
+ const { graph } = await createModel();
74
+
75
+ // Create nodes.
76
+ const node1 = await graph.getOrCreateNode('node-1');
77
+ const node2 = await graph.getOrCreateNode('node-2');
78
+
79
+ {
80
+ expect(graph.hf.getSheetNames()).to.toHaveLength(3);
81
+ node1.graph.hf.setCellContents({ sheet: node1.sheetId, row: 0, col: 0 }, [[100, 200, 300, '=SUM(A1:C1)']]);
82
+ node2.graph.hf.setCellContents({ sheet: node2.sheetId, row: 0, col: 0 }, "='node-1'!D1");
83
+ const value1 = node1.graph.hf.getCellValue({ sheet: node1.sheetId, col: 3, row: 0 });
84
+ const value2 = node2.graph.hf.getCellValue({ sheet: node2.sheetId, col: 0, row: 0 });
85
+ expect(value1).to.eq(value2);
86
+ }
87
+
88
+ // Get updated event.
89
+ const trigger = new Trigger<CellValue>();
90
+ node2.update.on(({ change }) => {
91
+ const value = node2.graph.hf.getCellValue({ sheet: node2.sheetId, col: 0, row: 0 });
92
+ expect(value).to.eq(change?.newValue);
93
+ trigger.wake(value);
94
+ });
95
+
96
+ {
97
+ node1.graph.hf.setCellContents({ sheet: node1.sheetId, row: 0, col: 0 }, 400);
98
+ const value1 = node1.graph.hf.getCellValue({ sheet: node1.sheetId, col: 3, row: 0 });
99
+ const value2 = await trigger.wait();
100
+ expect(value1).to.eq(value2);
101
+ }
102
+ });
103
+
104
+ test('async function', async () => {
105
+ const { graph, model } = await createModel();
106
+
107
+ // Triggers function.
108
+ model.setValue(addressFromA1Notation('A1'), '=TEST()');
109
+ const trigger = new Trigger<CellScalarValue>();
110
+ model.update.once(({ type }) => {
111
+ if (type === 'valuesUpdated') {
112
+ const value = model.getValue(addressFromA1Notation('A1'));
113
+ trigger.wake(value);
114
+ }
115
+ });
116
+
117
+ // Get initial value (null).
118
+ const v1 = model.getValue(addressFromA1Notation('A1'));
119
+ expect(v1).to.be.null;
120
+ expect(graph.context.info.invocations.TEST).to.eq(undefined);
121
+
122
+ // Wait until async update triggered.
123
+ const v2 = await trigger.wait();
124
+ expect(v2).not.to.be.null;
125
+ expect(graph.context.info.invocations.TEST).to.eq(1);
126
+ });
127
+ });
@@ -4,7 +4,9 @@
4
4
 
5
5
  import { type FunctionPluginDefinition } from 'hyperformula';
6
6
  import { type ConfigParams } from 'hyperformula/typings/ConfigParams';
7
+ import { type Listeners } from 'hyperformula/typings/Emitter';
7
8
  import { type FunctionTranslationsPackage } from 'hyperformula/typings/interpreter';
9
+ import defaultsDeep from 'lodash.defaultsdeep';
8
10
 
9
11
  import { Event } from '@dxos/async';
10
12
  import { type SpaceId, type Space, Filter, fullyQualifiedId } from '@dxos/client/echo';
@@ -15,7 +17,7 @@ import { log } from '@dxos/log';
15
17
  import { FunctionType } from '@dxos/plugin-script/types';
16
18
  import { nonNullable } from '@dxos/util';
17
19
 
18
- import { HyperFormula } from '#hyperformula';
20
+ import { ExportedCellChange, HyperFormula } from '#hyperformula';
19
21
  import { FunctionContext, type FunctionContextOptions } from './async-function';
20
22
  import { ComputeNode } from './compute-node';
21
23
  import { EdgeFunctionPlugin, EdgeFunctionPluginTranslations } from './edge-function';
@@ -43,14 +45,15 @@ export type ComputeGraphOptions = {
43
45
 
44
46
  export const defaultOptions: ComputeGraphOptions = {
45
47
  licenseKey: 'gpl-v3',
46
- plugins: [
47
- {
48
- plugin: EdgeFunctionPlugin,
49
- translations: EdgeFunctionPluginTranslations,
50
- },
51
- ],
52
48
  };
53
49
 
50
+ export const defaultPlugins: ComputeGraphPlugin[] = [
51
+ {
52
+ plugin: EdgeFunctionPlugin,
53
+ translations: EdgeFunctionPluginTranslations,
54
+ },
55
+ ];
56
+
54
57
  /**
55
58
  * Marker for sheets that are managed by an ECHO object.
56
59
  */
@@ -59,34 +62,29 @@ export const createSheetName = (id: string) => `${PREFIX}${id}`;
59
62
  export const getSheetId = (name: string): string | undefined =>
60
63
  name.startsWith(PREFIX) ? name.slice(PREFIX.length) : undefined;
61
64
 
62
- /**
63
- * NOTE: Async imports to decouple hyperformula deps.
64
- */
65
- export const createComputeGraphRegistry = (options: Partial<FunctionContextOptions> = {}) => {
66
- return new ComputeGraphRegistry({
67
- ...defaultOptions,
68
- ...options,
69
- });
70
- };
71
-
72
65
  /**
73
66
  * Manages a collection of ComputeGraph instances for each space.
74
67
  *
75
68
  * [ComputePlugin] => [ComputeGraphRegistry] => [ComputeGraph(Space)] => [ComputeNode(Object)]
69
+ *
70
+ * NOTE: The ComputeGraphRegistry manages the hierarchy of resources via its root Context.
76
71
  */
77
72
  // TODO(burdon): Move graph into separate plugin; isolate HF deps.
78
73
  export class ComputeGraphRegistry extends Resource {
79
- private readonly _registry = new Map<SpaceId, ComputeGraph>();
74
+ private readonly _graphs = new Map<SpaceId, ComputeGraph>();
80
75
 
81
- constructor(private readonly _options: ComputeGraphOptions = defaultOptions) {
76
+ private readonly _options: ComputeGraphOptions;
77
+
78
+ constructor(options: ComputeGraphOptions = { plugins: defaultPlugins }) {
82
79
  super();
80
+ this._options = defaultsDeep({}, options, defaultOptions);
83
81
  this._options.plugins?.forEach(({ plugin, translations }) => {
84
82
  HyperFormula.registerFunctionPlugin(plugin, translations);
85
83
  });
86
84
  }
87
85
 
88
86
  getGraph(spaceId: SpaceId): ComputeGraph | undefined {
89
- return this._registry.get(spaceId);
87
+ return this._graphs.get(spaceId);
90
88
  }
91
89
 
92
90
  async getOrCreateGraph(space: Space): Promise<ComputeGraph> {
@@ -100,22 +98,29 @@ export class ComputeGraphRegistry extends Resource {
100
98
  }
101
99
 
102
100
  async createGraph(space: Space): Promise<ComputeGraph> {
103
- invariant(!this._registry.has(space.id), `ComputeGraph already exists for space: ${space.id}`);
101
+ invariant(!this._graphs.has(space.id), `ComputeGraph already exists for space: ${space.id}`);
104
102
  const hf = HyperFormula.buildEmpty(this._options);
105
103
  const graph = new ComputeGraph(hf, space, this._options);
106
- await graph.open(this._ctx);
107
- this._registry.set(space.id, graph);
104
+ this._graphs.set(space.id, graph);
105
+ await graph.open();
108
106
  return graph;
109
107
  }
108
+
109
+ protected override async _close() {
110
+ for (const graph of this._graphs.values()) {
111
+ await graph.close();
112
+ }
113
+ }
110
114
  }
111
115
 
116
+ export type ComputeGraphEvent = 'functionsUpdated';
117
+
112
118
  /**
113
119
  * Per-space compute and dependency graph.
114
120
  * Consists of multiple ComputeNode (corresponding to a HyperFormula sheet).
115
121
  * Manages the set of custom functions.
116
122
  * HyperFormula manages the dependency graph.
117
123
  */
118
- // TODO(burdon): Tests.
119
124
  export class ComputeGraph extends Resource {
120
125
  public readonly id = `graph-${PublicKey.random().truncate()}`;
121
126
 
@@ -125,13 +130,10 @@ export class ComputeGraph extends Resource {
125
130
  // Cached function objects.
126
131
  private _functions: FunctionType[] = [];
127
132
 
128
- // The context is passed to all functions.
129
- public readonly context = new FunctionContext(this._hf, this._space, this.refresh.bind(this), this._options);
133
+ public readonly update = new Event<{ type: ComputeGraphEvent }>();
130
134
 
131
- // TODO(burdon): Typed events.
132
- // TODO(burdon): Tie into HyperFormula dependency graph.
133
- // TODO(burdon): Event propagation.
134
- public readonly update = new Event();
135
+ // The context is passed to all functions.
136
+ public readonly context = new FunctionContext(this._hf, this._space, this._options);
135
137
 
136
138
  constructor(
137
139
  private readonly _hf: HyperFormula,
@@ -139,21 +141,36 @@ export class ComputeGraph extends Resource {
139
141
  private readonly _options?: Partial<FunctionContextOptions>,
140
142
  ) {
141
143
  super();
142
-
143
144
  this._hf.updateConfig({ context: this.context });
145
+ // TODO(burdon): If debounce then aggregate changes.
146
+ const onValuesUpdate: Listeners['valuesUpdated'] = (changes) => {
147
+ for (const change of changes) {
148
+ if (change instanceof ExportedCellChange) {
149
+ const { sheet } = change;
150
+ const node = this._nodes.get(sheet);
151
+ if (node) {
152
+ node.update.emit({ type: 'valuesUpdated', change });
153
+ }
154
+ }
155
+ }
156
+ };
157
+
158
+ this._hf.on('valuesUpdated', onValuesUpdate);
159
+ this._ctx.onDispose(() => this._hf.off('valuesUpdated', onValuesUpdate));
144
160
  }
145
161
 
146
- // TODO(burdon): Remove.
147
162
  get hf() {
148
163
  return this._hf;
149
164
  }
150
165
 
151
- refresh() {
152
- log('refresh', { id: this.id });
153
- this.update.emit();
154
- }
166
+ // refresh() {
167
+ // log('refresh', { id: this.id });
168
+ // this.update.emit();
169
+ // }
155
170
 
156
- getFunctions({ standard = true, echo = true }: { standard?: boolean; echo?: boolean } = {}): FunctionDefinition[] {
171
+ getFunctions(
172
+ { standard, echo }: { standard?: boolean; echo?: boolean } = { standard: true, echo: true },
173
+ ): FunctionDefinition[] {
157
174
  return [
158
175
  ...(standard
159
176
  ? this._hf
@@ -172,19 +189,19 @@ export class ComputeGraph extends Resource {
172
189
  // This would enable on-the-fly instantiation of new models when then are referenced.
173
190
  // E.g., Cross-object reference would be stored as "ObjectId!A1"
174
191
  // The graph would then load the object and create a ComputeNode (model) of the appropriate type.
175
- getOrCreateNode(name: string): ComputeNode {
192
+ async getOrCreateNode(name: string): Promise<ComputeNode> {
176
193
  invariant(name.length);
177
194
  if (!this._hf.doesSheetExist(name)) {
178
195
  log.info('created node', { space: this._space?.id, name });
179
196
  this._hf.addSheet(name);
180
- this.update.emit();
197
+ // this.update.emit();
181
198
  }
182
199
 
183
200
  const sheetId = this._hf.getSheetId(name);
184
201
  invariant(sheetId !== undefined);
185
202
 
186
- // TODO(burdon): Chain context?
187
203
  const node = new ComputeNode(this, sheetId);
204
+ await node.open();
188
205
  this._nodes.set(sheetId, node);
189
206
  return node;
190
207
  }
@@ -281,10 +298,16 @@ export class ComputeGraph extends Resource {
281
298
  const query = this._space.db.query(Filter.schema(FunctionType));
282
299
  const unsubscribe = query.subscribe(({ objects }) => {
283
300
  this._functions = objects.filter(({ binding }) => binding);
284
- this.update.emit();
301
+ this.update.emit({ type: 'functionsUpdated' });
285
302
  });
286
303
 
287
304
  this._ctx.onDispose(unsubscribe);
288
305
  }
289
306
  }
307
+
308
+ protected override async _close() {
309
+ for (const node of this._nodes.values()) {
310
+ await node.close();
311
+ }
312
+ }
290
313
  }
@@ -2,6 +2,9 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import { type Listeners } from 'hyperformula/typings/Emitter';
6
+ import { type ExportedCellChange } from 'hyperformula/typings/Exporter';
7
+
5
8
  import { Event } from '@dxos/async';
6
9
  import { Resource } from '@dxos/context';
7
10
 
@@ -10,13 +13,17 @@ import { type ComputeGraph } from './compute-graph';
10
13
  import { type CellAddress } from '../defs';
11
14
  import { type CellScalarValue } from '../types';
12
15
 
16
+ export type ComputeNodeEvent = {
17
+ type: keyof Listeners;
18
+ change?: ExportedCellChange;
19
+ };
20
+
13
21
  /**
14
22
  * Individual "sheet" (typically corresponds to an ECHO object).
15
23
  */
16
24
  // TODO(burdon): Factor out common HF wrapper from from SheetModel.
17
25
  export class ComputeNode extends Resource {
18
- // TODO(burdon): Chaing events.
19
- public readonly update = new Event();
26
+ public readonly update = new Event<ComputeNodeEvent>();
20
27
 
21
28
  constructor(
22
29
  private readonly _graph: ComputeGraph,
@@ -25,13 +32,12 @@ export class ComputeNode extends Resource {
25
32
  super();
26
33
  }
27
34
 
28
- // TODO(burdon): Remove?
29
35
  get graph() {
30
36
  return this._graph;
31
37
  }
32
38
 
33
- get hf() {
34
- return this._graph.hf;
39
+ clear() {
40
+ this._graph.hf.clearSheet(this.sheetId);
35
41
  }
36
42
 
37
43
  getValue(cell: CellAddress): CellScalarValue {
@@ -48,4 +54,9 @@ export class ComputeNode extends Resource {
48
54
  typeof value === 'string' && value.charAt(0) === '=' ? this._graph.mapFormulaToNative(value) : value;
49
55
  this._graph.hf.setCellContents({ sheet: this.sheetId, row: cell.row, col: cell.col }, [[mappedValue]]);
50
56
  }
57
+
58
+ protected override async _open() {
59
+ // const unsubscribe = this._graph.update.on(this.update.emit);
60
+ // this._ctx.onDispose(unsubscribe);
61
+ }
51
62
  }
@@ -74,8 +74,7 @@ EdgeFunctionPlugin.implementedFunctions = {
74
74
  // Binding
75
75
  { argumentType: FunctionArgumentType.STRING },
76
76
 
77
- // Remote function arguments (currently supporting up to 9).
78
- { argumentType: FunctionArgumentType.ANY, optionalArg: true },
77
+ // Remote function arguments (currently supporting up to 8).
79
78
  { argumentType: FunctionArgumentType.ANY, optionalArg: true },
80
79
  { argumentType: FunctionArgumentType.ANY, optionalArg: true },
81
80
  { argumentType: FunctionArgumentType.ANY, optionalArg: true },
@@ -8,8 +8,9 @@ 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 { parseNumberString } from './util';
11
+ import { type AsyncFunction, FunctionPluginAsync } from '../async-function';
12
+ import { type ComputeGraphPlugin } from '../compute-graph';
13
+ import { parseNumberString } from '../util';
13
14
 
14
15
  /**
15
16
  * https://hyperformula.handsontable.com/guide/custom-functions.html#add-a-simple-custom-function
@@ -64,3 +65,10 @@ export const CustomPluginTranslations = {
64
65
  CRYPTO: 'CRYPTO',
65
66
  },
66
67
  };
68
+
69
+ export const testPlugins: ComputeGraphPlugin[] = [
70
+ {
71
+ plugin: CustomPlugin,
72
+ translations: CustomPluginTranslations,
73
+ },
74
+ ];
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './custom-function';
@@ -14,14 +14,14 @@ import { withTheme } from '@dxos/storybook-utils';
14
14
  import { ComputeGraphContextProvider } from '../components';
15
15
  import { createSheet } from '../defs';
16
16
  import { useComputeGraph, useSheetModel } from '../hooks';
17
- import { withGraphDecorator } from '../testing';
17
+ import { withComputeGraphDecorator } from '../testing';
18
18
  import { SheetType } from '../types';
19
19
 
20
20
  const Story = () => {
21
21
  const space = useSpace();
22
22
  const graph = useComputeGraph(space);
23
23
  const [sheet, setSheet] = useState<SheetType>();
24
- const model = useSheetModel(space, sheet);
24
+ const model = useSheetModel(graph, sheet);
25
25
  useEffect(() => {
26
26
  if (space) {
27
27
  const sheet = space.db.add(createSheet());
@@ -41,7 +41,7 @@ export default {
41
41
  component: ComputeGraphContextProvider,
42
42
  decorators: [
43
43
  withClientProvider({ types: [SheetType], createIdentity: true, createSpace: true }),
44
- withGraphDecorator,
44
+ withComputeGraphDecorator(),
45
45
  withTheme,
46
46
  ],
47
47
  render: (args: any) => <Story {...args} />,
@@ -16,5 +16,6 @@ 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
- return useAsyncState(async () => space && registry.getOrCreateGraph(space), [space, registry]);
19
+ const [graph] = useAsyncState(async () => space && registry.getOrCreateGraph(space), [space, registry]);
20
+ return graph;
20
21
  };
@@ -4,9 +4,7 @@
4
4
 
5
5
  import { useEffect, useState } from 'react';
6
6
 
7
- import { type Space } from '@dxos/react-client/echo';
8
-
9
- import { useComputeGraph } from './useComputeGraph';
7
+ import { type ComputeGraph } from '../graph';
10
8
  import { SheetModel } from '../model';
11
9
  import { type SheetType } from '../types';
12
10
 
@@ -15,14 +13,13 @@ export type UseSheetModelOptions = {
15
13
  };
16
14
 
17
15
  export const useSheetModel = (
18
- space?: Space,
16
+ graph?: ComputeGraph,
19
17
  sheet?: SheetType,
20
18
  { readonly }: UseSheetModelOptions = {},
21
19
  ): SheetModel | undefined => {
22
- const graph = useComputeGraph(space);
23
20
  const [model, setModel] = useState<SheetModel>();
24
21
  useEffect(() => {
25
- if (!space || !graph || !sheet) {
22
+ if (!graph || !sheet) {
26
23
  return;
27
24
  }
28
25
 
@@ -37,7 +34,7 @@ export const useSheetModel = (
37
34
  clearTimeout(t);
38
35
  void model?.close();
39
36
  };
40
- }, [space, sheet, graph, readonly]);
37
+ }, [graph, sheet, readonly]);
41
38
 
42
39
  return model;
43
40
  };