@verdant-web/store 3.6.3 → 3.7.0

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 (56) hide show
  1. package/dist/bundle/index.js +12 -12
  2. package/dist/bundle/index.js.map +3 -3
  3. package/dist/esm/__tests__/batching.test.js +67 -1
  4. package/dist/esm/__tests__/batching.test.js.map +1 -1
  5. package/dist/esm/__tests__/documents.test.js +22 -0
  6. package/dist/esm/__tests__/documents.test.js.map +1 -1
  7. package/dist/esm/__tests__/fixtures/testStorage.d.ts +3 -1
  8. package/dist/esm/__tests__/fixtures/testStorage.js +3 -2
  9. package/dist/esm/__tests__/fixtures/testStorage.js.map +1 -1
  10. package/dist/esm/__tests__/mutations.test.js +40 -0
  11. package/dist/esm/__tests__/mutations.test.js.map +1 -1
  12. package/dist/esm/client/Client.d.ts +22 -1
  13. package/dist/esm/client/Client.js.map +1 -1
  14. package/dist/esm/client/ClientDescriptor.d.ts +7 -1
  15. package/dist/esm/client/ClientDescriptor.js +1 -0
  16. package/dist/esm/client/ClientDescriptor.js.map +1 -1
  17. package/dist/esm/entities/Entity.d.ts +40 -3
  18. package/dist/esm/entities/Entity.js +87 -24
  19. package/dist/esm/entities/Entity.js.map +1 -1
  20. package/dist/esm/entities/Entity.test.js +24 -2
  21. package/dist/esm/entities/Entity.test.js.map +1 -1
  22. package/dist/esm/entities/EntityMetadata.d.ts +2 -0
  23. package/dist/esm/entities/EntityMetadata.js +9 -1
  24. package/dist/esm/entities/EntityMetadata.js.map +1 -1
  25. package/dist/esm/entities/EntityStore.d.ts +1 -2
  26. package/dist/esm/entities/EntityStore.js +10 -7
  27. package/dist/esm/entities/EntityStore.js.map +1 -1
  28. package/dist/esm/entities/OperationBatcher.d.ts +25 -0
  29. package/dist/esm/entities/OperationBatcher.js +31 -3
  30. package/dist/esm/entities/OperationBatcher.js.map +1 -1
  31. package/dist/esm/entities/types.d.ts +18 -1
  32. package/dist/esm/metadata/Metadata.d.ts +3 -1
  33. package/dist/esm/metadata/Metadata.js +8 -1
  34. package/dist/esm/metadata/Metadata.js.map +1 -1
  35. package/dist/esm/queries/BaseQuery.d.ts +3 -0
  36. package/dist/esm/queries/BaseQuery.js +45 -11
  37. package/dist/esm/queries/BaseQuery.js.map +1 -1
  38. package/dist/esm/sync/Sync.d.ts +8 -2
  39. package/dist/esm/sync/Sync.js +6 -3
  40. package/dist/esm/sync/Sync.js.map +1 -1
  41. package/package.json +2 -2
  42. package/src/__tests__/batching.test.ts +78 -0
  43. package/src/__tests__/documents.test.ts +28 -0
  44. package/src/__tests__/fixtures/testStorage.ts +10 -1
  45. package/src/__tests__/mutations.test.ts +53 -0
  46. package/src/client/Client.ts +14 -1
  47. package/src/client/ClientDescriptor.ts +9 -0
  48. package/src/entities/Entity.test.ts +31 -2
  49. package/src/entities/Entity.ts +128 -28
  50. package/src/entities/EntityMetadata.ts +13 -3
  51. package/src/entities/EntityStore.ts +10 -9
  52. package/src/entities/OperationBatcher.ts +69 -2
  53. package/src/entities/types.ts +29 -1
  54. package/src/metadata/Metadata.ts +13 -0
  55. package/src/queries/BaseQuery.ts +50 -13
  56. package/src/sync/Sync.ts +12 -2
@@ -84,9 +84,14 @@ export interface ObjectEntity<
84
84
  Snapshot = DataFromInit<Init>,
85
85
  > extends BaseEntity<Init, Value, Snapshot> {
86
86
  keys(): string[];
87
+ readonly size: number;
87
88
  entries(): [string, Exclude<Value[keyof Value], undefined>][];
88
89
  values(): Exclude<Value[keyof Value], undefined>[];
89
- set<Key extends keyof Init>(key: Key, value: Init[Key]): void;
90
+ set<Key extends keyof Init>(
91
+ key: Key,
92
+ value: Init[Key],
93
+ options?: { force?: boolean },
94
+ ): void;
90
95
  delete(key: DeletableKeys<Value>): void;
91
96
  update(
92
97
  value: DeepPartial<Init>,
@@ -114,6 +119,16 @@ export interface ObjectEntity<
114
119
  merge?: boolean;
115
120
  },
116
121
  ): void;
122
+ /**
123
+ * Deletes the entity from either its parent (if it's a nested value)
124
+ * or the database itself. WARNING: this method is tricky. It will
125
+ * throw an error on nested fields which are not deletable in the
126
+ * schema. Deleting any entity and then attempting to access its
127
+ * data will also result in an error.
128
+ *
129
+ * Prefer using client.<collection>.delete(id) instead.
130
+ */
131
+ deleteSelf(): void;
117
132
  readonly isList: false;
118
133
  }
119
134
 
@@ -125,6 +140,11 @@ export interface ListEntity<
125
140
  BaseEntity<Init, Value, Snapshot> {
126
141
  readonly isList: true;
127
142
  readonly length: number;
143
+ set(
144
+ index: number,
145
+ value: ListItemInit<Init>,
146
+ options?: { force?: boolean },
147
+ ): void;
128
148
  push(value: ListItemInit<Init>): void;
129
149
  insert(index: number, value: ListItemInit<Init>): void;
130
150
  move(from: number, to: number): void;
@@ -138,6 +158,14 @@ export interface ListEntity<
138
158
  removeFirst(item: ListItemValue<Value>): void;
139
159
  removeLast(item: ListItemValue<Value>): void;
140
160
  map<U>(callback: (value: ListItemValue<Value>, index: number) => U): U[];
161
+ reduce<U>(
162
+ callback: (
163
+ accumulator: U,
164
+ currentValue: ListItemValue<Value>,
165
+ index: number,
166
+ ) => U,
167
+ initialValue: U,
168
+ ): U;
141
169
  filter(
142
170
  callback: (value: ListItemValue<Value>, index: number) => boolean,
143
171
  ): ListItemValue<Value>[];
@@ -60,12 +60,16 @@ export class Metadata extends EventSubscriber<{
60
60
 
61
61
  private context: Omit<Context, 'documentDb' | 'getNow'>;
62
62
 
63
+ private onOperation?: (operation: Operation) => void;
64
+
63
65
  constructor({
64
66
  disableRebasing,
65
67
  context,
68
+ onOperation,
66
69
  }: {
67
70
  disableRebasing?: boolean;
68
71
  context: Omit<Context, 'documentDb' | 'getNow'>;
72
+ onOperation?: (operation: Operation) => void;
69
73
  }) {
70
74
  super();
71
75
  this.context = context;
@@ -76,6 +80,7 @@ export class Metadata extends EventSubscriber<{
76
80
  this.ackInfo = new AckInfoStore(this.db);
77
81
  this.messageCreator = new MessageCreator(this);
78
82
  this.patchCreator = new PatchCreator(() => this.now);
83
+ this.onOperation = onOperation;
79
84
  if (disableRebasing) this.disableRebasing = disableRebasing;
80
85
  }
81
86
 
@@ -312,6 +317,10 @@ export class Metadata extends EventSubscriber<{
312
317
 
313
318
  // we can now enqueue and check for rebase opportunities
314
319
  this.tryAutonomousRebase();
320
+
321
+ if (this.onOperation) {
322
+ operations.forEach((o) => this.onOperation!(o));
323
+ }
315
324
  };
316
325
 
317
326
  /**
@@ -340,6 +349,10 @@ export class Metadata extends EventSubscriber<{
340
349
 
341
350
  this.ack(operations[operations.length - 1].timestamp);
342
351
 
352
+ if (this.onOperation) {
353
+ operations.forEach((o) => this.onOperation!(o));
354
+ }
355
+
343
356
  return affectedDocumentOids;
344
357
  };
345
358
 
@@ -42,6 +42,7 @@ export abstract class BaseQuery<T> extends Disposable {
42
42
 
43
43
  readonly collection;
44
44
  readonly key;
45
+ readonly isListQuery;
45
46
 
46
47
  constructor({
47
48
  initial,
@@ -53,6 +54,7 @@ export abstract class BaseQuery<T> extends Disposable {
53
54
  super();
54
55
  this._rawValue = initial;
55
56
  this._value = initial;
57
+ this.isListQuery = Array.isArray(initial);
56
58
  this._events = new EventSubscriber<BaseQueryEvents>(
57
59
  (event: keyof BaseQueryEvents) => {
58
60
  if (event === 'change') this._allUnsubscribedHandler?.(this);
@@ -98,6 +100,19 @@ export abstract class BaseQuery<T> extends Disposable {
98
100
  return this._status;
99
101
  }
100
102
 
103
+ private set status(v: QueryStatus) {
104
+ if (this._status === v) return;
105
+ this._status = v;
106
+ this._events.emit('statusChange', this._status);
107
+ }
108
+
109
+ get hasDeleted() {
110
+ if (this.isListQuery) {
111
+ return (this._rawValue as any[]).length !== (this._value as any[]).length;
112
+ }
113
+ return !!this._rawValue && !this._value;
114
+ }
115
+
101
116
  /**
102
117
  * Subscribe to changes in the query value.
103
118
  *
@@ -138,16 +153,38 @@ export abstract class BaseQuery<T> extends Disposable {
138
153
  protected setValue = (value: T) => {
139
154
  this._rawValue = value;
140
155
  this.subscribeToDeleteAndRestore(this._rawValue);
141
- this._value = filterResultSet(value);
142
- // validate the value
143
- if (
144
- Array.isArray(this._value) &&
145
- this._value.some((v) => v.getSnapshot() === null)
146
- ) {
147
- debugger;
156
+ const filtered = filterResultSet(value);
157
+
158
+ // prevent excess change notifications by diffing
159
+ // value by identity for single-value queries,
160
+ // and by item identity for multi-value
161
+ let changed = true;
162
+ // always fire change when going from initial to ready
163
+ if (this.status === 'initializing' || this.status === 'initial') {
164
+ changed = true;
165
+ } else {
166
+ // compare values by identity, after filtering.
167
+ if (this.isListQuery) {
168
+ if (
169
+ (this._value as any[]).length === (filtered as any[]).length &&
170
+ (this._value as any[]).every((v, i) => v === (filtered as any[])[i])
171
+ ) {
172
+ changed = false;
173
+ }
174
+ } else {
175
+ if (this._value === filtered) {
176
+ changed = false;
177
+ }
178
+ }
179
+ }
180
+
181
+ this._value = filtered;
182
+
183
+ if (changed) {
184
+ this.context.log('debug', 'Query value changed', this.key);
185
+ this._events.emit('change', this._value);
148
186
  }
149
- this._status = 'ready';
150
- this._events.emit('change', this._value);
187
+ this.status = 'ready';
151
188
  };
152
189
 
153
190
  // re-applies filtering if results have changed
@@ -186,10 +223,10 @@ export abstract class BaseQuery<T> extends Disposable {
186
223
  execute = () => {
187
224
  this.context.log('debug', 'Executing query', this.key);
188
225
 
189
- if (this._status === 'initial') {
190
- this._status = 'initializing';
191
- } else if (this._status === 'ready') {
192
- this._status = 'revalidating';
226
+ if (this.status === 'initial') {
227
+ this.status = 'initializing';
228
+ } else if (this.status === 'ready') {
229
+ this.status = 'revalidating';
193
230
  }
194
231
  // no status change needed if already in a 'running' status.
195
232
 
package/src/sync/Sync.ts CHANGED
@@ -173,6 +173,11 @@ export interface ServerSyncOptions<Profile = any, Presence = any>
173
173
  * but is not yet thoroughly vetted.
174
174
  */
175
175
  useBroadcastChannel?: boolean;
176
+ /**
177
+ * Listen for outgoing messages from the client to the server.
178
+ * Not sure why you want to do this, but be careful.
179
+ */
180
+ onOutgoingMessage?: (message: ClientMessage) => void;
176
181
  }
177
182
 
178
183
  export class ServerSync<Presence = any, Profile = any>
@@ -196,6 +201,8 @@ export class ServerSync<Presence = any, Profile = any>
196
201
 
197
202
  readonly presence: PresenceManager<Profile, Presence>;
198
203
 
204
+ private onOutgoingMessage?: (message: ClientMessage) => void;
205
+
199
206
  private log;
200
207
 
201
208
  constructor(
@@ -211,6 +218,7 @@ export class ServerSync<Presence = any, Profile = any>
211
218
  presenceUpdateBatchTimeout,
212
219
  defaultProfile,
213
220
  useBroadcastChannel,
221
+ onOutgoingMessage,
214
222
  }: ServerSyncOptions<Profile, Presence>,
215
223
  {
216
224
  meta,
@@ -230,6 +238,7 @@ export class ServerSync<Presence = any, Profile = any>
230
238
  this.meta = meta;
231
239
  this.onData = onData;
232
240
  this.log = ctx.log;
241
+ this.onOutgoingMessage = onOutgoingMessage;
233
242
  this.presence = new PresenceManager({
234
243
  initialPresence,
235
244
  defaultProfile,
@@ -444,9 +453,10 @@ export class ServerSync<Presence = any, Profile = any>
444
453
  return this.pushPullSync.interval;
445
454
  }
446
455
 
447
- send = (message: ClientMessage) => {
456
+ send = async (message: ClientMessage) => {
448
457
  if (this.activeSync.status === 'active') {
449
- return this.activeSync.send(message);
458
+ await this.activeSync.send(message);
459
+ this.onOutgoingMessage?.(message);
450
460
  }
451
461
  };
452
462