@dxos/plugin-space 0.7.1 → 0.7.2-main.f1adc9f

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.
@@ -27,7 +27,7 @@ import {
27
27
  resolvePlugin,
28
28
  } from '@dxos/app-framework';
29
29
  import { EventSubscriptions, type Trigger, type UnsubscribeCallback } from '@dxos/async';
30
- import { type HasId, isReactiveObject } from '@dxos/echo-schema';
30
+ import { type HasId, isDeleted, isReactiveObject } from '@dxos/echo-schema';
31
31
  import { scheduledEffect } from '@dxos/echo-signals/core';
32
32
  import { invariant } from '@dxos/invariant';
33
33
  import { LocalStorageStore } from '@dxos/local-storage';
@@ -39,7 +39,7 @@ import { type Node, createExtension, memoize, toSignal } from '@dxos/plugin-grap
39
39
  import { ObservabilityAction } from '@dxos/plugin-observability/meta';
40
40
  import { type Client, PublicKey } from '@dxos/react-client';
41
41
  import {
42
- type EchoReactiveObject,
42
+ type ReactiveEchoObject,
43
43
  Expando,
44
44
  Filter,
45
45
  type PropertiesTypeProps,
@@ -54,6 +54,9 @@ import {
54
54
  loadObjectReferences,
55
55
  parseId,
56
56
  FQ_ID_LENGTH,
57
+ SPACE_ID_LENGTH,
58
+ OBJECT_ID_LENGTH,
59
+ parseFullyQualifiedId,
57
60
  } from '@dxos/react-client/echo';
58
61
  import { type JoinPanelProps, osTranslations } from '@dxos/shell/react';
59
62
  import { ComplexMap, nonNullable, reduceGroupBy } from '@dxos/util';
@@ -62,7 +65,6 @@ import {
62
65
  AwaitingObject,
63
66
  CollectionMain,
64
67
  CollectionSection,
65
- DefaultObjectSettings,
66
68
  JoinDialog,
67
69
  MenuFooter,
68
70
  PopoverRenameObject,
@@ -76,6 +78,7 @@ import {
76
78
  SyncStatus,
77
79
  SpaceSettingsDialog,
78
80
  type SpaceSettingsDialogProps,
81
+ DefaultObjectSettings,
79
82
  } from './components';
80
83
  import meta, { SPACE_PLUGIN, SpaceAction } from './meta';
81
84
  import translations from './translations';
@@ -252,17 +255,22 @@ export const SpacePlugin = ({
252
255
  subscriptions.add(
253
256
  scheduledEffect(
254
257
  () => ({
255
- ids: openIds(location.active),
256
- removed: location.closed ? [location.closed].flat() : [],
258
+ open: openIds(location.active, layout.layoutMode === 'solo' ? ['solo'] : ['main']),
259
+ closed: [...location.closed],
257
260
  }),
258
- ({ ids, removed }) => {
261
+ ({ open, closed }) => {
259
262
  const send = () => {
260
263
  const spaces = client.spaces.get();
261
264
  const identity = client.halo.identity.get();
262
265
  if (identity && location.active) {
263
266
  // Group parts by space for efficient messaging.
264
- const idsBySpace = reduceGroupBy(ids, (id) => {
265
- const [spaceId] = id.split(':'); // TODO(burdon): Factor out.
267
+ const idsBySpace = reduceGroupBy(open, (id) => {
268
+ const [spaceId] = parseFullyQualifiedId(id);
269
+ return spaceId;
270
+ });
271
+
272
+ const removedBySpace = reduceGroupBy(closed, (id) => {
273
+ const [spaceId] = parseFullyQualifiedId(id);
266
274
  return spaceId;
267
275
  });
268
276
 
@@ -273,7 +281,8 @@ export const SpacePlugin = ({
273
281
  }
274
282
  }
275
283
 
276
- for (const [spaceId, ids] of idsBySpace) {
284
+ for (const [spaceId, added] of idsBySpace) {
285
+ const removed = removedBySpace.get(spaceId) ?? [];
277
286
  const space = spaces.find((space) => space.id === spaceId);
278
287
  if (!space) {
279
288
  continue;
@@ -283,9 +292,8 @@ export const SpacePlugin = ({
283
292
  .postMessage('viewing', {
284
293
  identityKey: identity.identityKey.toHex(),
285
294
  attended: attention.attended ? [...attention.attended] : [],
286
- added: ids,
287
- // TODO(Zan): When we re-open a part, we should remove it from the removed list in the navigation plugin.
288
- removed: removed.filter((id) => !ids.includes(id)),
295
+ added,
296
+ removed,
289
297
  })
290
298
  // TODO(burdon): This seems defensive; why would this fail? Backoff interval.
291
299
  .catch((err) => {
@@ -296,6 +304,7 @@ export const SpacePlugin = ({
296
304
  };
297
305
 
298
306
  send();
307
+ // Send at interval to allow peers to expire entries if they become disconnected.
299
308
  const interval = setInterval(() => send(), ACTIVE_NODE_BROADCAST_INTERVAL);
300
309
  return () => clearInterval(interval);
301
310
  },
@@ -312,7 +321,13 @@ export const SpacePlugin = ({
312
321
  const { added, removed, attended } = message.payload;
313
322
 
314
323
  const identityKey = PublicKey.safeFrom(message.payload.identityKey);
315
- if (identityKey && Array.isArray(added) && Array.isArray(removed)) {
324
+ const currentIdentity = client.halo.identity.get();
325
+ if (
326
+ identityKey &&
327
+ !currentIdentity?.identityKey.equals(identityKey) &&
328
+ Array.isArray(added) &&
329
+ Array.isArray(removed)
330
+ ) {
316
331
  added.forEach((id) => {
317
332
  if (typeof id === 'string') {
318
333
  if (!(id in state.values.viewersByObject)) {
@@ -571,6 +586,7 @@ export const SpacePlugin = ({
571
586
  {
572
587
  id: SPACES,
573
588
  type: SPACES,
589
+ cacheable: ['label', 'role'],
574
590
  properties: {
575
591
  label: ['spaces label', { ns: SPACE_PLUGIN }],
576
592
  testId: 'spacePlugin.spaces',
@@ -679,39 +695,41 @@ export const SpacePlugin = ({
679
695
  );
680
696
  } catch {}
681
697
  },
682
- }),
683
-
684
- // Find an object by its fully qualified id.
685
- createExtension({
686
- id: `${SPACE_PLUGIN}/objects`,
687
698
  resolver: ({ id }) => {
688
- const [spaceId, objectId] = id.split(':');
689
- const space = client.spaces.get().find((space) => space.id === spaceId);
690
- if (!space) {
699
+ if (id.length !== SPACE_ID_LENGTH) {
691
700
  return;
692
701
  }
693
702
 
694
- const spaceState = toSignal(
695
- (onChange) => space.state.subscribe(() => onChange()).unsubscribe,
696
- () => space.state.get(),
697
- space.id,
703
+ const spaces = toSignal(
704
+ (onChange) => client.spaces.subscribe(() => onChange()).unsubscribe,
705
+ () => client.spaces.get(),
698
706
  );
699
- if (spaceState !== SpaceState.SPACE_READY) {
707
+
708
+ const isReady = toSignal(
709
+ (onChange) => client.spaces.isReady.subscribe(() => onChange()).unsubscribe,
710
+ () => client.spaces.isReady.get(),
711
+ );
712
+
713
+ if (!spaces || !isReady) {
700
714
  return;
701
715
  }
702
716
 
703
- const store = memoize(() => signal(space.db.getObjectById(objectId)), id);
704
- memoize(() => {
705
- if (!store.value) {
706
- void space.db.loadObjectById(objectId).then((o) => (store.value = o));
707
- }
708
- }, id);
709
- const object = store.value;
710
- if (!object) {
717
+ const space = spaces.find((space) => space.id === id);
718
+ if (!space) {
711
719
  return;
712
720
  }
713
721
 
714
- return createObjectNode({ object, space, resolve, navigable: state.values.navigableCollections });
722
+ if (space.state.get() === SpaceState.SPACE_INACTIVE) {
723
+ return false;
724
+ } else {
725
+ return constructSpaceNode({
726
+ space,
727
+ navigable: state.values.navigableCollections,
728
+ personal: space === client.spaces.default,
729
+ namesCache: state.values.spaceNames,
730
+ resolve,
731
+ });
732
+ }
715
733
  },
716
734
  }),
717
735
 
@@ -765,22 +783,9 @@ export const SpacePlugin = ({
765
783
  },
766
784
  }),
767
785
 
768
- // Create collection actions and action groups.
786
+ // Create nodes for objects in a collection or by its fully qualified id.
769
787
  createExtension({
770
- id: `${SPACE_PLUGIN}/object-actions`,
771
- filter: (node): node is Node<EchoReactiveObject<any>> => isEchoObject(node.data),
772
- actionGroups: ({ node }) =>
773
- constructObjectActionGroups({
774
- object: node.data,
775
- dispatch,
776
- navigable: state.values.navigableCollections,
777
- }),
778
- actions: ({ node }) => constructObjectActions({ node, dispatch }),
779
- }),
780
-
781
- // Create nodes for objects in collections.
782
- createExtension({
783
- id: `${SPACE_PLUGIN}/collection-objects`,
788
+ id: `${SPACE_PLUGIN}/objects`,
784
789
  filter: (node): node is Node<CollectionType> => node.data instanceof CollectionType,
785
790
  connector: ({ node }) => {
786
791
  const collection = node.data;
@@ -796,6 +801,64 @@ export const SpacePlugin = ({
796
801
  )
797
802
  .filter(nonNullable);
798
803
  },
804
+ resolver: ({ id }) => {
805
+ if (id.length !== FQ_ID_LENGTH) {
806
+ return;
807
+ }
808
+
809
+ const [spaceId, objectId] = id.split(':');
810
+ if (spaceId.length !== SPACE_ID_LENGTH && objectId.length !== OBJECT_ID_LENGTH) {
811
+ return;
812
+ }
813
+
814
+ const space = client.spaces.get().find((space) => space.id === spaceId);
815
+ if (!space) {
816
+ return;
817
+ }
818
+
819
+ const spaceState = toSignal(
820
+ (onChange) => space.state.subscribe(() => onChange()).unsubscribe,
821
+ () => space.state.get(),
822
+ space.id,
823
+ );
824
+ if (spaceState !== SpaceState.SPACE_READY) {
825
+ return;
826
+ }
827
+
828
+ const store = memoize(() => signal(space.db.getObjectById(objectId)), id);
829
+ memoize(() => {
830
+ if (!store.value) {
831
+ void space.db
832
+ .query({ id: objectId })
833
+ .first()
834
+ .then((o) => (store.value = o))
835
+ .catch((err) => log.catch(err, { objectId }));
836
+ }
837
+ }, id);
838
+ const object = store.value;
839
+ if (!object) {
840
+ return;
841
+ }
842
+
843
+ if (isDeleted(object)) {
844
+ return false;
845
+ } else {
846
+ return createObjectNode({ object, space, resolve, navigable: state.values.navigableCollections });
847
+ }
848
+ },
849
+ }),
850
+
851
+ // Create collection actions and action groups.
852
+ createExtension({
853
+ id: `${SPACE_PLUGIN}/object-actions`,
854
+ filter: (node): node is Node<ReactiveEchoObject<any>> => isEchoObject(node.data),
855
+ actionGroups: ({ node }) =>
856
+ constructObjectActionGroups({
857
+ object: node.data,
858
+ dispatch,
859
+ navigable: state.values.navigableCollections,
860
+ }),
861
+ actions: ({ node }) => constructObjectActions({ node, dispatch }),
799
862
  }),
800
863
 
801
864
  // Create nodes for object settings.
@@ -812,7 +875,13 @@ export const SpacePlugin = ({
812
875
 
813
876
  const [subjectId] = id.split('~');
814
877
  const { spaceId, objectId } = parseId(subjectId);
815
- const space = client.spaces.get().find((space) => space.id === spaceId);
878
+ const spaces = toSignal(
879
+ (onChange) => client.spaces.subscribe(() => onChange()).unsubscribe,
880
+ () => client.spaces.get(),
881
+ );
882
+ const space = spaces?.find(
883
+ (space) => space.id === spaceId && space.state.get() === SpaceState.SPACE_READY,
884
+ );
816
885
  if (!objectId) {
817
886
  const label = space
818
887
  ? space.properties.name || ['unnamed space label', { ns: SPACE_PLUGIN }]
@@ -834,18 +903,7 @@ export const SpacePlugin = ({
834
903
  };
835
904
  }
836
905
 
837
- const object = toSignal(
838
- (onChange) => {
839
- const timeout = setTimeout(async () => {
840
- await space?.db.loadObjectById(objectId);
841
- onChange();
842
- });
843
-
844
- return () => clearTimeout(timeout);
845
- },
846
- () => space?.db.getObjectById(objectId),
847
- subjectId,
848
- );
906
+ const [object] = memoizeQuery(space, { id: objectId });
849
907
  if (!object || !subjectId) {
850
908
  return;
851
909
  }
@@ -4,13 +4,13 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
- import { type EchoReactiveObject } from '@dxos/react-client/echo';
7
+ import { type ReactiveEchoObject } from '@dxos/react-client/echo';
8
8
  import { Input, useTranslation } from '@dxos/react-ui';
9
9
 
10
10
  import { SPACE_PLUGIN } from '../meta';
11
11
 
12
12
  export type DefaultObjectSettingsProps = {
13
- object: EchoReactiveObject<any>;
13
+ object: ReactiveEchoObject<any>;
14
14
  };
15
15
 
16
16
  export const DefaultObjectSettings = ({ object }: DefaultObjectSettingsProps) => {
@@ -5,14 +5,14 @@
5
5
  import { Planet } from '@phosphor-icons/react';
6
6
  import React from 'react';
7
7
 
8
- import { type EchoReactiveObject, getSpace } from '@dxos/client/echo';
8
+ import { type ReactiveEchoObject, getSpace } from '@dxos/client/echo';
9
9
  import { useClient } from '@dxos/react-client';
10
10
  import { DropdownMenu, toLocalizedString, useTranslation } from '@dxos/react-ui';
11
11
 
12
12
  import { SPACE_PLUGIN } from '../meta';
13
13
  import { getSpaceDisplayName } from '../util';
14
14
 
15
- export const MenuFooter = ({ object }: { object: EchoReactiveObject<any> }) => {
15
+ export const MenuFooter = ({ object }: { object: ReactiveEchoObject<any> }) => {
16
16
  const { t } = useTranslation(SPACE_PLUGIN);
17
17
  const client = useClient();
18
18
  const space = getSpace(object);
package/src/util.tsx CHANGED
@@ -33,12 +33,13 @@ import {
33
33
  isEchoObject,
34
34
  isSpace,
35
35
  type Echo,
36
- type EchoReactiveObject,
36
+ type ReactiveEchoObject,
37
37
  type FilterSource,
38
38
  type Query,
39
39
  type QueryOptions,
40
40
  type Space,
41
41
  SpaceState,
42
+ Filter,
42
43
  } from '@dxos/react-client/echo';
43
44
 
44
45
  import { SpaceAction, SPACE_PLUGIN } from './meta';
@@ -59,12 +60,16 @@ const EMPTY_ARRAY: never[] = [];
59
60
  * @param options
60
61
  * @returns
61
62
  */
62
- export const memoizeQuery = <T extends EchoReactiveObject<any>>(
63
+ export const memoizeQuery = <T extends ReactiveEchoObject<any>>(
63
64
  spaceOrEcho?: Space | Echo,
64
65
  filter?: FilterSource<T>,
65
66
  options?: QueryOptions,
66
67
  ): T[] => {
67
- const key = isSpace(spaceOrEcho) ? spaceOrEcho.id : undefined;
68
+ const key = JSON.stringify({
69
+ space: isSpace(spaceOrEcho) ? spaceOrEcho.id : undefined,
70
+ filter: Filter.from(filter).toProto(),
71
+ });
72
+
68
73
  const query = memoize(
69
74
  () =>
70
75
  isSpace(spaceOrEcho)
@@ -111,7 +116,7 @@ const getCollectionGraphNodePartials = ({
111
116
  // Change on disk.
112
117
  collection.objects = nextOrder.filter(isEchoObject);
113
118
  },
114
- onTransferStart: (child: Node<EchoReactiveObject<any>>, index?: number) => {
119
+ onTransferStart: (child: Node<ReactiveEchoObject<any>>, index?: number) => {
115
120
  // TODO(wittjosiah): Support transfer between spaces.
116
121
  // const childSpace = getSpace(child.data);
117
122
  // if (space && childSpace && !childSpace.key.equals(space.key)) {
@@ -139,7 +144,7 @@ const getCollectionGraphNodePartials = ({
139
144
 
140
145
  // }
141
146
  },
142
- onTransferEnd: (child: Node<EchoReactiveObject<any>>, destination: Node) => {
147
+ onTransferEnd: (child: Node<ReactiveEchoObject<any>>, destination: Node) => {
143
148
  // Remove child from origin collection.
144
149
  const index = collection.objects.indexOf(child.data);
145
150
  if (index > -1) {
@@ -155,7 +160,7 @@ const getCollectionGraphNodePartials = ({
155
160
  // childSpace.db.remove(child.data);
156
161
  // }
157
162
  },
158
- onCopy: async (child: Node<EchoReactiveObject<any>>, index?: number) => {
163
+ onCopy: async (child: Node<ReactiveEchoObject<any>>, index?: number) => {
159
164
  // Create clone of child and add to destination space.
160
165
  const newObject = await cloneObject(child.data, resolve, space);
161
166
  space.db.add(newObject);
@@ -200,6 +205,7 @@ export const constructSpaceNode = ({
200
205
  return {
201
206
  id: space.id,
202
207
  type: SPACE_TYPE,
208
+ cacheable: ['label', 'icon', 'role'],
203
209
  data: space,
204
210
  properties: {
205
211
  ...partials,
@@ -412,7 +418,7 @@ export const createObjectNode = ({
412
418
  navigable = false,
413
419
  resolve,
414
420
  }: {
415
- object: EchoReactiveObject<any>;
421
+ object: ReactiveEchoObject<any>;
416
422
  space: Space;
417
423
  navigable?: boolean;
418
424
  resolve: MetadataResolver;
@@ -435,6 +441,7 @@ export const createObjectNode = ({
435
441
  return {
436
442
  id: fullyQualifiedId(object),
437
443
  type,
444
+ cacheable: ['label', 'icon', 'role'],
438
445
  data: object,
439
446
  properties: {
440
447
  ...partials,
@@ -454,7 +461,7 @@ export const constructObjectActionGroups = ({
454
461
  navigable,
455
462
  dispatch,
456
463
  }: {
457
- object: EchoReactiveObject<any>;
464
+ object: ReactiveEchoObject<any>;
458
465
  navigable: boolean;
459
466
  dispatch: IntentDispatcher;
460
467
  }) => {
@@ -512,7 +519,7 @@ export const constructObjectActions = ({
512
519
  node,
513
520
  dispatch,
514
521
  }: {
515
- node: Node<EchoReactiveObject<any>>;
522
+ node: Node<ReactiveEchoObject<any>>;
516
523
  dispatch: IntentDispatcher;
517
524
  }) => {
518
525
  const object = node.data;
@@ -601,9 +608,9 @@ export const getActiveSpace = (graph: Graph, active?: string) => {
601
608
  * @deprecated This is a temporary solution.
602
609
  */
603
610
  export const getNestedObjects = async (
604
- object: EchoReactiveObject<any>,
611
+ object: ReactiveEchoObject<any>,
605
612
  resolve: MetadataResolver,
606
- ): Promise<EchoReactiveObject<any>[]> => {
613
+ ): Promise<ReactiveEchoObject<any>[]> => {
607
614
  const type = getTypename(object);
608
615
  if (!type) {
609
616
  return [];
@@ -615,7 +622,7 @@ export const getNestedObjects = async (
615
622
  return [];
616
623
  }
617
624
 
618
- const objects: EchoReactiveObject<any>[] = await loadReferences(object);
625
+ const objects: ReactiveEchoObject<any>[] = await loadReferences(object);
619
626
  const nested = await Promise.all(objects.map((object) => getNestedObjects(object, resolve)));
620
627
  return [...objects, ...nested.flat()];
621
628
  };