@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.
- package/dist/bundle/index.js +12 -12
- package/dist/bundle/index.js.map +3 -3
- package/dist/esm/__tests__/batching.test.js +67 -1
- package/dist/esm/__tests__/batching.test.js.map +1 -1
- package/dist/esm/__tests__/documents.test.js +22 -0
- package/dist/esm/__tests__/documents.test.js.map +1 -1
- package/dist/esm/__tests__/fixtures/testStorage.d.ts +3 -1
- package/dist/esm/__tests__/fixtures/testStorage.js +3 -2
- package/dist/esm/__tests__/fixtures/testStorage.js.map +1 -1
- package/dist/esm/__tests__/mutations.test.js +40 -0
- package/dist/esm/__tests__/mutations.test.js.map +1 -1
- package/dist/esm/client/Client.d.ts +22 -1
- package/dist/esm/client/Client.js.map +1 -1
- package/dist/esm/client/ClientDescriptor.d.ts +7 -1
- package/dist/esm/client/ClientDescriptor.js +1 -0
- package/dist/esm/client/ClientDescriptor.js.map +1 -1
- package/dist/esm/entities/Entity.d.ts +40 -3
- package/dist/esm/entities/Entity.js +87 -24
- package/dist/esm/entities/Entity.js.map +1 -1
- package/dist/esm/entities/Entity.test.js +24 -2
- package/dist/esm/entities/Entity.test.js.map +1 -1
- package/dist/esm/entities/EntityMetadata.d.ts +2 -0
- package/dist/esm/entities/EntityMetadata.js +9 -1
- package/dist/esm/entities/EntityMetadata.js.map +1 -1
- package/dist/esm/entities/EntityStore.d.ts +1 -2
- package/dist/esm/entities/EntityStore.js +10 -7
- package/dist/esm/entities/EntityStore.js.map +1 -1
- package/dist/esm/entities/OperationBatcher.d.ts +25 -0
- package/dist/esm/entities/OperationBatcher.js +31 -3
- package/dist/esm/entities/OperationBatcher.js.map +1 -1
- package/dist/esm/entities/types.d.ts +18 -1
- package/dist/esm/metadata/Metadata.d.ts +3 -1
- package/dist/esm/metadata/Metadata.js +8 -1
- package/dist/esm/metadata/Metadata.js.map +1 -1
- package/dist/esm/queries/BaseQuery.d.ts +3 -0
- package/dist/esm/queries/BaseQuery.js +45 -11
- package/dist/esm/queries/BaseQuery.js.map +1 -1
- package/dist/esm/sync/Sync.d.ts +8 -2
- package/dist/esm/sync/Sync.js +6 -3
- package/dist/esm/sync/Sync.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/batching.test.ts +78 -0
- package/src/__tests__/documents.test.ts +28 -0
- package/src/__tests__/fixtures/testStorage.ts +10 -1
- package/src/__tests__/mutations.test.ts +53 -0
- package/src/client/Client.ts +14 -1
- package/src/client/ClientDescriptor.ts +9 -0
- package/src/entities/Entity.test.ts +31 -2
- package/src/entities/Entity.ts +128 -28
- package/src/entities/EntityMetadata.ts +13 -3
- package/src/entities/EntityStore.ts +10 -9
- package/src/entities/OperationBatcher.ts +69 -2
- package/src/entities/types.ts +29 -1
- package/src/metadata/Metadata.ts +13 -0
- package/src/queries/BaseQuery.ts +50 -13
- package/src/sync/Sync.ts +12 -2
package/src/entities/types.ts
CHANGED
|
@@ -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>(
|
|
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>[];
|
package/src/metadata/Metadata.ts
CHANGED
|
@@ -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
|
|
package/src/queries/BaseQuery.ts
CHANGED
|
@@ -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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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.
|
|
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.
|
|
190
|
-
this.
|
|
191
|
-
} else if (this.
|
|
192
|
-
this.
|
|
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
|
-
|
|
458
|
+
await this.activeSync.send(message);
|
|
459
|
+
this.onOutgoingMessage?.(message);
|
|
450
460
|
}
|
|
451
461
|
};
|
|
452
462
|
|