@dxos/echo-atom 0.8.4-main.3eb6e50203 → 0.8.4-main.40e3dcdf1b

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/echo-atom",
3
- "version": "0.8.4-main.3eb6e50203",
3
+ "version": "0.8.4-main.40e3dcdf1b",
4
4
  "description": "Effect Atom wrappers for ECHO objects with explicit subscriptions.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -29,17 +29,15 @@
29
29
  ],
30
30
  "dependencies": {
31
31
  "@effect-atom/atom": "^0.5.1",
32
- "lodash.isequal": "^4.5.0",
33
- "@dxos/echo": "0.8.4-main.3eb6e50203",
34
- "@dxos/echo-db": "0.8.4-main.3eb6e50203",
35
- "@dxos/invariant": "0.8.4-main.3eb6e50203",
36
- "@dxos/util": "0.8.4-main.3eb6e50203"
32
+ "@dxos/echo": "0.8.4-main.40e3dcdf1b",
33
+ "@dxos/echo-db": "0.8.4-main.40e3dcdf1b",
34
+ "@dxos/invariant": "0.8.4-main.40e3dcdf1b",
35
+ "@dxos/util": "0.8.4-main.40e3dcdf1b"
37
36
  },
38
37
  "devDependencies": {
39
- "@types/lodash.isequal": "^4.5.0",
40
38
  "effect": "3.19.16",
41
- "@dxos/test-utils": "0.8.4-main.3eb6e50203",
42
- "@dxos/random": "0.8.4-main.3eb6e50203"
39
+ "@dxos/random": "0.8.4-main.40e3dcdf1b",
40
+ "@dxos/test-utils": "0.8.4-main.40e3dcdf1b"
43
41
  },
44
42
  "peerDependencies": {
45
43
  "effect": "3.19.16"
package/src/atom.test.ts CHANGED
@@ -357,7 +357,7 @@ describe('AtomObj.makeWithReactive', () => {
357
357
  tasks: [Ref.make(task)],
358
358
  }),
359
359
  );
360
- await db.flush({ indexes: true });
360
+ await db.flush();
361
361
 
362
362
  const ref = person.tasks![0];
363
363
  const registry = Registry.make();
@@ -381,7 +381,7 @@ describe('AtomObj.makeWithReactive', () => {
381
381
  tasks: [Ref.make(task)],
382
382
  }),
383
383
  );
384
- await db.flush({ indexes: true });
384
+ await db.flush();
385
385
 
386
386
  const ref = person.tasks![0];
387
387
  const registry = Registry.make();
@@ -390,7 +390,7 @@ describe('AtomObj.makeWithReactive', () => {
390
390
  expect(registry.get(atom)).toBe(task);
391
391
 
392
392
  db.remove(task);
393
- await db.flush({ indexes: true });
393
+ await db.flush();
394
394
 
395
395
  expect(registry.get(atom)).toBeUndefined();
396
396
  });
package/src/atom.ts CHANGED
@@ -7,9 +7,8 @@ import * as Result from '@effect-atom/atom/Result';
7
7
  import * as Effect from 'effect/Effect';
8
8
  import * as Function from 'effect/Function';
9
9
  import * as Option from 'effect/Option';
10
- import isEqual from 'lodash.isequal';
11
10
 
12
- import { Obj, Ref } from '@dxos/echo';
11
+ import { type Entity, Obj, Ref, Relation } from '@dxos/echo';
13
12
  import { assertArgument } from '@dxos/invariant';
14
13
 
15
14
  import { loadRefTarget } from './ref-utils';
@@ -27,7 +26,7 @@ const objectFamily = Atom.family(<T extends Obj.Unknown>(obj: T): Atom.Atom<Obj.
27
26
  get.addFinalizer(() => unsubscribe());
28
27
 
29
28
  return Obj.getSnapshot(obj);
30
- });
29
+ }).pipe(Atom.keepAlive);
31
30
  });
32
31
 
33
32
  /**
@@ -52,7 +51,7 @@ const refFamily = Atom.family(<T extends Obj.Unknown>(ref: Ref.Ref<T>): Atom.Ato
52
51
  });
53
52
 
54
53
  return loadRefTarget(ref, get, setupTargetSubscription);
55
- });
54
+ }).pipe(Atom.keepAlive);
56
55
  });
57
56
 
58
57
  /**
@@ -84,7 +83,7 @@ const propertyFamily = Atom.family(<T extends Obj.Unknown>(obj: T) =>
84
83
 
85
84
  const unsubscribe = Obj.subscribe(obj, () => {
86
85
  const newValue = obj[key];
87
- if (!isEqual(previousSnapshot, newValue)) {
86
+ if (previousSnapshot !== newValue) {
88
87
  previousSnapshot = snapshotForComparison(newValue);
89
88
  // Return a snapshot copy so React sees a new reference.
90
89
  get.setSelf(snapshotForComparison(newValue));
@@ -95,7 +94,7 @@ const propertyFamily = Atom.family(<T extends Obj.Unknown>(obj: T) =>
95
94
 
96
95
  // Return a snapshot copy so React sees a new reference.
97
96
  return snapshotForComparison(obj[key]);
98
- });
97
+ }).pipe(Atom.keepAlive);
99
98
  }),
100
99
  );
101
100
 
@@ -111,27 +110,30 @@ const propertyFamily = Atom.family(<T extends Obj.Unknown>(obj: T) =>
111
110
  * @returns An atom that returns the object snapshot (plain data). Returns undefined only for refs (async loading) or undefined input.
112
111
  */
113
112
  export function make<T extends Obj.Unknown>(obj: T): Atom.Atom<Obj.Snapshot<T>>;
113
+ export function make<T extends Relation.Unknown>(relation: T): Atom.Atom<Relation.Snapshot<T>>;
114
+ export function make<T extends Entity.Unknown>(entity: T): Atom.Atom<Entity.Snapshot>;
114
115
  export function make<T extends Obj.Unknown>(ref: Ref.Ref<T>): Atom.Atom<Obj.Snapshot<T> | undefined>;
115
116
  export function make<T extends Obj.Unknown>(
116
117
  objOrRef: T | Ref.Ref<T> | undefined,
117
118
  ): Atom.Atom<Obj.Snapshot<T> | undefined>;
118
- export function make<T extends Obj.Unknown>(
119
+ export function make<T extends Entity.Unknown>(
119
120
  objOrRef: T | Ref.Ref<T> | undefined,
120
- ): Atom.Atom<Obj.Snapshot<T> | undefined> {
121
+ ): Atom.Atom<Entity.Snapshot | undefined> {
121
122
  if (objOrRef === undefined) {
122
- return Atom.make<Obj.Snapshot<T> | undefined>(() => undefined);
123
+ return Atom.make<Entity.Snapshot | undefined>(() => undefined);
123
124
  }
124
125
 
125
126
  // Handle Ref inputs.
126
127
  if (Ref.isRef(objOrRef)) {
127
- return refFamily(objOrRef as Ref.Ref<T>);
128
+ return refFamily(objOrRef as any);
128
129
  }
129
130
 
130
131
  // At this point, objOrRef is definitely T (not a Ref).
131
132
  const obj = objOrRef as T;
132
- assertArgument(Obj.isObject(obj), 'obj', 'Object must be a reactive object');
133
+ assertArgument(Obj.isObject(obj) || Relation.isRelation(obj), 'obj', 'Object must be a reactive object');
133
134
 
134
- return objectFamily(obj);
135
+ // TODO(dmaretskyi): Fix echo types during review.
136
+ return objectFamily(obj as any);
135
137
  }
136
138
 
137
139
  /**
@@ -176,7 +178,7 @@ const objectWithReactiveFamily = Atom.family(<T extends Obj.Unknown>(obj: T): At
176
178
  get.addFinalizer(() => unsubscribe());
177
179
 
178
180
  return obj;
179
- });
181
+ }).pipe(Atom.keepAlive);
180
182
  });
181
183
 
182
184
  /**
@@ -7,8 +7,9 @@ import * as Schema from 'effect/Schema';
7
7
  import { afterEach, beforeEach, describe, expect, test } from 'vitest';
8
8
 
9
9
  import { Obj, type QueryResult, Type } from '@dxos/echo';
10
+ import { Filter, Query } from '@dxos/echo';
10
11
  import { TestSchema } from '@dxos/echo/testing';
11
- import { type EchoDatabase, Filter, Query } from '@dxos/echo-db';
12
+ import { type EchoDatabase } from '@dxos/echo-db';
12
13
  import { EchoTestBuilder } from '@dxos/echo-db/testing';
13
14
  import { SpaceId } from '@dxos/keys';
14
15
 
@@ -22,7 +23,7 @@ const TestItem = Schema.Struct({
22
23
  value: Schema.Number,
23
24
  }).pipe(
24
25
  Type.object({
25
- typename: 'example.com/type/TestItem',
26
+ typename: 'com.example.type.test-item',
26
27
  version: '0.1.0',
27
28
  }),
28
29
  );
@@ -47,7 +48,7 @@ describe('AtomQuery', () => {
47
48
  test('creates atom with initial results', async () => {
48
49
  db.add(Obj.make(TestItem, { name: 'Object 1', value: 100 }));
49
50
  db.add(Obj.make(TestItem, { name: 'Object 2', value: 100 }));
50
- await db.flush({ indexes: true });
51
+ await db.flush();
51
52
 
52
53
  const queryResult: QueryResult.QueryResult<TestItem> = db.query(
53
54
  Query.select(Filter.type(TestItem, { value: 100 })),
@@ -63,7 +64,7 @@ describe('AtomQuery', () => {
63
64
 
64
65
  test('registry.subscribe fires on QueryResult changes', async () => {
65
66
  db.add(Obj.make(TestItem, { name: 'Initial', value: 200 }));
66
- await db.flush({ indexes: true });
67
+ await db.flush();
67
68
 
68
69
  const queryResult: QueryResult.QueryResult<TestItem> = db.query(
69
70
  Query.select(Filter.type(TestItem, { value: 200 })),
@@ -86,7 +87,7 @@ describe('AtomQuery', () => {
86
87
 
87
88
  // Add a new object that matches the query.
88
89
  db.add(Obj.make(TestItem, { name: 'New Object', value: 200 }));
89
- await db.flush({ indexes: true, updates: true });
90
+ await db.flush({ updates: true });
90
91
 
91
92
  // Subscription should have fired.
92
93
  expect(updateCount).toBeGreaterThan(0);
@@ -96,7 +97,7 @@ describe('AtomQuery', () => {
96
97
  test('registry.subscribe fires when objects are removed', async () => {
97
98
  const obj1 = db.add(Obj.make(TestItem, { name: 'Object 1', value: 300 }));
98
99
  db.add(Obj.make(TestItem, { name: 'Object 2', value: 300 }));
99
- await db.flush({ indexes: true });
100
+ await db.flush();
100
101
 
101
102
  const queryResult: QueryResult.QueryResult<TestItem> = db.query(
102
103
  Query.select(Filter.type(TestItem, { value: 300 })),
@@ -119,7 +120,7 @@ describe('AtomQuery', () => {
119
120
 
120
121
  // Remove an object.
121
122
  db.remove(obj1);
122
- await db.flush({ indexes: true, updates: true });
123
+ await db.flush({ updates: true });
123
124
 
124
125
  // Subscription should have fired.
125
126
  expect(updateCount).toBeGreaterThan(0);
@@ -129,7 +130,7 @@ describe('AtomQuery', () => {
129
130
 
130
131
  test('unsubscribing from registry stops receiving updates', async () => {
131
132
  db.add(Obj.make(TestItem, { name: 'Initial', value: 400 }));
132
- await db.flush({ indexes: true });
133
+ await db.flush();
133
134
 
134
135
  const queryResult: QueryResult.QueryResult<TestItem> = db.query(
135
136
  Query.select(Filter.type(TestItem, { value: 400 })),
@@ -150,7 +151,7 @@ describe('AtomQuery', () => {
150
151
 
151
152
  // Add object and verify subscription fires.
152
153
  db.add(Obj.make(TestItem, { name: 'Object 2', value: 400 }));
153
- await db.flush({ indexes: true, updates: true });
154
+ await db.flush({ updates: true });
154
155
  const countAfterFirstAdd = updateCount;
155
156
  expect(countAfterFirstAdd).toBeGreaterThan(0);
156
157
 
@@ -159,7 +160,7 @@ describe('AtomQuery', () => {
159
160
 
160
161
  // Add another object.
161
162
  db.add(Obj.make(TestItem, { name: 'Object 3', value: 400 }));
162
- await db.flush({ indexes: true, updates: true });
163
+ await db.flush({ updates: true });
163
164
 
164
165
  // Update count should not have changed after unsubscribe.
165
166
  expect(updateCount).toBe(countAfterFirstAdd);
@@ -179,7 +180,7 @@ describe('AtomQuery', () => {
179
180
 
180
181
  test('multiple atoms from same query share underlying subscription', async () => {
181
182
  db.add(Obj.make(TestItem, { name: 'Object', value: 500 }));
182
- await db.flush({ indexes: true });
183
+ await db.flush();
183
184
 
184
185
  const queryResult: QueryResult.QueryResult<TestItem> = db.query(
185
186
  Query.select(Filter.type(TestItem, { value: 500 })),
package/src/query-atom.ts CHANGED
@@ -37,7 +37,7 @@ const queryableRegistry = new WeakDictionary<string, Database.Queryable>();
37
37
  // Key separator that won't appear in identifiers (DXN strings use colons).
38
38
  const KEY_SEPARATOR = '~';
39
39
 
40
- // Atom.family keyed by "identifier\0serializedAST".
40
+ // Atom.family keyed by "identifier~serializedAST".
41
41
  const queryFamily = Atom.family((key: string) => {
42
42
  // Parse key outside Atom.make - runs once per key.
43
43
  const separatorIndex = key.indexOf(KEY_SEPARATOR);
@@ -8,6 +8,7 @@ import { describe, expect, test } from 'vitest';
8
8
  import { Obj } from '@dxos/echo';
9
9
  import { TestSchema } from '@dxos/echo/testing';
10
10
  import { createObject } from '@dxos/echo-db';
11
+ import { arrayMove } from '@dxos/util';
11
12
 
12
13
  import * as AtomObj from './atom';
13
14
 
@@ -203,4 +204,30 @@ describe('Echo Atom - Reactivity', () => {
203
204
 
204
205
  unsubscribe();
205
206
  });
207
+
208
+ test('property atom for array property updates when array is reordered in place', () => {
209
+ // Verifies that makeProperty(obj, 'columns')-style atoms subscribe to in-place
210
+ // array mutations (e.g. arrayMove), so UI stays in sync after column reorder.
211
+ const obj = createObject(Obj.make(TestSchema.Example, { stringArray: ['a', 'b', 'c'] }));
212
+
213
+ const registry = Registry.make();
214
+ const atom = AtomObj.makeProperty(obj, 'stringArray');
215
+
216
+ const initial = registry.get(atom);
217
+ expect(initial).toEqual(['a', 'b', 'c']);
218
+
219
+ let updateCount = 0;
220
+ registry.subscribe(atom, () => {
221
+ updateCount++;
222
+ });
223
+
224
+ // Reorder in place (e.g. move first to last).
225
+ Obj.change(obj, (obj) => {
226
+ arrayMove(obj.stringArray!, 0, 2);
227
+ });
228
+
229
+ expect(updateCount).toBe(1);
230
+ const afterReorder = registry.get(atom);
231
+ expect(afterReorder).toEqual(['b', 'c', 'a']);
232
+ });
206
233
  });
@@ -33,7 +33,7 @@ describe('AtomRef - Basic Functionality', () => {
33
33
 
34
34
  const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
35
35
  db.add(targetObj);
36
- await db.flush({ indexes: true });
36
+ await db.flush();
37
37
 
38
38
  const ref = Ref.make(targetObj);
39
39
  const atom = AtomRef.make(ref);
@@ -48,7 +48,7 @@ describe('AtomRef - Basic Functionality', () => {
48
48
 
49
49
  const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
50
50
  db.add(targetObj);
51
- await db.flush({ indexes: true });
51
+ await db.flush();
52
52
 
53
53
  const ref = Ref.make(targetObj);
54
54
  const atom = AtomRef.make(ref);
@@ -90,7 +90,7 @@ describe('AtomRef - Referential Equality', () => {
90
90
 
91
91
  const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
92
92
  db.add(targetObj);
93
- await db.flush({ indexes: true });
93
+ await db.flush();
94
94
 
95
95
  const ref = Ref.make(targetObj);
96
96
 
@@ -116,7 +116,7 @@ describe('AtomRef - Referential Equality', () => {
116
116
  });
117
117
  db.add(targetObj1);
118
118
  db.add(targetObj2);
119
- await db.flush({ indexes: true });
119
+ await db.flush();
120
120
 
121
121
  const ref1 = Ref.make(targetObj1);
122
122
  const ref2 = Ref.make(targetObj2);
@@ -133,7 +133,7 @@ describe('AtomRef - Referential Equality', () => {
133
133
 
134
134
  const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
135
135
  db.add(targetObj);
136
- await db.flush({ indexes: true });
136
+ await db.flush();
137
137
 
138
138
  // Create two separate refs to the same target.
139
139
  const ref1 = Ref.make(targetObj);
@@ -158,7 +158,7 @@ describe('AtomRef - Referential Equality', () => {
158
158
 
159
159
  const targetObj = Obj.make(TestSchema.Person, { name: 'Target', username: 'target', email: 'target@example.com' });
160
160
  db.add(targetObj);
161
- await db.flush({ indexes: true });
161
+ await db.flush();
162
162
 
163
163
  const ref = Ref.make(targetObj);
164
164
 
@@ -197,7 +197,7 @@ describe('AtomRef - Expando Objects', () => {
197
197
  test('works with Expando objects', async () => {
198
198
  const targetObj = Obj.make(TestSchema.Expando, { name: 'Expando Target', value: 42 });
199
199
  db.add(targetObj);
200
- await db.flush({ indexes: true });
200
+ await db.flush();
201
201
 
202
202
  const ref = Ref.make(targetObj);
203
203
  const atom = AtomRef.make(ref);