@frelseren/promise-task-maps 0.1.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/README.md +150 -0
- package/dist/index.cjs +223 -0
- package/dist/index.d.cts +18 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +195 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Promise Task Maps
|
|
2
|
+
|
|
3
|
+
Promise-based task mapping operators inspired by RxJS: `switchMap`, `concatMap`, `mergeMap`, and `exhaustMap`.
|
|
4
|
+
|
|
5
|
+
This package is framework-agnostic and works in browser environments, including LWC.
|
|
6
|
+
|
|
7
|
+
## Why this library
|
|
8
|
+
|
|
9
|
+
RxJS operators are excellent for stream pipelines, but many codebases only need Promise-based task control:
|
|
10
|
+
|
|
11
|
+
- `switchMap`: keep only the latest task result
|
|
12
|
+
- `concatMap`: queue and execute tasks one-by-one
|
|
13
|
+
- `mergeMap`: run tasks concurrently with an optional limit
|
|
14
|
+
- `exhaustMap`: ignore new tasks while one is in progress
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @nv/promise-task-maps
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## API
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
type PromiseTask<I, O> = (input: I, context: { signal: AbortSignal; callId: number }) => Promise<O> | O;
|
|
26
|
+
|
|
27
|
+
type TaskRunner<I, O> = {
|
|
28
|
+
execute(input: I): Promise<O | undefined>;
|
|
29
|
+
cancel(reason?: string): void;
|
|
30
|
+
};
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { createSwitchMap } from "@nv/promise-task-maps";
|
|
37
|
+
|
|
38
|
+
const searchUsers = createSwitchMap(async (query: string, { signal }) => {
|
|
39
|
+
const response = await fetch(`/api/users?q=${encodeURIComponent(query)}`, { signal });
|
|
40
|
+
return response.json();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Typing fast in a search box calls execute repeatedly.
|
|
44
|
+
// Older in-flight calls are canceled/ignored; only the latest resolves with data.
|
|
45
|
+
const result = await searchUsers.execute("nic");
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### concatMap example
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { createConcatMap } from "@nv/promise-task-maps";
|
|
52
|
+
|
|
53
|
+
const saveDraft = createConcatMap(async (payload: { id: string; body: string }, { signal }) => {
|
|
54
|
+
const response = await fetch(`/api/drafts/${payload.id}`, {
|
|
55
|
+
method: "PUT",
|
|
56
|
+
headers: { "Content-Type": "application/json" },
|
|
57
|
+
body: JSON.stringify({ body: payload.body }),
|
|
58
|
+
signal
|
|
59
|
+
});
|
|
60
|
+
return response.ok;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Calls are queued and executed in order to avoid race conditions.
|
|
64
|
+
await Promise.all([
|
|
65
|
+
saveDraft.execute({ id: "a1", body: "v1" }),
|
|
66
|
+
saveDraft.execute({ id: "a1", body: "v2" }),
|
|
67
|
+
saveDraft.execute({ id: "a1", body: "v3" })
|
|
68
|
+
]);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### mergeMap example
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { createMergeMap } from "@nv/promise-task-maps";
|
|
75
|
+
|
|
76
|
+
const fetchProfile = createMergeMap(
|
|
77
|
+
async (userId: string, { signal }) => {
|
|
78
|
+
const response = await fetch(`/api/users/${userId}`, { signal });
|
|
79
|
+
return response.json();
|
|
80
|
+
},
|
|
81
|
+
{ concurrency: 3 }
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Up to 3 requests run in parallel; remaining calls wait in queue.
|
|
85
|
+
const profiles = await Promise.all([
|
|
86
|
+
fetchProfile.execute("u1"),
|
|
87
|
+
fetchProfile.execute("u2"),
|
|
88
|
+
fetchProfile.execute("u3"),
|
|
89
|
+
fetchProfile.execute("u4")
|
|
90
|
+
]);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### exhaustMap example
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { createExhaustMap } from "@nv/promise-task-maps";
|
|
97
|
+
|
|
98
|
+
const submitOrder = createExhaustMap(async (order: { items: string[] }, { signal }) => {
|
|
99
|
+
const response = await fetch("/api/orders", {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: { "Content-Type": "application/json" },
|
|
102
|
+
body: JSON.stringify(order),
|
|
103
|
+
signal
|
|
104
|
+
});
|
|
105
|
+
return response.json();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Double-click protection: second call returns undefined while first is in flight.
|
|
109
|
+
const firstAttempt = submitOrder.execute({ items: ["sku-1"] });
|
|
110
|
+
const secondAttempt = submitOrder.execute({ items: ["sku-1"] });
|
|
111
|
+
|
|
112
|
+
await firstAttempt; // order accepted
|
|
113
|
+
await secondAttempt; // undefined
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## LWC example
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { LightningElement } from "lwc";
|
|
120
|
+
import { createSwitchMap } from "@nv/promise-task-maps";
|
|
121
|
+
|
|
122
|
+
export default class UserLookup extends LightningElement {
|
|
123
|
+
users = [];
|
|
124
|
+
|
|
125
|
+
lookup = createSwitchMap(async (query: string, { signal }) => {
|
|
126
|
+
const res = await fetch(`/services/apexrest/users?q=${encodeURIComponent(query)}`, { signal });
|
|
127
|
+
return res.json();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
async handleInput(event: Event) {
|
|
131
|
+
const target = event.target as HTMLInputElement;
|
|
132
|
+
const data = await this.lookup.execute(target.value);
|
|
133
|
+
if (data) {
|
|
134
|
+
this.users = data;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
disconnectedCallback() {
|
|
139
|
+
this.lookup.cancel("component disconnected");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Development
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
npm install
|
|
148
|
+
npm test
|
|
149
|
+
npm run build
|
|
150
|
+
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
createConcatMap: () => createConcatMap,
|
|
24
|
+
createExhaustMap: () => createExhaustMap,
|
|
25
|
+
createMergeMap: () => createMergeMap,
|
|
26
|
+
createSwitchMap: () => createSwitchMap
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
var isAbortError = (error) => {
|
|
30
|
+
return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
|
|
31
|
+
};
|
|
32
|
+
var createController = () => new AbortController();
|
|
33
|
+
function createSwitchMap(task) {
|
|
34
|
+
let latestCallId = 0;
|
|
35
|
+
let latestController = createController();
|
|
36
|
+
return {
|
|
37
|
+
async execute(input) {
|
|
38
|
+
latestCallId += 1;
|
|
39
|
+
const callId = latestCallId;
|
|
40
|
+
latestController.abort();
|
|
41
|
+
latestController = createController();
|
|
42
|
+
try {
|
|
43
|
+
const result = await task(input, { signal: latestController.signal, callId });
|
|
44
|
+
return callId === latestCallId ? result : void 0;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (callId !== latestCallId || isAbortError(error)) {
|
|
47
|
+
return void 0;
|
|
48
|
+
}
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
cancel(reason) {
|
|
53
|
+
latestCallId += 1;
|
|
54
|
+
latestController.abort(reason);
|
|
55
|
+
latestController = createController();
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function createConcatMap(task) {
|
|
60
|
+
let active = false;
|
|
61
|
+
let currentCallId = 0;
|
|
62
|
+
let currentController = createController();
|
|
63
|
+
const queue = [];
|
|
64
|
+
const processQueue = async () => {
|
|
65
|
+
if (active) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const next = queue.shift();
|
|
69
|
+
if (!next) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
active = true;
|
|
73
|
+
currentCallId = next.callId;
|
|
74
|
+
currentController = createController();
|
|
75
|
+
try {
|
|
76
|
+
const result = await task(next.input, {
|
|
77
|
+
signal: currentController.signal,
|
|
78
|
+
callId: next.callId
|
|
79
|
+
});
|
|
80
|
+
next.resolve(result);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (isAbortError(error)) {
|
|
83
|
+
next.resolve(void 0);
|
|
84
|
+
} else {
|
|
85
|
+
next.reject(error);
|
|
86
|
+
}
|
|
87
|
+
} finally {
|
|
88
|
+
active = false;
|
|
89
|
+
void processQueue();
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
return {
|
|
93
|
+
execute(input) {
|
|
94
|
+
const callId = currentCallId + queue.length + 1;
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
queue.push({ input, resolve, reject, callId });
|
|
97
|
+
void processQueue();
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
cancel(reason) {
|
|
101
|
+
currentController.abort(reason);
|
|
102
|
+
while (queue.length > 0) {
|
|
103
|
+
const queued = queue.shift();
|
|
104
|
+
queued?.resolve(void 0);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function createMergeMap(task, options = {}) {
|
|
110
|
+
const concurrency = options.concurrency ?? Number.POSITIVE_INFINITY;
|
|
111
|
+
if (!Number.isFinite(concurrency) && concurrency !== Number.POSITIVE_INFINITY) {
|
|
112
|
+
throw new Error("mergeMap concurrency must be a finite positive number or Infinity");
|
|
113
|
+
}
|
|
114
|
+
if (concurrency <= 0) {
|
|
115
|
+
throw new Error("mergeMap concurrency must be greater than 0");
|
|
116
|
+
}
|
|
117
|
+
let callId = 0;
|
|
118
|
+
let activeCount = 0;
|
|
119
|
+
let generation = 0;
|
|
120
|
+
const queue = [];
|
|
121
|
+
const activeControllers = /* @__PURE__ */ new Map();
|
|
122
|
+
const pumpQueue = () => {
|
|
123
|
+
while (activeCount < concurrency && queue.length > 0) {
|
|
124
|
+
const next = queue.shift();
|
|
125
|
+
if (!next) {
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
if (next.generation !== generation) {
|
|
129
|
+
next.resolve(void 0);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
activeCount += 1;
|
|
133
|
+
const controller = createController();
|
|
134
|
+
activeControllers.set(next.callId, controller);
|
|
135
|
+
void (async () => {
|
|
136
|
+
try {
|
|
137
|
+
const result = await task(next.input, {
|
|
138
|
+
signal: controller.signal,
|
|
139
|
+
callId: next.callId
|
|
140
|
+
});
|
|
141
|
+
next.resolve(result);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (isAbortError(error) || next.generation !== generation) {
|
|
144
|
+
next.resolve(void 0);
|
|
145
|
+
} else {
|
|
146
|
+
next.reject(error);
|
|
147
|
+
}
|
|
148
|
+
} finally {
|
|
149
|
+
activeCount -= 1;
|
|
150
|
+
activeControllers.delete(next.callId);
|
|
151
|
+
pumpQueue();
|
|
152
|
+
}
|
|
153
|
+
})();
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
return {
|
|
157
|
+
execute(input) {
|
|
158
|
+
callId += 1;
|
|
159
|
+
const thisCallId = callId;
|
|
160
|
+
const thisGeneration = generation;
|
|
161
|
+
return new Promise((resolve, reject) => {
|
|
162
|
+
queue.push({
|
|
163
|
+
input,
|
|
164
|
+
resolve,
|
|
165
|
+
reject,
|
|
166
|
+
callId: thisCallId,
|
|
167
|
+
generation: thisGeneration
|
|
168
|
+
});
|
|
169
|
+
pumpQueue();
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
cancel(reason) {
|
|
173
|
+
generation += 1;
|
|
174
|
+
for (const controller of activeControllers.values()) {
|
|
175
|
+
controller.abort(reason);
|
|
176
|
+
}
|
|
177
|
+
while (queue.length > 0) {
|
|
178
|
+
const queued = queue.shift();
|
|
179
|
+
queued?.resolve(void 0);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function createExhaustMap(task) {
|
|
185
|
+
let active = false;
|
|
186
|
+
let callId = 0;
|
|
187
|
+
let generation = 0;
|
|
188
|
+
let controller = createController();
|
|
189
|
+
return {
|
|
190
|
+
async execute(input) {
|
|
191
|
+
if (active) {
|
|
192
|
+
return void 0;
|
|
193
|
+
}
|
|
194
|
+
callId += 1;
|
|
195
|
+
const thisCallId = callId;
|
|
196
|
+
const thisGeneration = generation;
|
|
197
|
+
active = true;
|
|
198
|
+
controller = createController();
|
|
199
|
+
try {
|
|
200
|
+
const result = await task(input, { signal: controller.signal, callId: thisCallId });
|
|
201
|
+
return thisGeneration === generation ? result : void 0;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (isAbortError(error) || thisGeneration !== generation) {
|
|
204
|
+
return void 0;
|
|
205
|
+
}
|
|
206
|
+
throw error;
|
|
207
|
+
} finally {
|
|
208
|
+
active = false;
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
cancel(reason) {
|
|
212
|
+
generation += 1;
|
|
213
|
+
controller.abort(reason);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
218
|
+
0 && (module.exports = {
|
|
219
|
+
createConcatMap,
|
|
220
|
+
createExhaustMap,
|
|
221
|
+
createMergeMap,
|
|
222
|
+
createSwitchMap
|
|
223
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface TaskContext {
|
|
2
|
+
signal: AbortSignal;
|
|
3
|
+
callId: number;
|
|
4
|
+
}
|
|
5
|
+
type PromiseTask<I, O> = (input: I, context: TaskContext) => Promise<O> | O;
|
|
6
|
+
interface TaskRunner<I, O> {
|
|
7
|
+
execute(input: I): Promise<O | undefined>;
|
|
8
|
+
cancel(reason?: string): void;
|
|
9
|
+
}
|
|
10
|
+
interface MergeMapOptions {
|
|
11
|
+
concurrency?: number;
|
|
12
|
+
}
|
|
13
|
+
declare function createSwitchMap<I, O>(task: PromiseTask<I, O>): TaskRunner<I, O>;
|
|
14
|
+
declare function createConcatMap<I, O>(task: PromiseTask<I, O>): TaskRunner<I, O>;
|
|
15
|
+
declare function createMergeMap<I, O>(task: PromiseTask<I, O>, options?: MergeMapOptions): TaskRunner<I, O>;
|
|
16
|
+
declare function createExhaustMap<I, O>(task: PromiseTask<I, O>): TaskRunner<I, O>;
|
|
17
|
+
|
|
18
|
+
export { type MergeMapOptions, type PromiseTask, type TaskContext, type TaskRunner, createConcatMap, createExhaustMap, createMergeMap, createSwitchMap };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface TaskContext {
|
|
2
|
+
signal: AbortSignal;
|
|
3
|
+
callId: number;
|
|
4
|
+
}
|
|
5
|
+
type PromiseTask<I, O> = (input: I, context: TaskContext) => Promise<O> | O;
|
|
6
|
+
interface TaskRunner<I, O> {
|
|
7
|
+
execute(input: I): Promise<O | undefined>;
|
|
8
|
+
cancel(reason?: string): void;
|
|
9
|
+
}
|
|
10
|
+
interface MergeMapOptions {
|
|
11
|
+
concurrency?: number;
|
|
12
|
+
}
|
|
13
|
+
declare function createSwitchMap<I, O>(task: PromiseTask<I, O>): TaskRunner<I, O>;
|
|
14
|
+
declare function createConcatMap<I, O>(task: PromiseTask<I, O>): TaskRunner<I, O>;
|
|
15
|
+
declare function createMergeMap<I, O>(task: PromiseTask<I, O>, options?: MergeMapOptions): TaskRunner<I, O>;
|
|
16
|
+
declare function createExhaustMap<I, O>(task: PromiseTask<I, O>): TaskRunner<I, O>;
|
|
17
|
+
|
|
18
|
+
export { type MergeMapOptions, type PromiseTask, type TaskContext, type TaskRunner, createConcatMap, createExhaustMap, createMergeMap, createSwitchMap };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var isAbortError = (error) => {
|
|
3
|
+
return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
|
|
4
|
+
};
|
|
5
|
+
var createController = () => new AbortController();
|
|
6
|
+
function createSwitchMap(task) {
|
|
7
|
+
let latestCallId = 0;
|
|
8
|
+
let latestController = createController();
|
|
9
|
+
return {
|
|
10
|
+
async execute(input) {
|
|
11
|
+
latestCallId += 1;
|
|
12
|
+
const callId = latestCallId;
|
|
13
|
+
latestController.abort();
|
|
14
|
+
latestController = createController();
|
|
15
|
+
try {
|
|
16
|
+
const result = await task(input, { signal: latestController.signal, callId });
|
|
17
|
+
return callId === latestCallId ? result : void 0;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (callId !== latestCallId || isAbortError(error)) {
|
|
20
|
+
return void 0;
|
|
21
|
+
}
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
cancel(reason) {
|
|
26
|
+
latestCallId += 1;
|
|
27
|
+
latestController.abort(reason);
|
|
28
|
+
latestController = createController();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function createConcatMap(task) {
|
|
33
|
+
let active = false;
|
|
34
|
+
let currentCallId = 0;
|
|
35
|
+
let currentController = createController();
|
|
36
|
+
const queue = [];
|
|
37
|
+
const processQueue = async () => {
|
|
38
|
+
if (active) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const next = queue.shift();
|
|
42
|
+
if (!next) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
active = true;
|
|
46
|
+
currentCallId = next.callId;
|
|
47
|
+
currentController = createController();
|
|
48
|
+
try {
|
|
49
|
+
const result = await task(next.input, {
|
|
50
|
+
signal: currentController.signal,
|
|
51
|
+
callId: next.callId
|
|
52
|
+
});
|
|
53
|
+
next.resolve(result);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (isAbortError(error)) {
|
|
56
|
+
next.resolve(void 0);
|
|
57
|
+
} else {
|
|
58
|
+
next.reject(error);
|
|
59
|
+
}
|
|
60
|
+
} finally {
|
|
61
|
+
active = false;
|
|
62
|
+
void processQueue();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
execute(input) {
|
|
67
|
+
const callId = currentCallId + queue.length + 1;
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
queue.push({ input, resolve, reject, callId });
|
|
70
|
+
void processQueue();
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
cancel(reason) {
|
|
74
|
+
currentController.abort(reason);
|
|
75
|
+
while (queue.length > 0) {
|
|
76
|
+
const queued = queue.shift();
|
|
77
|
+
queued?.resolve(void 0);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function createMergeMap(task, options = {}) {
|
|
83
|
+
const concurrency = options.concurrency ?? Number.POSITIVE_INFINITY;
|
|
84
|
+
if (!Number.isFinite(concurrency) && concurrency !== Number.POSITIVE_INFINITY) {
|
|
85
|
+
throw new Error("mergeMap concurrency must be a finite positive number or Infinity");
|
|
86
|
+
}
|
|
87
|
+
if (concurrency <= 0) {
|
|
88
|
+
throw new Error("mergeMap concurrency must be greater than 0");
|
|
89
|
+
}
|
|
90
|
+
let callId = 0;
|
|
91
|
+
let activeCount = 0;
|
|
92
|
+
let generation = 0;
|
|
93
|
+
const queue = [];
|
|
94
|
+
const activeControllers = /* @__PURE__ */ new Map();
|
|
95
|
+
const pumpQueue = () => {
|
|
96
|
+
while (activeCount < concurrency && queue.length > 0) {
|
|
97
|
+
const next = queue.shift();
|
|
98
|
+
if (!next) {
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
if (next.generation !== generation) {
|
|
102
|
+
next.resolve(void 0);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
activeCount += 1;
|
|
106
|
+
const controller = createController();
|
|
107
|
+
activeControllers.set(next.callId, controller);
|
|
108
|
+
void (async () => {
|
|
109
|
+
try {
|
|
110
|
+
const result = await task(next.input, {
|
|
111
|
+
signal: controller.signal,
|
|
112
|
+
callId: next.callId
|
|
113
|
+
});
|
|
114
|
+
next.resolve(result);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (isAbortError(error) || next.generation !== generation) {
|
|
117
|
+
next.resolve(void 0);
|
|
118
|
+
} else {
|
|
119
|
+
next.reject(error);
|
|
120
|
+
}
|
|
121
|
+
} finally {
|
|
122
|
+
activeCount -= 1;
|
|
123
|
+
activeControllers.delete(next.callId);
|
|
124
|
+
pumpQueue();
|
|
125
|
+
}
|
|
126
|
+
})();
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
return {
|
|
130
|
+
execute(input) {
|
|
131
|
+
callId += 1;
|
|
132
|
+
const thisCallId = callId;
|
|
133
|
+
const thisGeneration = generation;
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
queue.push({
|
|
136
|
+
input,
|
|
137
|
+
resolve,
|
|
138
|
+
reject,
|
|
139
|
+
callId: thisCallId,
|
|
140
|
+
generation: thisGeneration
|
|
141
|
+
});
|
|
142
|
+
pumpQueue();
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
cancel(reason) {
|
|
146
|
+
generation += 1;
|
|
147
|
+
for (const controller of activeControllers.values()) {
|
|
148
|
+
controller.abort(reason);
|
|
149
|
+
}
|
|
150
|
+
while (queue.length > 0) {
|
|
151
|
+
const queued = queue.shift();
|
|
152
|
+
queued?.resolve(void 0);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function createExhaustMap(task) {
|
|
158
|
+
let active = false;
|
|
159
|
+
let callId = 0;
|
|
160
|
+
let generation = 0;
|
|
161
|
+
let controller = createController();
|
|
162
|
+
return {
|
|
163
|
+
async execute(input) {
|
|
164
|
+
if (active) {
|
|
165
|
+
return void 0;
|
|
166
|
+
}
|
|
167
|
+
callId += 1;
|
|
168
|
+
const thisCallId = callId;
|
|
169
|
+
const thisGeneration = generation;
|
|
170
|
+
active = true;
|
|
171
|
+
controller = createController();
|
|
172
|
+
try {
|
|
173
|
+
const result = await task(input, { signal: controller.signal, callId: thisCallId });
|
|
174
|
+
return thisGeneration === generation ? result : void 0;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (isAbortError(error) || thisGeneration !== generation) {
|
|
177
|
+
return void 0;
|
|
178
|
+
}
|
|
179
|
+
throw error;
|
|
180
|
+
} finally {
|
|
181
|
+
active = false;
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
cancel(reason) {
|
|
185
|
+
generation += 1;
|
|
186
|
+
controller.abort(reason);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
export {
|
|
191
|
+
createConcatMap,
|
|
192
|
+
createExhaustMap,
|
|
193
|
+
createMergeMap,
|
|
194
|
+
createSwitchMap
|
|
195
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@frelseren/promise-task-maps",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "RxJS-like mapping operators for Promise-based task management",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.cjs",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"sideEffects": false,
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18"
|
|
12
|
+
},
|
|
13
|
+
"author": {
|
|
14
|
+
"name": "Nikita Verkhoshintcev",
|
|
15
|
+
"email": "nikita@digitalflask.com",
|
|
16
|
+
"url": "https://digitalflask.com"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://digitalflask.com",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/Frelseren/promise-task-maps.git"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/Frelseren/promise-task-maps/issues"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"import": "./dist/index.js",
|
|
33
|
+
"require": "./dist/index.cjs"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsup src/index.ts --dts --format esm,cjs --clean",
|
|
41
|
+
"prepublishOnly": "npm test && npm run build",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"test:watch": "vitest"
|
|
44
|
+
},
|
|
45
|
+
"keywords": [
|
|
46
|
+
"promise",
|
|
47
|
+
"task",
|
|
48
|
+
"switchMap",
|
|
49
|
+
"concatMap",
|
|
50
|
+
"mergeMap",
|
|
51
|
+
"exhaustMap",
|
|
52
|
+
"lwc"
|
|
53
|
+
],
|
|
54
|
+
"license": "MIT",
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"tsup": "^8.3.0",
|
|
57
|
+
"typescript": "^5.7.0",
|
|
58
|
+
"vitest": "^2.1.8"
|
|
59
|
+
}
|
|
60
|
+
}
|