@verdant-web/store 5.4.3 → 5.5.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 +8 -8
- package/dist/bundle/index.js.map +4 -4
- package/dist/esm/__tests__/QueryBatcher.test.d.ts +1 -0
- package/dist/esm/__tests__/QueryBatcher.test.js +17 -0
- package/dist/esm/__tests__/QueryBatcher.test.js.map +1 -0
- package/dist/esm/__tests__/entities.test.js +3 -3
- package/dist/esm/__tests__/entities.test.js.map +1 -1
- package/dist/esm/__tests__/queries.test.js +16 -0
- package/dist/esm/__tests__/queries.test.js.map +1 -1
- package/dist/esm/entities/Entity.d.ts +2 -3
- package/dist/esm/entities/Entity.js +37 -17
- package/dist/esm/entities/Entity.js.map +1 -1
- package/dist/esm/entities/EntityStore.js +1 -1
- package/dist/esm/entities/EntityStore.js.map +1 -1
- package/dist/esm/files/EntityFile.d.ts +22 -7
- package/dist/esm/files/EntityFile.js +30 -6
- package/dist/esm/files/EntityFile.js.map +1 -1
- package/dist/esm/files/FileManager.d.ts +5 -4
- package/dist/esm/files/FileManager.js +35 -16
- package/dist/esm/files/FileManager.js.map +1 -1
- package/dist/esm/queries/BaseQuery.d.ts +4 -1
- package/dist/esm/queries/BaseQuery.js +22 -11
- package/dist/esm/queries/BaseQuery.js.map +1 -1
- package/dist/esm/queries/QueryBatcher.d.ts +2 -0
- package/dist/esm/queries/QueryBatcher.js +39 -0
- package/dist/esm/queries/QueryBatcher.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/QueryBatcher.test.ts +21 -0
- package/src/__tests__/entities.test.ts +3 -3
- package/src/__tests__/queries.test.ts +19 -0
- package/src/entities/Entity.ts +30 -8
- package/src/entities/EntityStore.ts +1 -1
- package/src/files/EntityFile.ts +52 -10
- package/src/files/FileManager.ts +45 -20
- package/src/queries/BaseQuery.ts +25 -12
- package/src/queries/QueryBatcher.ts +54 -0
package/src/entities/Entity.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
validateEntityField,
|
|
28
28
|
} from '@verdant-web/common';
|
|
29
29
|
import { Context } from '../context/context.js';
|
|
30
|
-
import {
|
|
30
|
+
import { EntityFileContext } from '../files/EntityFile.js';
|
|
31
31
|
import { FileManager } from '../files/FileManager.js';
|
|
32
32
|
import { processValueFiles } from '../files/utils.js';
|
|
33
33
|
import { ClientWithCollections, EntityFile } from '../index.js';
|
|
@@ -751,16 +751,38 @@ export class Entity<
|
|
|
751
751
|
this.emit('changeDeep', target, ev);
|
|
752
752
|
this.parent?.deepChange(target, ev);
|
|
753
753
|
};
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
754
|
+
private createFileContext = (key: any): EntityFileContext => ({
|
|
755
|
+
oid: this.oid,
|
|
756
|
+
key,
|
|
757
|
+
getFileRef: () => {
|
|
758
|
+
const file = this.rawView?.[key];
|
|
759
|
+
return isFileRef(file) ? file : null;
|
|
760
|
+
},
|
|
761
|
+
applyFileRef: (file) => {
|
|
762
|
+
if (this.readonlyKeys.includes(key as string)) {
|
|
763
|
+
throw new Error(`Cannot set readonly key ${key.toString()}`);
|
|
764
|
+
}
|
|
765
|
+
this.addPendingOperations(
|
|
766
|
+
this.isList
|
|
767
|
+
? this.patchCreator.createListSet(this.oid, key, file)
|
|
768
|
+
: this.patchCreator.createSet(this.oid, key, file),
|
|
769
|
+
);
|
|
770
|
+
},
|
|
771
|
+
subscribe: (callback) => this.subscribe('change', callback),
|
|
772
|
+
onChange: () => this.deepChange(this, { isLocal: false, oid: this.oid }),
|
|
773
|
+
});
|
|
758
774
|
|
|
759
775
|
private getChild = (key: any, oid: ObjectIdentifier) => {
|
|
760
776
|
const schema = getChildFieldSchema(this.schema, key);
|
|
761
777
|
if (!schema) {
|
|
778
|
+
let schemaString = '';
|
|
779
|
+
try {
|
|
780
|
+
schemaString = JSON.stringify(this.schema);
|
|
781
|
+
} catch {
|
|
782
|
+
schemaString = '<unable to stringify schema>';
|
|
783
|
+
}
|
|
762
784
|
throw new Error(
|
|
763
|
-
`No schema for key ${String(key)} in ${
|
|
785
|
+
`No schema for key ${String(key)} in ${schemaString} (${this.oid})`,
|
|
764
786
|
);
|
|
765
787
|
}
|
|
766
788
|
return this.entityFamily.get({
|
|
@@ -828,7 +850,7 @@ export class Entity<
|
|
|
828
850
|
const file = this.files.get(child.id, {
|
|
829
851
|
downloadRemote: !!fieldSchema.downloadRemote,
|
|
830
852
|
ctx: this.ctx,
|
|
831
|
-
|
|
853
|
+
fileContext: this.createFileContext(key),
|
|
832
854
|
});
|
|
833
855
|
|
|
834
856
|
return file as KeyValue[Key];
|
|
@@ -1013,7 +1035,7 @@ export class Entity<
|
|
|
1013
1035
|
});
|
|
1014
1036
|
}
|
|
1015
1037
|
}
|
|
1016
|
-
return processValueFiles(value, (file) => this.files.add(file
|
|
1038
|
+
return processValueFiles(value, (file) => this.files.add(file));
|
|
1017
1039
|
};
|
|
1018
1040
|
|
|
1019
1041
|
private getDeleteMode = (key: any) => {
|
package/src/files/EntityFile.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
EventSubscriber,
|
|
3
|
+
FileData,
|
|
4
|
+
FileRef,
|
|
5
|
+
ObjectIdentifier,
|
|
6
|
+
PropertyName,
|
|
7
|
+
} from '@verdant-web/common';
|
|
2
8
|
import { Context } from '../context/context.js';
|
|
3
|
-
import { Entity } from '../entities/Entity.js';
|
|
4
9
|
|
|
5
10
|
export type EntityFileEvents = {
|
|
6
11
|
change: () => void;
|
|
@@ -9,12 +14,23 @@ export type EntityFileEvents = {
|
|
|
9
14
|
export const UPDATE = Symbol('entity-file-update');
|
|
10
15
|
export const MARK_FAILED = Symbol('entity-file-mark-failed');
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
|
|
17
|
+
export interface EntityFileContext {
|
|
18
|
+
readonly oid: ObjectIdentifier;
|
|
19
|
+
readonly key: PropertyName;
|
|
20
|
+
getFileRef(): FileRef | null;
|
|
21
|
+
applyFileRef(file: FileRef): void;
|
|
22
|
+
subscribe(callback: () => void): () => void;
|
|
23
|
+
onChange(): void;
|
|
24
|
+
}
|
|
14
25
|
|
|
15
26
|
export type EntityFileSnapshot = {
|
|
16
27
|
id: string;
|
|
17
28
|
url?: string | null;
|
|
29
|
+
name: string;
|
|
30
|
+
remote: boolean;
|
|
31
|
+
type: string;
|
|
32
|
+
file?: Blob | null;
|
|
33
|
+
alt: string | null;
|
|
18
34
|
};
|
|
19
35
|
|
|
20
36
|
/**
|
|
@@ -30,26 +46,29 @@ export class EntityFile extends EventSubscriber<EntityFileEvents> {
|
|
|
30
46
|
private _failedReason: string | undefined;
|
|
31
47
|
private _downloadRemote = false;
|
|
32
48
|
private _uploaded = false;
|
|
49
|
+
private _alt: string | null = null;
|
|
33
50
|
private ctx: Context;
|
|
34
51
|
private unsubscribes: (() => void)[] = [];
|
|
35
|
-
private
|
|
52
|
+
private fileContext: EntityFileContext;
|
|
36
53
|
|
|
37
54
|
constructor(
|
|
38
55
|
public readonly id: string,
|
|
39
56
|
{
|
|
40
57
|
downloadRemote = false,
|
|
41
58
|
ctx,
|
|
42
|
-
|
|
59
|
+
fileContext,
|
|
43
60
|
}: {
|
|
44
61
|
downloadRemote?: boolean;
|
|
45
62
|
ctx: Context;
|
|
46
|
-
|
|
63
|
+
fileContext: EntityFileContext;
|
|
47
64
|
},
|
|
48
65
|
) {
|
|
49
66
|
super();
|
|
50
67
|
this.ctx = ctx;
|
|
51
|
-
this.
|
|
68
|
+
this.fileContext = fileContext;
|
|
52
69
|
this._downloadRemote = downloadRemote;
|
|
70
|
+
this._alt = fileContext.getFileRef()?.alt ?? null;
|
|
71
|
+
this.unsubscribes.push(fileContext.subscribe(this.onContextChange));
|
|
53
72
|
|
|
54
73
|
this.unsubscribes.push(
|
|
55
74
|
this.ctx.internalEvents.subscribe(`fileUploaded:${id}`, this.onUploaded),
|
|
@@ -68,9 +87,31 @@ export class EntityFile extends EventSubscriber<EntityFileEvents> {
|
|
|
68
87
|
get error() {
|
|
69
88
|
return this._failedReason || null;
|
|
70
89
|
}
|
|
90
|
+
get alt() {
|
|
91
|
+
return this._alt;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
setAlt = (alt: string | null) => {
|
|
95
|
+
const file = this.fileContext.getFileRef();
|
|
96
|
+
if (!file || file.id !== this.id) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
'Cannot set alt text on a file which is no longer present',
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
if ((file.alt ?? null) === alt) return;
|
|
102
|
+
this.fileContext.applyFileRef({ ...file, alt });
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
private onContextChange = () => {
|
|
106
|
+
const nextAlt = this.fileContext.getFileRef()?.alt ?? null;
|
|
107
|
+
if (nextAlt !== this._alt) {
|
|
108
|
+
this._alt = nextAlt;
|
|
109
|
+
this.emitChange();
|
|
110
|
+
}
|
|
111
|
+
};
|
|
71
112
|
|
|
72
113
|
private emitChange() {
|
|
73
|
-
this.
|
|
114
|
+
this.fileContext.onChange();
|
|
74
115
|
this.emit('change');
|
|
75
116
|
}
|
|
76
117
|
|
|
@@ -135,7 +176,7 @@ export class EntityFile extends EventSubscriber<EntityFileEvents> {
|
|
|
135
176
|
this.dispose();
|
|
136
177
|
};
|
|
137
178
|
|
|
138
|
-
getSnapshot():
|
|
179
|
+
getSnapshot(): EntityFileSnapshot {
|
|
139
180
|
return {
|
|
140
181
|
id: this.id,
|
|
141
182
|
url: this._fileData?.url ?? this._objectUrl ?? undefined,
|
|
@@ -143,6 +184,7 @@ export class EntityFile extends EventSubscriber<EntityFileEvents> {
|
|
|
143
184
|
remote: this._fileData?.remote ?? false,
|
|
144
185
|
type: this.type ?? '',
|
|
145
186
|
file: this._fileData?.file,
|
|
187
|
+
alt: this.alt,
|
|
146
188
|
};
|
|
147
189
|
}
|
|
148
190
|
}
|
package/src/files/FileManager.ts
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { FileData } from '@verdant-web/common';
|
|
2
2
|
import { Context } from '../context/context.js';
|
|
3
|
-
import { Entity } from '../entities/Entity.js';
|
|
4
3
|
import { Disposable } from '../internal.js';
|
|
5
4
|
import { Sync } from '../sync/Sync.js';
|
|
6
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
EntityFile,
|
|
7
|
+
EntityFileContext,
|
|
8
|
+
MARK_FAILED,
|
|
9
|
+
UPDATE,
|
|
10
|
+
} from './EntityFile.js';
|
|
7
11
|
|
|
8
12
|
export class FileManager extends Disposable {
|
|
9
13
|
private sync;
|
|
10
14
|
private context;
|
|
11
15
|
|
|
12
|
-
private cache = new Map<string, EntityFile
|
|
16
|
+
private cache = new Map<string, WeakRef<EntityFile>>();
|
|
17
|
+
private fileData = new Map<string, WeakRef<FileData>>();
|
|
13
18
|
|
|
14
19
|
constructor({ sync, context }: { sync: Sync; context: Context }) {
|
|
15
20
|
super();
|
|
@@ -23,24 +28,17 @@ export class FileManager extends Disposable {
|
|
|
23
28
|
);
|
|
24
29
|
}
|
|
25
30
|
|
|
26
|
-
add = async (file: FileData
|
|
27
|
-
// immediately cache the file
|
|
28
|
-
let entityFile = this.cache.get(file.id);
|
|
29
|
-
if (!entityFile) {
|
|
30
|
-
entityFile = new EntityFile(file.id, { ctx: this.context, parent });
|
|
31
|
-
this.cache.set(file.id, entityFile);
|
|
32
|
-
}
|
|
33
|
-
|
|
31
|
+
add = async (file: FileData) => {
|
|
34
32
|
if (!file.remote) {
|
|
35
33
|
// immediately update local files.
|
|
36
|
-
|
|
34
|
+
this.updateFileData(file);
|
|
37
35
|
}
|
|
38
36
|
// this will download any original remote file and trigger a re-upload to the
|
|
39
37
|
// new file's identity, in addition to storing it on disk
|
|
40
38
|
const processedFile = await (
|
|
41
39
|
await this.context.files
|
|
42
40
|
).add(file, { cloneRemote: true });
|
|
43
|
-
|
|
41
|
+
this.updateFileData(processedFile);
|
|
44
42
|
};
|
|
45
43
|
|
|
46
44
|
/**
|
|
@@ -49,22 +47,49 @@ export class FileManager extends Disposable {
|
|
|
49
47
|
*/
|
|
50
48
|
get = (
|
|
51
49
|
id: string,
|
|
52
|
-
options: {
|
|
50
|
+
options: {
|
|
51
|
+
downloadRemote?: boolean;
|
|
52
|
+
ctx: Context;
|
|
53
|
+
fileContext: EntityFileContext;
|
|
54
|
+
},
|
|
53
55
|
) => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
const cacheKey = `${options.fileContext.oid}:${String(options.fileContext.key)}:${id}`;
|
|
57
|
+
const cachedFile = this.cache.get(cacheKey)?.deref();
|
|
58
|
+
if (cachedFile) {
|
|
59
|
+
return cachedFile;
|
|
56
60
|
}
|
|
61
|
+
this.cache.delete(cacheKey);
|
|
57
62
|
const file = new EntityFile(id, options);
|
|
58
|
-
this.cache.set(
|
|
59
|
-
this.
|
|
63
|
+
this.cache.set(cacheKey, this.context.weakRef(file));
|
|
64
|
+
const data = this.fileData.get(id)?.deref();
|
|
65
|
+
if (data) {
|
|
66
|
+
file[UPDATE](data);
|
|
67
|
+
} else {
|
|
68
|
+
this.fileData.delete(id);
|
|
69
|
+
this.load(file);
|
|
70
|
+
}
|
|
60
71
|
return file;
|
|
61
72
|
};
|
|
62
73
|
|
|
74
|
+
private updateFileData = (data: FileData) => {
|
|
75
|
+
this.fileData.set(data.id, this.context.weakRef(data));
|
|
76
|
+
for (const [key, fileRef] of this.cache) {
|
|
77
|
+
const file = fileRef.deref();
|
|
78
|
+
if (!file) {
|
|
79
|
+
this.cache.delete(key);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (file.id === data.id) {
|
|
83
|
+
file[UPDATE](data);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
63
88
|
private load = async (file: EntityFile) => {
|
|
64
89
|
const fileData = await (await this.context.files).get(file.id);
|
|
65
90
|
// immediately apply any file data we have
|
|
66
91
|
if (fileData) {
|
|
67
|
-
|
|
92
|
+
this.updateFileData(fileData);
|
|
68
93
|
}
|
|
69
94
|
|
|
70
95
|
if (!fileData?.url && (!fileData || fileData.remote)) {
|
|
@@ -96,7 +121,7 @@ export class FileManager extends Disposable {
|
|
|
96
121
|
url: result.data.url,
|
|
97
122
|
};
|
|
98
123
|
await (await this.context.files).update(copyWithUrl);
|
|
99
|
-
|
|
124
|
+
this.updateFileData(result.data);
|
|
100
125
|
} else {
|
|
101
126
|
this.context.log('error', 'Failed to load file', result);
|
|
102
127
|
if (!fileData) {
|
package/src/queries/BaseQuery.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventSubscriber } from '@verdant-web/common';
|
|
2
|
-
import { Context } from '../context/context.js';
|
|
2
|
+
import type { Context } from '../context/context.js';
|
|
3
3
|
import { Entity } from '../entities/Entity.js';
|
|
4
4
|
import { Disposable } from '../utils/Disposable.js';
|
|
5
5
|
import {
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
QueryRun,
|
|
9
9
|
QueryTiming,
|
|
10
10
|
} from './diagnostics.js';
|
|
11
|
+
import { enqueueQueryRun } from './QueryBatcher.js';
|
|
11
12
|
import { filterResultSet } from './utils.js';
|
|
12
13
|
|
|
13
14
|
export type BaseQueryEvents = {
|
|
@@ -28,11 +29,7 @@ export type QueryStatus = 'initial' | 'initializing' | 'revalidating' | 'ready';
|
|
|
28
29
|
export const ON_ALL_UNSUBSCRIBED = Symbol('ON_ALL_UNSUBSCRIBED');
|
|
29
30
|
export const UPDATE = Symbol('UPDATE');
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
// subscribe(event: 'change', callback: (value: T) => void): () => void;
|
|
33
|
-
// subscribe(event: 'statusChange', callback: (status: QueryStatus) => void): () => void;
|
|
34
|
-
// subscribe(callback: (value: T) => void): () => void;
|
|
35
|
-
// }
|
|
32
|
+
const REVALIDATION_DEBOUNCE = 50;
|
|
36
33
|
|
|
37
34
|
export abstract class BaseQuery<T> extends Disposable {
|
|
38
35
|
private _rawValue;
|
|
@@ -50,6 +47,8 @@ export abstract class BaseQuery<T> extends Disposable {
|
|
|
50
47
|
};
|
|
51
48
|
private _startedAt: number | null = null;
|
|
52
49
|
private _timingHistory: QueryRun[] = [];
|
|
50
|
+
private _revalidationTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
51
|
+
private _needsTrailingRevalidation = false;
|
|
53
52
|
|
|
54
53
|
protected context;
|
|
55
54
|
|
|
@@ -80,6 +79,7 @@ export abstract class BaseQuery<T> extends Disposable {
|
|
|
80
79
|
this.key = key;
|
|
81
80
|
this.collection = collection;
|
|
82
81
|
this.type = this.constructor.name;
|
|
82
|
+
this.addDispose(() => clearTimeout(this._revalidationTimeout));
|
|
83
83
|
const shouldUpdateFn =
|
|
84
84
|
shouldUpdate ||
|
|
85
85
|
((collections: string[]) => collections.includes(collection));
|
|
@@ -88,11 +88,7 @@ export abstract class BaseQuery<T> extends Disposable {
|
|
|
88
88
|
'collectionsChanged',
|
|
89
89
|
(collections) => {
|
|
90
90
|
if (shouldUpdateFn(collections)) {
|
|
91
|
-
this.
|
|
92
|
-
// immediately refilter the result set - deleted
|
|
93
|
-
// entities will already be nulled out
|
|
94
|
-
// this.refreshValue();
|
|
95
|
-
this.execute();
|
|
91
|
+
this.revalidate();
|
|
96
92
|
}
|
|
97
93
|
},
|
|
98
94
|
),
|
|
@@ -100,6 +96,23 @@ export abstract class BaseQuery<T> extends Disposable {
|
|
|
100
96
|
// TODO: subscribe to document changes and update if necessary.
|
|
101
97
|
}
|
|
102
98
|
|
|
99
|
+
private revalidate = () => {
|
|
100
|
+
if (this._revalidationTimeout !== undefined) {
|
|
101
|
+
this._needsTrailingRevalidation = true;
|
|
102
|
+
clearTimeout(this._revalidationTimeout);
|
|
103
|
+
} else {
|
|
104
|
+
this.execute();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this._revalidationTimeout = setTimeout(() => {
|
|
108
|
+
this._revalidationTimeout = undefined;
|
|
109
|
+
if (this._needsTrailingRevalidation) {
|
|
110
|
+
this._needsTrailingRevalidation = false;
|
|
111
|
+
this.execute();
|
|
112
|
+
}
|
|
113
|
+
}, REVALIDATION_DEBOUNCE);
|
|
114
|
+
};
|
|
115
|
+
|
|
103
116
|
get current() {
|
|
104
117
|
return this._value;
|
|
105
118
|
}
|
|
@@ -249,7 +262,7 @@ export abstract class BaseQuery<T> extends Disposable {
|
|
|
249
262
|
}
|
|
250
263
|
// no status change needed if already in a 'running' status.
|
|
251
264
|
|
|
252
|
-
this._executionPromise = this.run
|
|
265
|
+
this._executionPromise = enqueueQueryRun(this.context, this.run)
|
|
253
266
|
.then(() => this._value)
|
|
254
267
|
.catch((err) => {
|
|
255
268
|
if (err instanceof Error) {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Context } from '../context/context.js';
|
|
2
|
+
|
|
3
|
+
const BATCH_SIZE = 20;
|
|
4
|
+
|
|
5
|
+
type QueuedRun = {
|
|
6
|
+
run: () => Promise<any>;
|
|
7
|
+
resolve: (value: any) => void;
|
|
8
|
+
reject: (reason?: any) => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
class QueryBatcher {
|
|
12
|
+
private queue: QueuedRun[] = [];
|
|
13
|
+
private scheduled = false;
|
|
14
|
+
|
|
15
|
+
enqueue<T>(run: () => Promise<T>): Promise<T> {
|
|
16
|
+
const promise = new Promise<T>((resolve, reject) => {
|
|
17
|
+
this.queue.push({ run, resolve, reject });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (!this.scheduled) {
|
|
21
|
+
this.scheduled = true;
|
|
22
|
+
queueMicrotask(this.flush);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return promise;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private flush = () => {
|
|
29
|
+
const batch = this.queue.splice(0, BATCH_SIZE);
|
|
30
|
+
for (const { run, resolve, reject } of batch) {
|
|
31
|
+
run().then(resolve, reject);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (this.queue.length) {
|
|
35
|
+
setTimeout(this.flush, 0);
|
|
36
|
+
} else {
|
|
37
|
+
this.scheduled = false;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const batchers = new WeakMap<Context, QueryBatcher>();
|
|
43
|
+
|
|
44
|
+
export function enqueueQueryRun<T>(
|
|
45
|
+
context: Context,
|
|
46
|
+
run: () => Promise<T>,
|
|
47
|
+
): Promise<T> {
|
|
48
|
+
let batcher = batchers.get(context);
|
|
49
|
+
if (!batcher) {
|
|
50
|
+
batcher = new QueryBatcher();
|
|
51
|
+
batchers.set(context, batcher);
|
|
52
|
+
}
|
|
53
|
+
return batcher.enqueue(run);
|
|
54
|
+
}
|