@gentbajko/slopify 0.6.1 → 0.8.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 +36 -1
- package/SUBTITLES.md +3 -3
- package/dist/adapter-registry.js +12 -2
- package/dist/adapters/image/google.js +12 -15
- package/dist/adapters/image/models.js +72 -0
- package/dist/adapters/image/openai.js +3 -5
- package/dist/adapters/llm/catalogue-files.js +23 -0
- package/dist/adapters/llm/claude-code.js +3 -4
- package/dist/adapters/llm/codex-models.js +46 -0
- package/dist/adapters/llm/codex.js +7 -10
- package/dist/adapters/llm/gemini-models.js +85 -0
- package/dist/adapters/llm/gemini-workspace.js +24 -1
- package/dist/adapters/llm/gemini.js +4 -7
- package/dist/adapters/llm/openrouter.js +12 -2
- package/dist/adapters/tts/cartesia.js +9 -1
- package/dist/adapters/tts/elevenlabs.js +33 -2
- package/dist/adapters/tts/inworld-async.js +150 -0
- package/dist/adapters/tts/inworld-text.js +32 -0
- package/dist/adapters/tts/inworld.js +170 -0
- package/dist/adapters/tts/openai.js +31 -4
- package/dist/assets/models.yaml +470 -0
- package/dist/catalog/registry.js +147 -0
- package/dist/catalog/schema.js +91 -0
- package/dist/catalog/store.js +90 -0
- package/dist/catalog/validate.js +43 -0
- package/dist/edge/cli.js +5 -0
- package/dist/edge/events/hub.js +9 -2
- package/dist/edge/events/preview-cache.js +40 -0
- package/dist/edge/http/actions.js +2 -0
- package/dist/edge/http/app.js +28 -1
- package/dist/edge/http/audio-preview.js +46 -0
- package/dist/edge/http/planning.js +101 -0
- package/dist/edge/http/projects.js +4 -2
- package/dist/edge/http/providers.js +44 -0
- package/dist/edge/http/update.js +57 -0
- package/dist/edge/update-worker.js +44 -0
- package/dist/kernel/audio-preview.js +188 -0
- package/dist/kernel/db/migrations/0003-batch-queue.sql +11 -0
- package/dist/kernel/ports/llm.js +1 -0
- package/dist/kernel/ports/text.js +26 -0
- package/dist/kernel/runner/providers.js +80 -28
- package/dist/kernel/runner/queue.js +65 -0
- package/dist/main.js +142 -27
- package/dist/model-catalog.js +37 -0
- package/dist/slices/admission/repo.js +6 -1
- package/dist/slices/admission/start.js +9 -8
- package/dist/slices/article/continuation.js +8 -1
- package/dist/slices/article/segments.js +1 -0
- package/dist/slices/batch/index.js +82 -0
- package/dist/slices/control/index.js +6 -1
- package/dist/slices/control/providers.js +16 -1
- package/dist/slices/estimate/index.js +81 -0
- package/dist/slices/narration/live.js +21 -0
- package/dist/slices/narration/plan.js +58 -0
- package/dist/slices/narration/run.js +15 -36
- package/dist/slices/research/run.js +7 -0
- package/dist/slices/settings/model.js +2 -0
- package/dist/slices/settings/models.js +89 -0
- package/dist/slices/storage/staging.js +2 -1
- package/dist/slices/subtitles/captions.js +4 -1
- package/dist/slices/subtitles/layout.js +19 -0
- package/dist/slices/subtitles/model.js +9 -0
- package/dist/slices/subtitles/prepare.js +6 -1
- package/dist/slices/thumbnail/run.js +3 -0
- package/dist/updater/candidate.js +28 -0
- package/dist/updater/forward.js +35 -0
- package/dist/updater/install-flow.js +26 -0
- package/dist/updater/install.js +50 -0
- package/dist/updater/model.js +24 -0
- package/dist/updater/plan.js +135 -0
- package/dist/updater/readiness.js +15 -0
- package/dist/updater/registry.js +19 -0
- package/dist/updater/service.js +128 -0
- package/dist/updater/worker.js +136 -0
- package/dist/web/assets/index-6zz8telY.css +1 -0
- package/dist/web/assets/index-CbYEcBOa.js +130 -0
- package/dist/web/index.html +2 -2
- package/package.json +2 -1
- package/dist/web/assets/index-CNYs0noB.js +0 -130
- package/dist/web/assets/index-Mk0bvBg-.css +0 -1
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
const inactive = { append: () => { }, complete: () => { }, interrupt: () => { } };
|
|
3
|
+
// A preview is a disposable copy of bytes from the paid call already in flight.
|
|
4
|
+
// It never initiates synthesis, persists audio, or backpressures the provider.
|
|
5
|
+
// Readers pull from shared bounded storage, so a slow browser cannot grow a queue.
|
|
6
|
+
export function createAudioPreviewStore(limits = {}) {
|
|
7
|
+
const maxBytes = limits.maxBytes ?? 64 * 1024 * 1024;
|
|
8
|
+
const maxEntryBytes = limits.maxEntryBytes ?? 16 * 1024 * 1024;
|
|
9
|
+
const maxEntries = limits.maxEntries ?? 128;
|
|
10
|
+
const keepMs = limits.keepMs ?? 5 * 60_000;
|
|
11
|
+
const now = limits.now ?? Date.now;
|
|
12
|
+
const entries = new Map();
|
|
13
|
+
let retained = 0;
|
|
14
|
+
let closed = false;
|
|
15
|
+
const notify = (entry) => {
|
|
16
|
+
const listeners = [...entry.changed];
|
|
17
|
+
entry.changed.clear();
|
|
18
|
+
for (const listener of listeners)
|
|
19
|
+
listener();
|
|
20
|
+
};
|
|
21
|
+
const stop = (entry, state) => {
|
|
22
|
+
retained -= entry.bytes;
|
|
23
|
+
entry.bytes = 0;
|
|
24
|
+
entry.chunks.length = 0;
|
|
25
|
+
entry.state = state;
|
|
26
|
+
entry.endedAt = now();
|
|
27
|
+
notify(entry);
|
|
28
|
+
};
|
|
29
|
+
const remove = (entry) => {
|
|
30
|
+
stop(entry, "interrupted");
|
|
31
|
+
entries.delete(entry.id);
|
|
32
|
+
};
|
|
33
|
+
const prune = () => {
|
|
34
|
+
for (const entry of entries.values()) {
|
|
35
|
+
if (entry.endedAt !== undefined && now() - entry.endedAt >= keepMs)
|
|
36
|
+
remove(entry);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const expiry = setInterval(prune, Math.min(keepMs, 30_000));
|
|
40
|
+
expiry.unref();
|
|
41
|
+
const store = {
|
|
42
|
+
begin(projectId, key, label) {
|
|
43
|
+
if (closed)
|
|
44
|
+
return inactive;
|
|
45
|
+
prune();
|
|
46
|
+
for (const entry of entries.values()) {
|
|
47
|
+
if (entry.projectId === projectId && entry.key === key)
|
|
48
|
+
remove(entry);
|
|
49
|
+
}
|
|
50
|
+
// At capacity, completed previews yield to current generation. Otherwise
|
|
51
|
+
// this request has no preview; the normal durable narration still succeeds.
|
|
52
|
+
if (entries.size >= maxEntries) {
|
|
53
|
+
const oldest = [...entries.values()].find((entry) => entry.state !== "streaming");
|
|
54
|
+
if (oldest !== undefined)
|
|
55
|
+
remove(oldest);
|
|
56
|
+
}
|
|
57
|
+
if (entries.size >= maxEntries)
|
|
58
|
+
return inactive;
|
|
59
|
+
const entry = {
|
|
60
|
+
id: randomUUID(),
|
|
61
|
+
projectId,
|
|
62
|
+
key,
|
|
63
|
+
label,
|
|
64
|
+
chunks: [],
|
|
65
|
+
changed: new Set(),
|
|
66
|
+
state: "streaming",
|
|
67
|
+
bytes: 0,
|
|
68
|
+
endedAt: undefined,
|
|
69
|
+
};
|
|
70
|
+
entries.set(entry.id, entry);
|
|
71
|
+
const active = () => entries.get(entry.id) === entry && entry.state === "streaming";
|
|
72
|
+
return {
|
|
73
|
+
append(bytes) {
|
|
74
|
+
if (!active() || bytes.byteLength === 0)
|
|
75
|
+
return;
|
|
76
|
+
if (retained + bytes.byteLength > maxBytes ||
|
|
77
|
+
entry.bytes + bytes.byteLength > maxEntryBytes) {
|
|
78
|
+
stop(entry, "unavailable");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// Own these bounded buffers. A provider may reuse its read buffer.
|
|
82
|
+
for (let at = 0; at < bytes.byteLength; at += 64 * 1024) {
|
|
83
|
+
entry.chunks.push(bytes.slice(at, at + 64 * 1024));
|
|
84
|
+
}
|
|
85
|
+
retained += bytes.byteLength;
|
|
86
|
+
entry.bytes += bytes.byteLength;
|
|
87
|
+
notify(entry);
|
|
88
|
+
},
|
|
89
|
+
complete() {
|
|
90
|
+
if (!active())
|
|
91
|
+
return;
|
|
92
|
+
entry.state = "ready";
|
|
93
|
+
entry.endedAt = now();
|
|
94
|
+
notify(entry);
|
|
95
|
+
},
|
|
96
|
+
interrupt() {
|
|
97
|
+
if (active())
|
|
98
|
+
stop(entry, "interrupted");
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
list(projectId) {
|
|
103
|
+
prune();
|
|
104
|
+
return [...entries.values()]
|
|
105
|
+
.filter((entry) => entry.projectId === projectId)
|
|
106
|
+
.map(({ id, label, state, bytes }) => ({ id, label, state, bytes }));
|
|
107
|
+
},
|
|
108
|
+
stream(projectId, id, signal) {
|
|
109
|
+
prune();
|
|
110
|
+
const entry = entries.get(id);
|
|
111
|
+
if (entry === undefined ||
|
|
112
|
+
entry.projectId !== projectId ||
|
|
113
|
+
entry.state === "interrupted" ||
|
|
114
|
+
entry.state === "unavailable")
|
|
115
|
+
return undefined;
|
|
116
|
+
return follow(entry, signal);
|
|
117
|
+
},
|
|
118
|
+
clear(projectId) {
|
|
119
|
+
for (const entry of entries.values())
|
|
120
|
+
if (entry.projectId === projectId)
|
|
121
|
+
remove(entry);
|
|
122
|
+
},
|
|
123
|
+
close() {
|
|
124
|
+
closed = true;
|
|
125
|
+
clearInterval(expiry);
|
|
126
|
+
for (const entry of entries.values())
|
|
127
|
+
remove(entry);
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
return store;
|
|
131
|
+
}
|
|
132
|
+
function follow(entry, signal) {
|
|
133
|
+
let index = 0;
|
|
134
|
+
let ended = false;
|
|
135
|
+
let wake;
|
|
136
|
+
let control;
|
|
137
|
+
const detach = () => {
|
|
138
|
+
ended = true;
|
|
139
|
+
signal.removeEventListener("abort", abort);
|
|
140
|
+
if (wake !== undefined) {
|
|
141
|
+
entry.changed.delete(wake);
|
|
142
|
+
wake();
|
|
143
|
+
wake = undefined;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const abort = () => {
|
|
147
|
+
if (ended)
|
|
148
|
+
return;
|
|
149
|
+
detach();
|
|
150
|
+
control?.error(new Error("Audio preview disconnected"));
|
|
151
|
+
};
|
|
152
|
+
return new ReadableStream({
|
|
153
|
+
start(controller) {
|
|
154
|
+
control = controller;
|
|
155
|
+
if (signal.aborted)
|
|
156
|
+
abort();
|
|
157
|
+
else
|
|
158
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
159
|
+
},
|
|
160
|
+
async pull(controller) {
|
|
161
|
+
while (!ended) {
|
|
162
|
+
if (entry.state === "interrupted" || entry.state === "unavailable") {
|
|
163
|
+
detach();
|
|
164
|
+
controller.error(new Error("Audio preview was interrupted"));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const bytes = entry.chunks[index];
|
|
168
|
+
if (bytes !== undefined) {
|
|
169
|
+
index += 1;
|
|
170
|
+
controller.enqueue(bytes);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (entry.state === "ready") {
|
|
174
|
+
detach();
|
|
175
|
+
controller.close();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
await new Promise((resolve) => {
|
|
179
|
+
wake = resolve;
|
|
180
|
+
entry.changed.add(resolve);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
cancel() {
|
|
185
|
+
detach();
|
|
186
|
+
},
|
|
187
|
+
}, { highWaterMark: 0 });
|
|
188
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
CREATE TABLE batches (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
created_at TEXT NOT NULL
|
|
4
|
+
);
|
|
5
|
+
CREATE TABLE project_queue (
|
|
6
|
+
position INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
7
|
+
project_id TEXT NOT NULL UNIQUE REFERENCES projects(id) ON DELETE CASCADE,
|
|
8
|
+
batch_id TEXT NOT NULL REFERENCES batches(id),
|
|
9
|
+
state TEXT NOT NULL DEFAULT 'queued' CHECK(state IN ('queued','active','finished'))
|
|
10
|
+
);
|
|
11
|
+
CREATE INDEX project_queue_state ON project_queue(state, position);
|
package/dist/kernel/ports/llm.js
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Bounded UTF-16 length is conservative for providers counting Unicode characters.
|
|
2
|
+
// Every character survives; the split never cuts a surrogate pair.
|
|
3
|
+
export function splitText(text, limit) {
|
|
4
|
+
if (!Number.isInteger(limit) || limit < 2)
|
|
5
|
+
throw new Error("Text limit must be at least two characters");
|
|
6
|
+
const parts = [];
|
|
7
|
+
let rest = text;
|
|
8
|
+
while (rest.length > limit) {
|
|
9
|
+
const window = rest.slice(0, limit);
|
|
10
|
+
const sentences = [...window.matchAll(/[.!?]\s+|\n/g)];
|
|
11
|
+
const last = sentences.at(-1);
|
|
12
|
+
let cut = last && last.index > limit / 2
|
|
13
|
+
? last.index + last[0].length
|
|
14
|
+
: Math.max(window.lastIndexOf(" "), window.lastIndexOf("\n"), window.lastIndexOf("\t")) + 1;
|
|
15
|
+
if (cut < limit / 2)
|
|
16
|
+
cut = limit;
|
|
17
|
+
const code = rest.charCodeAt(cut - 1);
|
|
18
|
+
if (code >= 0xd800 && code <= 0xdbff)
|
|
19
|
+
cut--;
|
|
20
|
+
parts.push(rest.slice(0, cut));
|
|
21
|
+
rest = rest.slice(cut);
|
|
22
|
+
}
|
|
23
|
+
if (rest)
|
|
24
|
+
parts.push(rest);
|
|
25
|
+
return parts;
|
|
26
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { attempt } from "./attempt.js";
|
|
2
3
|
export function stageProviders(deps, context, pieceId) {
|
|
3
4
|
const ctx = {
|
|
@@ -10,25 +11,40 @@ export function stageProviders(deps, context, pieceId) {
|
|
|
10
11
|
...(pieceId === undefined ? {} : { pieceId }),
|
|
11
12
|
signal: context.signal,
|
|
12
13
|
};
|
|
14
|
+
const schedule = (provider, work) => deps.queue ? deps.queue.run(provider, context.signal, work) : work();
|
|
13
15
|
return {
|
|
14
16
|
llm: (call, onEvent) => {
|
|
15
17
|
// Resolved once; the adapter reads the stored key per request, so a key replaced
|
|
16
18
|
// mid-run still reaches the next attempt.
|
|
17
19
|
const port = deps.registry.llm(call.provider);
|
|
18
|
-
|
|
20
|
+
const callId = randomUUID();
|
|
21
|
+
const preview = (text, reset) => context.emit({
|
|
22
|
+
type: "llm.preview",
|
|
23
|
+
projectId: context.stage.projectId,
|
|
24
|
+
stage: context.stage.kind,
|
|
25
|
+
callId,
|
|
26
|
+
label: call.previewLabel ?? context.stage.kind,
|
|
27
|
+
text,
|
|
28
|
+
...(reset === undefined ? {} : { reset }),
|
|
29
|
+
});
|
|
30
|
+
return schedule(call.provider, () => attempt(ctx, async (signal, progress) => {
|
|
31
|
+
preview("", true);
|
|
19
32
|
let text = "";
|
|
20
33
|
let usage = null;
|
|
21
34
|
let finishReason = null;
|
|
22
35
|
for await (const event of port.complete({
|
|
23
36
|
model: call.model,
|
|
37
|
+
...(call.thinking === undefined ? {} : { thinking: call.thinking }),
|
|
24
38
|
messages: call.messages,
|
|
25
39
|
...(call.webSearch === undefined ? {} : { webSearch: call.webSearch }),
|
|
26
40
|
signal,
|
|
27
41
|
})) {
|
|
42
|
+
signal.throwIfAborted();
|
|
28
43
|
// Every event is a sign of life, so the idle clock restarts here.
|
|
29
44
|
progress();
|
|
30
45
|
if (event.type === "delta") {
|
|
31
46
|
text += event.text;
|
|
47
|
+
preview(event.text);
|
|
32
48
|
}
|
|
33
49
|
else if (event.type === "done") {
|
|
34
50
|
usage = event.usage;
|
|
@@ -44,47 +60,83 @@ export function stageProviders(deps, context, pieceId) {
|
|
|
44
60
|
throw new Error(unusable);
|
|
45
61
|
}
|
|
46
62
|
return answer;
|
|
47
|
-
}, { kind: "llm", streaming: port.capabilities.streams });
|
|
63
|
+
}, { kind: "llm", streaming: port.capabilities.streams }));
|
|
48
64
|
},
|
|
49
|
-
tts: (call) => {
|
|
65
|
+
tts: (call, observe) => {
|
|
50
66
|
const port = deps.registry.tts(call.provider);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
let continuation;
|
|
68
|
+
const notify = (event) => {
|
|
69
|
+
try {
|
|
70
|
+
observe?.(event);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// A preview is optional. A broken observer must never repeat a paid call.
|
|
74
|
+
deps.log.write("warn", "audio.preview", {
|
|
75
|
+
projectId: context.stage.projectId,
|
|
76
|
+
stage: "audio",
|
|
77
|
+
detail: "Could not update the live audio preview",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
return schedule(call.provider, () => attempt(ctx, async (signal, progress) => {
|
|
82
|
+
notify({ type: "start" });
|
|
83
|
+
try {
|
|
84
|
+
const spoken = await port.synthesize({
|
|
85
|
+
model: call.model,
|
|
86
|
+
voiceId: call.voiceId,
|
|
87
|
+
text: call.text,
|
|
88
|
+
signal,
|
|
89
|
+
onActivity: progress,
|
|
90
|
+
continuation: {
|
|
91
|
+
read: () => continuation,
|
|
92
|
+
write: (token) => {
|
|
93
|
+
continuation = token;
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
const reader = spoken.audio.getReader();
|
|
98
|
+
const chunks = [];
|
|
99
|
+
let total = 0;
|
|
100
|
+
try {
|
|
101
|
+
for (;;) {
|
|
102
|
+
const { done, value } = await reader.read();
|
|
103
|
+
signal.throwIfAborted();
|
|
104
|
+
if (done || value === undefined)
|
|
105
|
+
break;
|
|
106
|
+
progress();
|
|
107
|
+
chunks.push(value);
|
|
108
|
+
total += value.length;
|
|
109
|
+
notify({ type: "chunk", bytes: value });
|
|
110
|
+
}
|
|
64
111
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
112
|
+
finally {
|
|
113
|
+
reader.releaseLock();
|
|
114
|
+
}
|
|
115
|
+
const bytes = new Uint8Array(total);
|
|
116
|
+
let at = 0;
|
|
117
|
+
for (const chunk of chunks) {
|
|
118
|
+
bytes.set(chunk, at);
|
|
119
|
+
at += chunk.length;
|
|
120
|
+
}
|
|
121
|
+
notify({ type: "complete" });
|
|
122
|
+
return { bytes, container: spoken.container };
|
|
68
123
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
bytes.set(chunk, at);
|
|
73
|
-
at += chunk.length;
|
|
124
|
+
catch (error) {
|
|
125
|
+
notify({ type: "interrupted" });
|
|
126
|
+
throw error;
|
|
74
127
|
}
|
|
75
|
-
|
|
76
|
-
}, { kind: "tts", streaming: port.capabilities.streams });
|
|
128
|
+
}, { kind: "tts", streaming: port.capabilities.streams }));
|
|
77
129
|
},
|
|
78
130
|
image: (call) => {
|
|
79
131
|
const port = deps.registry.image(call.provider);
|
|
80
|
-
return attempt(ctx, (signal) => port.generate({
|
|
132
|
+
return schedule(call.provider, () => attempt(ctx, (signal) => port.generate({
|
|
81
133
|
model: call.model,
|
|
82
134
|
prompt: call.prompt,
|
|
83
135
|
aspect: call.aspect,
|
|
84
136
|
signal,
|
|
85
137
|
}),
|
|
86
138
|
// One request, one answer: the 300 s runs over the whole call.
|
|
87
|
-
{ kind: "image" });
|
|
139
|
+
{ kind: "image" }));
|
|
88
140
|
},
|
|
89
141
|
forPiece: (piece) => stageProviders(deps, context, piece),
|
|
90
142
|
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// One app-wide queue: no more than five calls, with lower provider limits.
|
|
2
|
+
// Waiting does not start an attempt or its idle timer. FIFO among eligible calls.
|
|
3
|
+
export function createProviderQueue(limitFor) {
|
|
4
|
+
const active = new Map();
|
|
5
|
+
const waiting = [];
|
|
6
|
+
let total = 0;
|
|
7
|
+
function drain() {
|
|
8
|
+
while (total < 5) {
|
|
9
|
+
const at = waiting.findIndex((w) => (active.get(w.provider) ?? 0) <
|
|
10
|
+
Math.max(1, Math.min(5, Math.floor(limitFor(w.provider)) || 1)));
|
|
11
|
+
if (at < 0)
|
|
12
|
+
return;
|
|
13
|
+
const next = waiting.splice(at, 1)[0];
|
|
14
|
+
if (!next)
|
|
15
|
+
return;
|
|
16
|
+
next.signal.removeEventListener("abort", next.cancel);
|
|
17
|
+
if (next.signal.aborted) {
|
|
18
|
+
next.cancel();
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
total++;
|
|
22
|
+
active.set(next.provider, (active.get(next.provider) ?? 0) + 1);
|
|
23
|
+
next.start();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
run: (provider, signal, work) => {
|
|
28
|
+
if (signal.aborted)
|
|
29
|
+
return Promise.reject(signal.reason);
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const waiter = {
|
|
32
|
+
provider,
|
|
33
|
+
signal,
|
|
34
|
+
cancel: () => {
|
|
35
|
+
const at = waiting.indexOf(waiter);
|
|
36
|
+
if (at >= 0)
|
|
37
|
+
waiting.splice(at, 1);
|
|
38
|
+
signal.removeEventListener("abort", waiter.cancel);
|
|
39
|
+
reject(signal.reason);
|
|
40
|
+
},
|
|
41
|
+
start: () => {
|
|
42
|
+
void Promise.resolve()
|
|
43
|
+
.then(() => {
|
|
44
|
+
signal.throwIfAborted();
|
|
45
|
+
return work();
|
|
46
|
+
})
|
|
47
|
+
.then(resolve, reject)
|
|
48
|
+
.finally(() => {
|
|
49
|
+
total--;
|
|
50
|
+
const left = (active.get(provider) ?? 1) - 1;
|
|
51
|
+
if (left)
|
|
52
|
+
active.set(provider, left);
|
|
53
|
+
else
|
|
54
|
+
active.delete(provider);
|
|
55
|
+
drain();
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
waiting.push(waiter);
|
|
60
|
+
signal.addEventListener("abort", waiter.cancel, { once: true });
|
|
61
|
+
drain();
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|