j-templates 8.0.5 → 8.0.6
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/Store/Diff/diffAsync.d.ts +1 -1
- package/Store/Diff/diffAsync.js +2 -2
- package/Store/Diff/diffSync.d.ts +1 -1
- package/Store/Diff/diffSync.js +2 -2
- package/Store/Diff/diffTree.d.ts +1 -1
- package/Store/Diff/diffTree.js +6 -6
- package/Store/Store/store.d.ts +5 -1
- package/Store/Store/store.js +3 -1
- package/Store/Store/storeAsync.d.ts +4 -0
- package/Store/Store/storeAsync.js +7 -1
- package/Store/Store/storeSync.js +1 -1
- package/Store/Tree/observableNode.d.ts +3 -0
- package/Store/Tree/observableNode.js +56 -8
- package/Store/index.d.ts +1 -0
- package/Store/index.js +3 -1
- package/Utils/asyncQueue.d.ts +1 -0
- package/Utils/asyncQueue.js +7 -0
- package/package.json +1 -1
|
@@ -28,7 +28,7 @@ export declare class DiffAsync implements IDiffTreeAsync {
|
|
|
28
28
|
* @param value - The new value to compare
|
|
29
29
|
* @returns Promise that resolves to diff results showing changes
|
|
30
30
|
*/
|
|
31
|
-
DiffPath(path: string, value: any): Promise<JsonDiffResult>;
|
|
31
|
+
DiffPath(path: string, value: any, flatten?: boolean): Promise<JsonDiffResult>;
|
|
32
32
|
/**
|
|
33
33
|
* Computes diffs for a batch of path/value pairs asynchronously.
|
|
34
34
|
* @param data - Array of objects with path and value properties
|
package/Store/Diff/diffAsync.js
CHANGED
|
@@ -28,10 +28,10 @@ class DiffAsync {
|
|
|
28
28
|
* @param value - The new value to compare
|
|
29
29
|
* @returns Promise that resolves to diff results showing changes
|
|
30
30
|
*/
|
|
31
|
-
async DiffPath(path, value) {
|
|
31
|
+
async DiffPath(path, value, flatten = true) {
|
|
32
32
|
return await this.workerQueue.Push({
|
|
33
33
|
method: "diffpath",
|
|
34
|
-
arguments: [path, value],
|
|
34
|
+
arguments: [path, value, flatten],
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
37
|
/**
|
package/Store/Diff/diffSync.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export declare class DiffSync implements IDiffTree {
|
|
|
21
21
|
* @param value - The new value to compare
|
|
22
22
|
* @returns Diff results showing changes
|
|
23
23
|
*/
|
|
24
|
-
DiffPath(path: string, value: any): import("../../Utils/json").JsonDiffResult;
|
|
24
|
+
DiffPath(path: string, value: any, flatten?: boolean): import("../../Utils/json").JsonDiffResult;
|
|
25
25
|
/**
|
|
26
26
|
* Computes diffs for a batch of path/value pairs.
|
|
27
27
|
* @param data - Array of objects with path and value properties
|
package/Store/Diff/diffSync.js
CHANGED
|
@@ -25,8 +25,8 @@ class DiffSync {
|
|
|
25
25
|
* @param value - The new value to compare
|
|
26
26
|
* @returns Diff results showing changes
|
|
27
27
|
*/
|
|
28
|
-
DiffPath(path, value) {
|
|
29
|
-
return this.diffTree.DiffPath(path, value);
|
|
28
|
+
DiffPath(path, value, flatten = true) {
|
|
29
|
+
return this.diffTree.DiffPath(path, value, flatten);
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
32
|
* Computes diffs for a batch of path/value pairs.
|
package/Store/Diff/diffTree.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export interface IDiffTree {
|
|
|
27
27
|
* @param value - The new value to compare
|
|
28
28
|
* @returns Diff results showing changes
|
|
29
29
|
*/
|
|
30
|
-
DiffPath(path: string, value: any): JsonDiffResult;
|
|
30
|
+
DiffPath(path: string, value: any, flatten?: boolean): JsonDiffResult;
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
33
|
* Constructor interface for IDiffTree.
|
package/Store/Diff/diffTree.js
CHANGED
|
@@ -25,7 +25,7 @@ function DiffTreeFactory(jsonDiffFactory, worker) {
|
|
|
25
25
|
break;
|
|
26
26
|
}
|
|
27
27
|
case "diffpath": {
|
|
28
|
-
const diff = diffTree.DiffPath(data.arguments[0], data.arguments[1]);
|
|
28
|
+
const diff = diffTree.DiffPath(data.arguments[0], data.arguments[1], data.arguments[2]);
|
|
29
29
|
ctx.postMessage(diff);
|
|
30
30
|
break;
|
|
31
31
|
}
|
|
@@ -133,18 +133,18 @@ function DiffTreeFactory(jsonDiffFactory, worker) {
|
|
|
133
133
|
* @param keyFunc - Optional function to extract keys from objects
|
|
134
134
|
* @returns Diff results showing all changes made
|
|
135
135
|
*/
|
|
136
|
-
function UpdateSource(source, path, value, keyFunc) {
|
|
136
|
+
function UpdateSource(source, path, value, flatten, keyFunc) {
|
|
137
137
|
const diffResult = [];
|
|
138
138
|
if (keyFunc) {
|
|
139
139
|
const keyPath = ResolveKeyPath(source, path, keyFunc);
|
|
140
140
|
if (keyPath !== path) {
|
|
141
|
-
const keyDiffResult = UpdateSource(source, keyPath, value, keyFunc);
|
|
141
|
+
const keyDiffResult = UpdateSource(source, keyPath, value, flatten, keyFunc);
|
|
142
142
|
diffResult.push(...keyDiffResult);
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
const sourceValue = GetPathValue(source, path);
|
|
146
146
|
JsonDiff(value, sourceValue, path, diffResult);
|
|
147
|
-
if (keyFunc) {
|
|
147
|
+
if (flatten && keyFunc) {
|
|
148
148
|
let flattened = {};
|
|
149
149
|
flattened = FlattenValue(flattened, value, keyFunc);
|
|
150
150
|
flattened = JsonDeepClone(flattened);
|
|
@@ -189,8 +189,8 @@ function DiffTreeFactory(jsonDiffFactory, worker) {
|
|
|
189
189
|
* @param value - The new value to compare
|
|
190
190
|
* @returns Diff results showing changes
|
|
191
191
|
*/
|
|
192
|
-
DiffPath(path, value) {
|
|
193
|
-
return UpdateSource(this.rootState, path, value, this.keyFunc);
|
|
192
|
+
DiffPath(path, value, flatten = true) {
|
|
193
|
+
return UpdateSource(this.rootState, path, value, flatten, this.keyFunc);
|
|
194
194
|
}
|
|
195
195
|
/**
|
|
196
196
|
* Retrieves the current value at a path.
|
package/Store/Store/store.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { JsonDiffResult } from "../../Utils/json";
|
|
|
6
6
|
* @see StoreSync
|
|
7
7
|
* @see StoreAsync
|
|
8
8
|
*/
|
|
9
|
-
export declare class Store {
|
|
9
|
+
export declare abstract class Store {
|
|
10
10
|
protected keyFunc?: (value: any) => string | undefined;
|
|
11
11
|
private rootMap;
|
|
12
12
|
private createNode;
|
|
@@ -31,6 +31,10 @@ export declare class Store {
|
|
|
31
31
|
* @returns The value or the default value
|
|
32
32
|
*/
|
|
33
33
|
Get<O>(id: string, defaultValue: O): O;
|
|
34
|
+
abstract Write(data: unknown, key?: string): Promise<void> | void;
|
|
35
|
+
abstract Patch(key: string, patch: unknown): Promise<void> | void;
|
|
36
|
+
abstract Push(key: string, ...data: unknown[]): Promise<void> | void;
|
|
37
|
+
abstract Splice(key: string, start: number, deleteCount?: number, ...items: unknown[]): Promise<any[]> | any[];
|
|
34
38
|
/**
|
|
35
39
|
* Updates the root map with diff results by grouping changes by root path.
|
|
36
40
|
* @param results - Array of diff results to apply
|
package/Store/Store/store.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Store = void 0;
|
|
4
|
+
const json_1 = require("../../Utils/json");
|
|
4
5
|
const observableNode_1 = require("../Tree/observableNode");
|
|
5
6
|
/**
|
|
6
7
|
* Base class for observable data store management.
|
|
@@ -27,6 +28,7 @@ class Store {
|
|
|
27
28
|
if (rootObject === undefined)
|
|
28
29
|
throw `No root object found for key: ${key}`;
|
|
29
30
|
const rootValue = observableNode_1.ObservableNode.Unwrap(rootObject); // rootObject[GET_OBSERVABLE_VALUE];
|
|
31
|
+
observableNode_1.ObservableNode.Touch(rootValue, key);
|
|
30
32
|
const alias = rootValue[key];
|
|
31
33
|
return alias;
|
|
32
34
|
});
|
|
@@ -70,7 +72,7 @@ class Store {
|
|
|
70
72
|
if (rootObject === undefined) {
|
|
71
73
|
if (results.length > 1 || results[0].path.length > 1)
|
|
72
74
|
throw `Unable to initialize root path ${rootPath} with ${results.length} results and initial path ${results[0].path}`;
|
|
73
|
-
const newRootObject = this.createNode({ [rootPath]: results[0].value });
|
|
75
|
+
const newRootObject = this.createNode({ [rootPath]: (0, json_1.JsonDeepClone)(results[0].value) });
|
|
74
76
|
this.rootMap.set(rootPath, newRootObject);
|
|
75
77
|
return;
|
|
76
78
|
}
|
|
@@ -47,6 +47,10 @@ export declare class StoreAsync extends Store {
|
|
|
47
47
|
* @param keyFunc Optional function to generate a key for a given data value.
|
|
48
48
|
*/
|
|
49
49
|
constructor(keyFunc?: (value: any) => string | undefined);
|
|
50
|
+
/**
|
|
51
|
+
* Returns a promise that resolve once all current queued writes have completed
|
|
52
|
+
*/
|
|
53
|
+
Flush(): Promise<void>;
|
|
50
54
|
/**
|
|
51
55
|
* Writes data to the store asynchronously.
|
|
52
56
|
* This method ensures that write operations are queued and executed in a non-blocking manner.
|
|
@@ -52,6 +52,12 @@ class StoreAsync extends store_1.Store {
|
|
|
52
52
|
this.queue = new asyncQueue_1.AsyncQueue();
|
|
53
53
|
this.diff = new diffAsync_1.DiffAsync(keyFunc);
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Returns a promise that resolve once all current queued writes have completed
|
|
57
|
+
*/
|
|
58
|
+
async Flush() {
|
|
59
|
+
return this.queue.Flush();
|
|
60
|
+
}
|
|
55
61
|
/**
|
|
56
62
|
* Writes data to the store asynchronously.
|
|
57
63
|
* This method ensures that write operations are queued and executed in a non-blocking manner.
|
|
@@ -82,7 +88,7 @@ class StoreAsync extends store_1.Store {
|
|
|
82
88
|
throw "Unable to patch undefined value";
|
|
83
89
|
const json = value.toJSON();
|
|
84
90
|
const mergedJson = (0, json_1.JsonMerge)(json, patch);
|
|
85
|
-
const diffResult = await this.diff.DiffPath(key, mergedJson);
|
|
91
|
+
const diffResult = await this.diff.DiffPath(key, mergedJson, false);
|
|
86
92
|
this.UpdateRootMap(diffResult);
|
|
87
93
|
});
|
|
88
94
|
}
|
package/Store/Store/storeSync.js
CHANGED
|
@@ -68,7 +68,7 @@ class StoreSync extends store_1.Store {
|
|
|
68
68
|
throw "Unable to patch undefined value";
|
|
69
69
|
const json = observableNode_1.ObservableNode.Unwrap(value);
|
|
70
70
|
const mergedJson = (0, json_1.JsonMerge)(json, patch);
|
|
71
|
-
const diffResult = this.diff.DiffPath(key, mergedJson);
|
|
71
|
+
const diffResult = this.diff.DiffPath(key, mergedJson, false);
|
|
72
72
|
this.UpdateRootMap(diffResult);
|
|
73
73
|
}
|
|
74
74
|
/**
|
|
@@ -46,6 +46,9 @@ export declare namespace ObservableNode {
|
|
|
46
46
|
* @param prop Optional property name or index to touch a specific nested property.
|
|
47
47
|
*/
|
|
48
48
|
function Update<T>(value: T, prop?: keyof T): void;
|
|
49
|
+
function Assign<T>(value: T, prop: keyof T, propValue: any): void;
|
|
50
|
+
function Touch<T>(value: T, prop?: keyof T): void;
|
|
51
|
+
function Read<T>(value: T, prop: keyof T): void;
|
|
49
52
|
/**
|
|
50
53
|
* Merges a new value into an observable node in-place, preserving the node's object identity.
|
|
51
54
|
* Computes the diff between the node's current value and the provided value, then applies only
|
|
@@ -8,7 +8,8 @@ const NODE_VALUE = Symbol("NODE_VALUE");
|
|
|
8
8
|
const NODE_PROXY = Symbol("NODE_PROXY");
|
|
9
9
|
const toJSON = "toJSON";
|
|
10
10
|
const IS_NODE = Symbol("IS_NODE");
|
|
11
|
-
const OBJECT_SCOPE = Symbol("
|
|
11
|
+
const OBJECT_SCOPE = Symbol("OBJECT_SCOPE");
|
|
12
|
+
const ITERATOR_SCOPE = Symbol("ITERATOR_SCOPE");
|
|
12
13
|
function Identity(value) {
|
|
13
14
|
return value;
|
|
14
15
|
}
|
|
@@ -31,15 +32,19 @@ function getOwnPropertyDescriptorArray(target, prop) {
|
|
|
31
32
|
};
|
|
32
33
|
}
|
|
33
34
|
function has(value, prop) {
|
|
35
|
+
observableScope_1.ObservableScope.Touch(value[ITERATOR_SCOPE]);
|
|
34
36
|
return Object.hasOwn(value[NODE_VALUE], prop);
|
|
35
37
|
}
|
|
36
38
|
function hasArray(value, prop) {
|
|
39
|
+
observableScope_1.ObservableScope.Touch(value[ITERATOR_SCOPE]);
|
|
37
40
|
return Object.hasOwn(value[NODE_VALUE], prop);
|
|
38
41
|
}
|
|
39
42
|
function ownKeys(value) {
|
|
43
|
+
observableScope_1.ObservableScope.Touch(value[ITERATOR_SCOPE]);
|
|
40
44
|
return Object.keys(value[NODE_VALUE]);
|
|
41
45
|
}
|
|
42
46
|
function ownKeysArray(value) {
|
|
47
|
+
observableScope_1.ObservableScope.Touch(value[ITERATOR_SCOPE]);
|
|
43
48
|
return Object.keys(value[NODE_VALUE]);
|
|
44
49
|
}
|
|
45
50
|
function UnwrapProxy(value, type = (0, json_2.JsonType)(value)) {
|
|
@@ -85,6 +90,9 @@ function CloneProxy(value, type = (0, json_2.JsonType)(value)) {
|
|
|
85
90
|
}
|
|
86
91
|
return value;
|
|
87
92
|
}
|
|
93
|
+
function GetPropertyScope(object, prop) {
|
|
94
|
+
return object[prop] ??= observableScope_1.ObservableScope.Basic(Property.bind(null, object[NODE_VALUE], prop));
|
|
95
|
+
}
|
|
88
96
|
function CreateProxyFactory(alias) {
|
|
89
97
|
function ToJsonCopy(value) {
|
|
90
98
|
const type = (0, json_2.JsonType)(value);
|
|
@@ -204,7 +212,8 @@ function CreateProxyFactory(alias) {
|
|
|
204
212
|
function CreateObjectProxy(value) {
|
|
205
213
|
const wrapper = {
|
|
206
214
|
[NODE_VALUE]: value,
|
|
207
|
-
[NODE_PROXY]: null
|
|
215
|
+
[NODE_PROXY]: null,
|
|
216
|
+
[ITERATOR_SCOPE]: observableScope_1.ObservableScope.Basic(Identity.bind(null, value))
|
|
208
217
|
};
|
|
209
218
|
const proxy = wrapper[NODE_PROXY] = new Proxy(wrapper, {
|
|
210
219
|
get: ObjectProxyGetter,
|
|
@@ -224,6 +233,7 @@ function CreateProxyFactory(alias) {
|
|
|
224
233
|
return true;
|
|
225
234
|
}
|
|
226
235
|
function ObjectProxyGetter(object, prop) {
|
|
236
|
+
observableScope_1.ObservableScope.Touch(object[OBJECT_SCOPE]);
|
|
227
237
|
switch (prop) {
|
|
228
238
|
case IS_NODE:
|
|
229
239
|
return true;
|
|
@@ -239,7 +249,7 @@ function CreateProxyFactory(alias) {
|
|
|
239
249
|
}
|
|
240
250
|
}
|
|
241
251
|
function GetAccessorValue(object, prop) {
|
|
242
|
-
const scope =
|
|
252
|
+
const scope = GetPropertyScope(object, prop);
|
|
243
253
|
const value = observableScope_1.ObservableScope.Value(scope);
|
|
244
254
|
return CreateProxyFromValue(value);
|
|
245
255
|
}
|
|
@@ -330,14 +340,51 @@ var ObservableNode;
|
|
|
330
340
|
* @param value The observable node to touch.
|
|
331
341
|
* @param prop Optional property name or index to touch a specific nested property.
|
|
332
342
|
*/
|
|
333
|
-
function Update(value, prop
|
|
343
|
+
function Update(value, prop) {
|
|
334
344
|
const wrapper = wrapperCache.get(value);
|
|
335
345
|
if (wrapper) {
|
|
336
|
-
const scope = Array.isArray(wrapper) ? wrapper[OBJECT_SCOPE] : wrapper[prop] ?? wrapper[
|
|
346
|
+
const scope = Array.isArray(wrapper) ? wrapper[OBJECT_SCOPE] : (prop && wrapper[prop]) ?? wrapper[ITERATOR_SCOPE];
|
|
337
347
|
observableScope_1.ObservableScope.Update(scope);
|
|
338
348
|
}
|
|
339
349
|
}
|
|
340
350
|
ObservableNode.Update = Update;
|
|
351
|
+
function Assign(value, prop, propValue) {
|
|
352
|
+
const propExists = Object.hasOwn(value, prop);
|
|
353
|
+
value[prop] = propValue;
|
|
354
|
+
const wrapper = wrapperCache.get(value);
|
|
355
|
+
if (wrapper) {
|
|
356
|
+
if (Array.isArray(wrapper))
|
|
357
|
+
observableScope_1.ObservableScope.Update(wrapper[OBJECT_SCOPE]);
|
|
358
|
+
else {
|
|
359
|
+
const scope = wrapper[prop];
|
|
360
|
+
if (scope) {
|
|
361
|
+
// A prop was just added to the object, fire the iterator scope
|
|
362
|
+
if (!propExists)
|
|
363
|
+
observableScope_1.ObservableScope.Update(wrapper[ITERATOR_SCOPE]);
|
|
364
|
+
observableScope_1.ObservableScope.Update(scope);
|
|
365
|
+
}
|
|
366
|
+
else
|
|
367
|
+
observableScope_1.ObservableScope.Update(wrapper[ITERATOR_SCOPE]);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
ObservableNode.Assign = Assign;
|
|
372
|
+
function Touch(value, prop) {
|
|
373
|
+
const wrapper = wrapperCache.get(value);
|
|
374
|
+
if (wrapper) {
|
|
375
|
+
const scope = prop && GetPropertyScope(wrapper, prop) || wrapper[OBJECT_SCOPE];
|
|
376
|
+
observableScope_1.ObservableScope.Touch(scope);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
ObservableNode.Touch = Touch;
|
|
380
|
+
function Read(value, prop) {
|
|
381
|
+
const wrapper = wrapperCache.get(value);
|
|
382
|
+
if (wrapper) {
|
|
383
|
+
const scope = Array.isArray(wrapper) ? wrapper[OBJECT_SCOPE] : (prop && wrapper[prop]) ?? wrapper[ITERATOR_SCOPE];
|
|
384
|
+
observableScope_1.ObservableScope.Update(scope);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
ObservableNode.Read = Read;
|
|
341
388
|
/**
|
|
342
389
|
* Merges a new value into an observable node in-place, preserving the node's object identity.
|
|
343
390
|
* Computes the diff between the node's current value and the provided value, then applies only
|
|
@@ -372,7 +419,7 @@ var ObservableNode;
|
|
|
372
419
|
const root = rootNode[NODE_VALUE];
|
|
373
420
|
if (diffResult.length === 1 && diffResult[0].path.length === 0) {
|
|
374
421
|
// Replacing rootNode
|
|
375
|
-
const rootPatch = diffResult[0].value;
|
|
422
|
+
const rootPatch = (0, json_1.JsonDeepClone)(diffResult[0].value);
|
|
376
423
|
const rootType = (0, json_2.JsonType)(root);
|
|
377
424
|
const rootPatchType = (0, json_2.JsonType)(rootPatch);
|
|
378
425
|
if (rootType !== rootPatchType)
|
|
@@ -418,8 +465,9 @@ var ObservableNode;
|
|
|
418
465
|
}
|
|
419
466
|
}
|
|
420
467
|
const assignValue = pathTuples[y][1];
|
|
421
|
-
assignValue[path[y]] = value;
|
|
422
|
-
ObservableNode.Update(assignValue, path[y]);
|
|
468
|
+
// assignValue[path[y]] = JsonDeepClone(value);
|
|
469
|
+
// ObservableNode.Update(assignValue, path[y]);
|
|
470
|
+
ObservableNode.Assign(assignValue, path[y], (0, json_1.JsonDeepClone)(value));
|
|
423
471
|
}
|
|
424
472
|
}
|
|
425
473
|
ObservableNode.ApplyDiff = ApplyDiff;
|
package/Store/index.d.ts
CHANGED
package/Store/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ObservableNode = exports.ObservableScope = exports.StoreAsync = exports.StoreSync = void 0;
|
|
3
|
+
exports.ObservableNode = exports.ObservableScope = exports.StoreAsync = exports.StoreSync = exports.Store = void 0;
|
|
4
|
+
var store_1 = require("./Store/store");
|
|
5
|
+
Object.defineProperty(exports, "Store", { enumerable: true, get: function () { return store_1.Store; } });
|
|
4
6
|
var storeSync_1 = require("./Store/storeSync");
|
|
5
7
|
Object.defineProperty(exports, "StoreSync", { enumerable: true, get: function () { return storeSync_1.StoreSync; } });
|
|
6
8
|
var storeAsync_1 = require("./Store/storeAsync");
|
package/Utils/asyncQueue.d.ts
CHANGED
package/Utils/asyncQueue.js
CHANGED
|
@@ -10,6 +10,13 @@ class AsyncQueue {
|
|
|
10
10
|
this.running = false;
|
|
11
11
|
this.queue = list_1.List.Create();
|
|
12
12
|
}
|
|
13
|
+
Flush() {
|
|
14
|
+
const ret = new Promise(resolve => {
|
|
15
|
+
list_1.List.Add(this.queue, resolve);
|
|
16
|
+
});
|
|
17
|
+
this.Start();
|
|
18
|
+
return ret;
|
|
19
|
+
}
|
|
13
20
|
/**
|
|
14
21
|
* Adds a callback to the queue and returns a promise that resolves when it executes.
|
|
15
22
|
* @template T - The type the callback promise resolves to
|