/
mod.ts
168 lines (150 loc) · 4.52 KB
/
mod.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
/**
* @module
*
* This module provides a simple API for batching operations. You create a {@link LoadFn loader} function, and then use the {@link load} function to load values for a given key.
*
* @example
* ```ts
* async function loader(keys: string[]) {
* // expect keys to be ['bar', 'baz'] (deduped)
* return keys.map(key => 'foo' + key);
* }
*
* const values = await Promise.all([
* load(loader, 'bar'),
* load(loader, 'bar'),
* load(loader, 'baz'),
* ]);
*
* console.log(values); // ['foobar', 'foobar', 'foobaz']
* ```
*/
import { identify } from 'object-identity';
/**
* A loader function used to provide values for a given set of keys.
*
* Returning a value for a key, will resolve its associated promise, while returning an error will reject it.
*
* > [!WARNING]
* > A loader function must provide a value for each key in the given set of keys.
* > Even if this value is an error, it must be provided.
*/
export type LoadFn<T, K = string> = (keys: K[]) => Promise<(T | Error)[]>;
// We keep a weakly held refernce to the user provided load function,
// and batch all operations against that function.
let batchContainer = new WeakMap<LoadFn<any, any>, Batch<any, any>>();
/**
* Load a value for a given key, against this loader function. We will batch against this loader function. Ensure that the loaderFn is the same reference between calls.
*
* @example
* ```ts
* async function loader(keys: string[]) {
* // expect keys to be ['bar', 'baz'] (deduped)
* return keys.map(key => 'foo' + key);
* }
*
* const values = await Promise.all([
* load(loader, 'bar'),
* load(loader, 'bar'),
* load(loader, 'baz'),
* ]);
*
* console.log(values); // ['foobar', 'foobar', 'foobaz']
* ```
*
* @example Using a custom identity function
* ```ts
* async function loader(keys: {query, variables}[]) {
* return keys.map((payload) => fetch(
* '/graphql',
* { body: JSON.stringify(payload) }
* ));
* }
*
* function load_query(query: string, variables: object) {
* // where request_id returns a string of the operation_id and possible `jsr:@mr/object-identity` identity`
* return load(loader, { query, variables }, request_id(query, variables));
* }
*
* const values = await Promise.all([
* load_query('query { foo }', {}),
* load_query('query { foo }', {}),
* load_query('query { bar }', {}),
* ]);
*/
export function load<T, K = string>(
loadFn: LoadFn<T, K>,
key: K,
identity: string = identify(key),
): Promise<T> {
let batch = batchContainer.get(loadFn);
let tasks: Task<T>[];
let keys: K[];
if (!batch) {
batchContainer.set(loadFn, batch = [[], keys = [], tasks = []]);
// Once we know we have a fresh batch, we schedule this batch to run after
// all currently queued microtasks.
queueMicrotask(function () {
let tmp, i = 0;
// As soon as we start processing this batch, we need to delete this
// batch from our container. This is because we want to ensure that
// any new requests for this batch will be added to a new batch.
batchContainer.delete(loadFn);
loadFn(keys).then(function (values) {
if (values.length !== tasks.length) {
return reject(new TypeError('same length mismatch'));
}
for (
;
(tmp = values[i++]), i <= values.length;
tmp instanceof Error ? tasks[i - 1].r(tmp) : tasks[i - 1].s(tmp)
);
}, reject);
function reject(error: Error) {
for (; (tmp = tasks[i++]); tmp.r(error));
}
});
}
let b = batch[0]!.indexOf(identity);
// If the batch exists, return its promise, without enqueueing a new task.
if (~b) return batch[2][b].p;
let k = batch[0].push(identity) - 1;
let t = (batch[2][k] = {} as Task<T>);
batch[1][k] = key;
return (t.p = new Promise<T>(function (resolve, reject) {
t.s = resolve;
t.r = reject;
}));
}
/**
* A convenience method to create a loader function for you, that hold's onto the stable reference of the loader function.
*
* @example
* ```ts
* const load = factory(async function loader(keys: string[]) {
* return keys.map(key => 'foo' + key);
* });
*
* const values = await Promise.all([
* load('bar'),
* load('bar'),
* load('baz'),
* ]);
*
* console.log(values); // ['foobar', 'foobar', 'foobaz']
* ```
*/
export function factory<T, K = string>(
loadFn: LoadFn<T, K>,
): (key: K, identity?: string | undefined) => Promise<T> {
return (load<T, K>).bind(0, loadFn);
}
// ---
type Task<T> = {
p: Promise<T>;
/** resolve */
s(v: T): void;
/** reject */
r(e: Error): void;
};
type Batch<T, K> = [identies: string[], keys: K[], tasks: Task<T>[]];