slackmark 0.5.0 → 0.6.1
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 +29 -17
- package/dist/adapters/block-to-plain-text.d.ts +24 -0
- package/dist/adapters/block-to-plain-text.d.ts.map +1 -0
- package/dist/adapters/blockquote-adapter.d.ts.map +1 -1
- package/dist/adapters/code-fence-adapter.d.ts +1 -1
- package/dist/adapters/code-fence-adapter.d.ts.map +1 -1
- package/dist/adapters/inline.d.ts.map +1 -1
- package/dist/adapters/list-adapter.d.ts.map +1 -1
- package/dist/adapters/math-block-adapter.d.ts +1 -1
- package/dist/adapters/math-block-adapter.d.ts.map +1 -1
- package/dist/adapters/mermaid-fence-adapter.d.ts.map +1 -1
- package/dist/adapters/table-adapter.d.ts.map +1 -1
- package/dist/adapters/task-list-adapter.d.ts.map +1 -1
- package/dist/adapters/try-render-image.d.ts +9 -0
- package/dist/adapters/try-render-image.d.ts.map +1 -0
- package/dist/assemble/message-assembler.d.ts +10 -0
- package/dist/assemble/message-assembler.d.ts.map +1 -1
- package/dist/budget/conversion-context.d.ts +5 -1
- package/dist/budget/conversion-context.d.ts.map +1 -1
- package/dist/budget/operation-scope.d.ts +9 -0
- package/dist/budget/operation-scope.d.ts.map +1 -0
- package/dist/budget/recording-receipt-collector.d.ts +13 -0
- package/dist/budget/recording-receipt-collector.d.ts.map +1 -0
- package/dist/capabilities/capabilities.d.ts.map +1 -1
- package/dist/constants.d.ts +14 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/converter/create-converter.d.ts +1 -1
- package/dist/converter/create-converter.d.ts.map +1 -1
- package/dist/converter/resolve-config.d.ts +4 -2
- package/dist/converter/resolve-config.d.ts.map +1 -1
- package/dist/converter/slackmark-converter.d.ts +11 -1
- package/dist/converter/slackmark-converter.d.ts.map +1 -1
- package/dist/errors.d.ts +81 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5080 -3585
- package/dist/index.js.map +1 -1
- package/dist/receipts.d.ts +37 -0
- package/dist/receipts.d.ts.map +1 -0
- package/dist/renderers/codecogs-renderer.d.ts +5 -5
- package/dist/renderers/codecogs-renderer.d.ts.map +1 -1
- package/dist/renderers/kroki-renderer.d.ts +5 -5
- package/dist/renderers/kroki-renderer.d.ts.map +1 -1
- package/dist/renderers/mermaid-ink-renderer.d.ts +5 -5
- package/dist/renderers/mermaid-ink-renderer.d.ts.map +1 -1
- package/dist/renderers/quickchart-renderer.d.ts +5 -5
- package/dist/renderers/quickchart-renderer.d.ts.map +1 -1
- package/dist/renderers/renderer-options.d.ts +4 -0
- package/dist/renderers/renderer-options.d.ts.map +1 -0
- package/dist/renderers/url-utils.d.ts.map +1 -1
- package/dist/renderers.d.ts +1 -0
- package/dist/renderers.d.ts.map +1 -1
- package/dist/renderers.js +412 -248
- package/dist/renderers.js.map +1 -1
- package/dist/slack/fetch-slack-uploader.d.ts +11 -2
- package/dist/slack/fetch-slack-uploader.d.ts.map +1 -1
- package/dist/slack/slack-upload-error.d.ts +18 -2
- package/dist/slack/slack-upload-error.d.ts.map +1 -1
- package/dist/slack.d.ts +2 -1
- package/dist/slack.d.ts.map +1 -1
- package/dist/slack.js +753 -230
- package/dist/slack.js.map +1 -1
- package/dist/types.d.ts +93 -11
- package/dist/types.d.ts.map +1 -1
- package/dist/validate/validate-blocks.d.ts.map +1 -1
- package/package.json +5 -4
package/dist/slack.js
CHANGED
|
@@ -1,255 +1,778 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
//#region src/errors.ts
|
|
2
|
+
/**
|
|
3
|
+
* Registry-keyed markers, not module-local symbols.
|
|
4
|
+
*
|
|
5
|
+
* Each published entry (`slackmark`, `slackmark/renderers`, `slackmark/slack`) is bundled
|
|
6
|
+
* without code splitting, so every entry carries its own copy of these classes and
|
|
7
|
+
* `instanceof` is false for an error that crosses an entry boundary. `Symbol.for` keys are
|
|
8
|
+
* shared by every copy, which is what makes the exported guards below reliable. Prefer
|
|
9
|
+
* those guards to `instanceof` for any error that can cross an entry.
|
|
10
|
+
*/
|
|
11
|
+
const RECEIPT_SNAPSHOT_LOCK = Symbol.for("slackmark.receiptSnapshotLock");
|
|
12
|
+
const CONFIGURATION_ERROR_BRAND = Symbol.for("slackmark.error.configuration");
|
|
13
|
+
const OPERATION_ERROR_BRAND = Symbol.for("slackmark.error.operation");
|
|
14
|
+
/**
|
|
15
|
+
* Marks a class prototype so instances from any bundled copy answer to one identity.
|
|
16
|
+
* Subclasses inherit the brand, and instances carry no extra own property.
|
|
17
|
+
*/
|
|
18
|
+
function brandErrorPrototype(prototype, brand) {
|
|
19
|
+
Object.defineProperty(prototype, brand, {
|
|
20
|
+
configurable: false,
|
|
21
|
+
enumerable: false,
|
|
22
|
+
value: true,
|
|
23
|
+
writable: false
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Reads a brand without trusting the value: a hostile getter must not escape a guard.
|
|
28
|
+
*/
|
|
29
|
+
function hasErrorBrand(value, brand) {
|
|
30
|
+
if (typeof value !== "object" || value === null) return false;
|
|
31
|
+
try {
|
|
32
|
+
return Reflect.get(value, brand) === true;
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
var ConfigurationError = class extends Error {
|
|
38
|
+
static {
|
|
39
|
+
brandErrorPrototype(this.prototype, CONFIGURATION_ERROR_BRAND);
|
|
40
|
+
}
|
|
41
|
+
name;
|
|
42
|
+
code;
|
|
43
|
+
stage;
|
|
44
|
+
retryable;
|
|
45
|
+
receipts;
|
|
46
|
+
settledReceipts;
|
|
47
|
+
constructor(message, options = {}) {
|
|
48
|
+
super(message, options?.cause !== void 0 ? { cause: options.cause } : void 0);
|
|
49
|
+
this.name = "ConfigurationError";
|
|
50
|
+
this.code = options.code ?? "invalid_configuration";
|
|
51
|
+
this.stage = options.stage;
|
|
52
|
+
this.retryable = false;
|
|
53
|
+
this.receipts = freezeReceipts(options.receipts ?? []);
|
|
54
|
+
this.settledReceipts = options.settledReceipts ?? Promise.resolve(this.receipts);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const OperationErrorCode = {
|
|
58
|
+
Aborted: "operation_aborted",
|
|
59
|
+
DeadlineExceeded: "operation_deadline_exceeded",
|
|
60
|
+
Shutdown: "operation_shutdown"
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Operational control-flow error. These failures are never degradations.
|
|
64
|
+
*/
|
|
65
|
+
var OperationError = class extends Error {
|
|
66
|
+
static {
|
|
67
|
+
brandErrorPrototype(this.prototype, OPERATION_ERROR_BRAND);
|
|
68
|
+
}
|
|
69
|
+
name;
|
|
70
|
+
code;
|
|
71
|
+
stage;
|
|
72
|
+
retryable;
|
|
73
|
+
receipts;
|
|
74
|
+
settledReceipts;
|
|
75
|
+
constructor(message, code, stage, retryable, options = {}) {
|
|
76
|
+
super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
|
|
77
|
+
this.name = "OperationError";
|
|
78
|
+
this.code = code;
|
|
79
|
+
this.stage = stage;
|
|
80
|
+
this.retryable = retryable;
|
|
81
|
+
this.receipts = freezeReceipts(options.receipts ?? []);
|
|
82
|
+
this.settledReceipts = options.settledReceipts ?? Promise.resolve(this.receipts);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
var OperationAbortedError = class extends OperationError {
|
|
86
|
+
name;
|
|
87
|
+
constructor(stage, options = {}) {
|
|
88
|
+
super("Operation aborted by caller", OperationErrorCode.Aborted, stage, false, options);
|
|
89
|
+
this.name = "OperationAbortedError";
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
var OperationDeadlineExceededError = class extends OperationError {
|
|
93
|
+
name;
|
|
94
|
+
constructor(stage, options = {}) {
|
|
95
|
+
super("Operation deadline exceeded", OperationErrorCode.DeadlineExceeded, stage, true, options);
|
|
96
|
+
this.name = "OperationDeadlineExceededError";
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Identifies a {@link ConfigurationError} from any entry of this package.
|
|
101
|
+
*
|
|
102
|
+
* Use this instead of `instanceof` when the error may have been thrown by a different
|
|
103
|
+
* entry — for example a `ConfigurationError` from `slackmark/renderers` caught by code
|
|
104
|
+
* that imported the class from `slackmark`.
|
|
105
|
+
*/
|
|
106
|
+
function isConfigurationError(value) {
|
|
107
|
+
return hasErrorBrand(value, CONFIGURATION_ERROR_BRAND);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Identifies an {@link OperationError} — abort, deadline, or shutdown — from any entry of
|
|
111
|
+
* this package, including its subclasses.
|
|
112
|
+
*
|
|
113
|
+
* Use this instead of `instanceof` when the error may have been thrown by a different
|
|
114
|
+
* entry — for example an abort raised inside `FetchSlackUploader` from `slackmark/slack`
|
|
115
|
+
* caught by code that imported the class from `slackmark`. Narrow further with `code`
|
|
116
|
+
* against {@link OperationErrorCode} rather than testing a subclass with `instanceof`.
|
|
117
|
+
*/
|
|
118
|
+
function isOperationError(value) {
|
|
119
|
+
return hasErrorBrand(value, OPERATION_ERROR_BRAND);
|
|
120
|
+
}
|
|
121
|
+
function attachReceipts(error, receipts, settledReceipts) {
|
|
122
|
+
if (!(error instanceof Error)) return error;
|
|
123
|
+
try {
|
|
124
|
+
if (Reflect.get(error, RECEIPT_SNAPSHOT_LOCK) === true) return error;
|
|
125
|
+
} catch {
|
|
126
|
+
return error;
|
|
127
|
+
}
|
|
128
|
+
let existing = [];
|
|
129
|
+
try {
|
|
130
|
+
const candidate = Reflect.get(error, "receipts");
|
|
131
|
+
if (Array.isArray(candidate)) existing = candidate.filter(isUploadReceipt);
|
|
132
|
+
} catch {
|
|
133
|
+
existing = [];
|
|
134
|
+
}
|
|
135
|
+
Reflect.defineProperty(error, "receipts", {
|
|
136
|
+
configurable: true,
|
|
137
|
+
enumerable: true,
|
|
138
|
+
value: freezeReceipts(mergeReceipts(existing, receipts)),
|
|
139
|
+
writable: false
|
|
140
|
+
});
|
|
141
|
+
if (settledReceipts !== void 0) {
|
|
142
|
+
const finalReceipts = freezeSettledReceipts(settledReceipts, receipts);
|
|
143
|
+
Reflect.defineProperty(error, "settledReceipts", {
|
|
144
|
+
configurable: true,
|
|
145
|
+
enumerable: true,
|
|
146
|
+
value: finalReceipts,
|
|
147
|
+
writable: false
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return error;
|
|
151
|
+
}
|
|
152
|
+
async function freezeSettledReceipts(settledReceipts, fallback) {
|
|
153
|
+
try {
|
|
154
|
+
return freezeReceipts(await settledReceipts);
|
|
155
|
+
} catch {
|
|
156
|
+
return freezeReceipts(fallback);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function mergeReceipts(existing, incoming) {
|
|
160
|
+
const existingGroups = groupReceipts(existing);
|
|
161
|
+
const incomingGroups = groupReceipts(incoming);
|
|
162
|
+
const merged = [...incoming];
|
|
163
|
+
for (const [key, values] of existingGroups) {
|
|
164
|
+
const overlap = incomingGroups.get(key)?.length ?? 0;
|
|
165
|
+
if (values.length > overlap) merged.push(...values.slice(overlap));
|
|
166
|
+
}
|
|
167
|
+
return merged;
|
|
168
|
+
}
|
|
169
|
+
function groupReceipts(receipts) {
|
|
170
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
171
|
+
for (const receipt of receipts) {
|
|
172
|
+
const key = receiptKey(receipt);
|
|
173
|
+
const values = grouped.get(key);
|
|
174
|
+
if (values === void 0) grouped.set(key, [receipt]);
|
|
175
|
+
else values.push(receipt);
|
|
176
|
+
}
|
|
177
|
+
return grouped;
|
|
178
|
+
}
|
|
179
|
+
function receiptKey(receipt) {
|
|
180
|
+
return [
|
|
181
|
+
receipt.provider,
|
|
182
|
+
receipt.phase,
|
|
183
|
+
receipt.fileId ?? "",
|
|
184
|
+
String(receipt.shared),
|
|
185
|
+
receipt.processing,
|
|
186
|
+
String(receipt.status ?? ""),
|
|
187
|
+
String(receipt.retryAfterMs ?? ""),
|
|
188
|
+
receipt.providerCode ?? "",
|
|
189
|
+
receipt.retrySafety
|
|
190
|
+
].join("\0");
|
|
191
|
+
}
|
|
192
|
+
function freezeReceipts(receipts) {
|
|
193
|
+
return Object.freeze(receipts.map((receipt) => Object.freeze({ ...receipt })));
|
|
194
|
+
}
|
|
195
|
+
function isUploadReceipt(value) {
|
|
196
|
+
return typeof value === "object" && value !== null && Reflect.get(value, "kind") === "upload";
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/receipts.ts
|
|
200
|
+
const UploadPhase = {
|
|
201
|
+
Requested: "requested",
|
|
202
|
+
Allocated: "allocated",
|
|
203
|
+
Uploaded: "uploaded",
|
|
204
|
+
Completed: "completed",
|
|
205
|
+
Shared: "shared",
|
|
206
|
+
Unknown: "unknown"
|
|
207
|
+
};
|
|
208
|
+
const UploadProcessing = {
|
|
209
|
+
NotRequested: "not-requested",
|
|
210
|
+
Pending: "pending",
|
|
211
|
+
Verified: "verified",
|
|
212
|
+
Unverified: "unverified"
|
|
213
|
+
};
|
|
214
|
+
const RetrySafety = {
|
|
215
|
+
Never: "never",
|
|
216
|
+
Safe: "safe",
|
|
217
|
+
MayDuplicate: "may-duplicate"
|
|
8
218
|
};
|
|
219
|
+
//#endregion
|
|
220
|
+
//#region src/slack/slack-upload-error.ts
|
|
221
|
+
/** See the brand rationale in `errors.ts`: this class is bundled into more than one entry. */
|
|
222
|
+
const SLACK_UPLOAD_ERROR_BRAND = Symbol.for("slackmark.error.slackUpload");
|
|
223
|
+
const SlackUploadStage = {
|
|
224
|
+
Admission: "admission",
|
|
225
|
+
GetUploadUrl: "get-upload-url",
|
|
226
|
+
UploadBytes: "upload-bytes",
|
|
227
|
+
CompleteUpload: "complete-upload",
|
|
228
|
+
FileInfo: "file-info",
|
|
229
|
+
ProcessingTimeout: "processing-timeout"
|
|
230
|
+
};
|
|
231
|
+
/**
|
|
232
|
+
* Integration error from one explicit stage of Slack's external-upload flow.
|
|
233
|
+
*/
|
|
9
234
|
var SlackUploadError = class extends Error {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
235
|
+
static {
|
|
236
|
+
brandErrorPrototype(this.prototype, SLACK_UPLOAD_ERROR_BRAND);
|
|
237
|
+
}
|
|
238
|
+
name;
|
|
239
|
+
stage;
|
|
240
|
+
status;
|
|
241
|
+
code;
|
|
242
|
+
retryable;
|
|
243
|
+
retryAfterMs;
|
|
244
|
+
receipt;
|
|
245
|
+
receipts;
|
|
246
|
+
settledReceipts;
|
|
247
|
+
constructor(message, details) {
|
|
248
|
+
super(message, details.cause !== void 0 ? { cause: details.cause } : void 0);
|
|
249
|
+
this.name = "SlackUploadError";
|
|
250
|
+
this.stage = details.stage;
|
|
251
|
+
this.status = details.status;
|
|
252
|
+
this.code = details.code;
|
|
253
|
+
this.retryable = details.retryable ?? isRetryable(details.status, details.code);
|
|
254
|
+
this.retryAfterMs = details.retryAfterMs;
|
|
255
|
+
this.receipt = Object.freeze(details.receipt ?? {
|
|
256
|
+
kind: "upload",
|
|
257
|
+
provider: "slack",
|
|
258
|
+
phase: UploadPhase.Requested,
|
|
259
|
+
fileId: void 0,
|
|
260
|
+
shared: false,
|
|
261
|
+
processing: UploadProcessing.NotRequested,
|
|
262
|
+
status: details.status,
|
|
263
|
+
retryAfterMs: details.retryAfterMs,
|
|
264
|
+
providerCode: details.code,
|
|
265
|
+
retrySafety: RetrySafety.Safe
|
|
266
|
+
});
|
|
267
|
+
this.receipts = Object.freeze([this.receipt]);
|
|
268
|
+
this.settledReceipts = Promise.resolve(this.receipts);
|
|
269
|
+
}
|
|
21
270
|
};
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
271
|
+
/**
|
|
272
|
+
* Identifies a {@link SlackUploadError} from any entry of this package. `slackmark` and
|
|
273
|
+
* `slackmark/slack` each bundle their own copy, so `instanceof` is unreliable when the
|
|
274
|
+
* error crosses between them.
|
|
275
|
+
*/
|
|
276
|
+
function isSlackUploadError(value) {
|
|
277
|
+
return hasErrorBrand(value, SLACK_UPLOAD_ERROR_BRAND);
|
|
278
|
+
}
|
|
279
|
+
function isRetryable(status, code) {
|
|
280
|
+
return status === 429 || status !== void 0 && status >= 500 || code === "fetch_failed" || code === "request_timeout" || code === "processing_timeout";
|
|
281
|
+
}
|
|
282
|
+
//#endregion
|
|
283
|
+
//#region src/slack/fetch-slack-uploader.ts
|
|
284
|
+
const SLACK_API_BASE_URL = "https://slack.com/api";
|
|
285
|
+
const DEFAULT_POLL_INTERVAL_MS = 250;
|
|
286
|
+
const DEFAULT_POLL_MAX_ATTEMPTS = 20;
|
|
287
|
+
const DEFAULT_POLL_TIMEOUT_MS = 3e4;
|
|
288
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
|
|
289
|
+
/**
|
|
290
|
+
* Web-standard implementation of Slack's external file-upload flow.
|
|
291
|
+
*
|
|
292
|
+
* @see https://docs.slack.dev/messaging/working-with-files
|
|
293
|
+
*/
|
|
28
294
|
var FetchSlackUploader = class {
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
295
|
+
token;
|
|
296
|
+
fetcher;
|
|
297
|
+
channelId;
|
|
298
|
+
requestTimeoutMs;
|
|
299
|
+
waitForProcessing;
|
|
300
|
+
pollIntervalMs;
|
|
301
|
+
pollMaxAttempts;
|
|
302
|
+
pollTimeoutMs;
|
|
303
|
+
sleep;
|
|
304
|
+
clock;
|
|
305
|
+
constructor(options) {
|
|
306
|
+
const resolved = resolveUploaderOptions(options);
|
|
307
|
+
this.token = resolved.token;
|
|
308
|
+
this.fetcher = resolved.fetcher;
|
|
309
|
+
this.channelId = resolved.channelId;
|
|
310
|
+
this.requestTimeoutMs = resolved.requestTimeoutMs;
|
|
311
|
+
this.waitForProcessing = resolved.waitForProcessing;
|
|
312
|
+
this.pollIntervalMs = resolved.pollIntervalMs;
|
|
313
|
+
this.pollMaxAttempts = resolved.pollMaxAttempts;
|
|
314
|
+
this.pollTimeoutMs = resolved.pollTimeoutMs;
|
|
315
|
+
this.sleep = resolved.sleep;
|
|
316
|
+
this.clock = resolved.clock;
|
|
317
|
+
}
|
|
318
|
+
async upload(img, context) {
|
|
319
|
+
let receipt = slackReceipt({
|
|
320
|
+
phase: UploadPhase.Requested,
|
|
321
|
+
processing: UploadProcessing.NotRequested,
|
|
322
|
+
retrySafety: RetrySafety.Safe
|
|
323
|
+
});
|
|
324
|
+
try {
|
|
325
|
+
const target = await this.getUploadTarget(img, context);
|
|
326
|
+
receipt = slackReceipt({
|
|
327
|
+
phase: UploadPhase.Allocated,
|
|
328
|
+
fileId: target.fileId,
|
|
329
|
+
processing: UploadProcessing.NotRequested,
|
|
330
|
+
retrySafety: RetrySafety.MayDuplicate
|
|
331
|
+
});
|
|
332
|
+
await this.uploadBytes(target, img, context);
|
|
333
|
+
receipt = slackReceipt({
|
|
334
|
+
phase: UploadPhase.Uploaded,
|
|
335
|
+
fileId: target.fileId,
|
|
336
|
+
processing: UploadProcessing.NotRequested,
|
|
337
|
+
retrySafety: RetrySafety.MayDuplicate
|
|
338
|
+
});
|
|
339
|
+
await this.completeUpload(target.fileId, img.filename, context);
|
|
340
|
+
receipt = slackReceipt({
|
|
341
|
+
phase: this.channelId === void 0 ? UploadPhase.Completed : UploadPhase.Shared,
|
|
342
|
+
fileId: target.fileId,
|
|
343
|
+
shared: this.channelId !== void 0,
|
|
344
|
+
processing: this.waitForProcessing ? UploadProcessing.Pending : UploadProcessing.NotRequested,
|
|
345
|
+
retrySafety: RetrySafety.Never
|
|
346
|
+
});
|
|
347
|
+
if (this.waitForProcessing) try {
|
|
348
|
+
await this.waitUntilProcessed(target.fileId, receipt, context);
|
|
349
|
+
receipt = slackReceipt({
|
|
350
|
+
...receipt,
|
|
351
|
+
processing: UploadProcessing.Verified
|
|
352
|
+
});
|
|
353
|
+
} catch (error) {
|
|
354
|
+
if (isOperationError(error)) throw attachReceipts(error, [receipt]);
|
|
355
|
+
if (isSlackUploadError(error)) receipt = slackReceipt({
|
|
356
|
+
...receipt,
|
|
357
|
+
processing: UploadProcessing.Unverified,
|
|
358
|
+
status: error.status,
|
|
359
|
+
retryAfterMs: error.retryAfterMs,
|
|
360
|
+
providerCode: error.code
|
|
361
|
+
});
|
|
362
|
+
else receipt = slackReceipt({
|
|
363
|
+
...receipt,
|
|
364
|
+
processing: UploadProcessing.Unverified,
|
|
365
|
+
providerCode: "processing_failed"
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
fileId: target.fileId,
|
|
370
|
+
receipt
|
|
371
|
+
};
|
|
372
|
+
} catch (error) {
|
|
373
|
+
if (isSlackUploadError(error)) throw withReceipt(error, receipt);
|
|
374
|
+
if (isOperationError(error)) throw attachReceipts(error, [receipt]);
|
|
375
|
+
throw error;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
async getUploadTarget(img, control) {
|
|
379
|
+
const payload = await this.callSlackApi("files.getUploadURLExternal", encodeForm([["filename", img.filename], ["length", String(img.data.byteLength)]]), SlackUploadStage.GetUploadUrl, control);
|
|
380
|
+
const uploadUrl = payload["upload_url"];
|
|
381
|
+
const fileId = payload["file_id"];
|
|
382
|
+
if (!isNonEmptyString(uploadUrl) || !isNonEmptyString(fileId)) throw new SlackUploadError("files.getUploadURLExternal returned no upload_url or file_id", {
|
|
383
|
+
stage: SlackUploadStage.GetUploadUrl,
|
|
384
|
+
code: "invalid_response"
|
|
385
|
+
});
|
|
386
|
+
return {
|
|
387
|
+
uploadUrl,
|
|
388
|
+
fileId
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
async uploadBytes(target, img, control) {
|
|
392
|
+
const stageSignal = createStageSignal(control, this.requestTimeoutMs, this.clock);
|
|
393
|
+
let response;
|
|
394
|
+
try {
|
|
395
|
+
response = await this.fetcher(target.uploadUrl, {
|
|
396
|
+
method: "POST",
|
|
397
|
+
headers: { "content-type": img.mimeType },
|
|
398
|
+
body: img.data,
|
|
399
|
+
signal: stageSignal.signal
|
|
400
|
+
});
|
|
401
|
+
} catch (cause) {
|
|
402
|
+
const operationError = stageSignal.operationError(SlackUploadStage.UploadBytes, cause);
|
|
403
|
+
if (operationError !== void 0) throw operationError;
|
|
404
|
+
throw new SlackUploadError("Slack byte upload failed", {
|
|
405
|
+
stage: SlackUploadStage.UploadBytes,
|
|
406
|
+
code: stageSignal.code(),
|
|
407
|
+
cause
|
|
408
|
+
});
|
|
409
|
+
} finally {
|
|
410
|
+
stageSignal.cleanup();
|
|
411
|
+
}
|
|
412
|
+
if (!response.ok) throw new SlackUploadError("Slack byte upload was rejected", {
|
|
413
|
+
stage: SlackUploadStage.UploadBytes,
|
|
414
|
+
status: response.status,
|
|
415
|
+
code: `http_${String(response.status)}`,
|
|
416
|
+
retryAfterMs: retryAfterMs(response)
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
async completeUpload(fileId, filename, control) {
|
|
420
|
+
const fields = [["files", JSON.stringify([{
|
|
421
|
+
id: fileId,
|
|
422
|
+
title: filename
|
|
423
|
+
}])]];
|
|
424
|
+
if (this.channelId !== void 0) fields.push(["channel_id", this.channelId]);
|
|
425
|
+
await this.callSlackApi("files.completeUploadExternal", encodeForm(fields), SlackUploadStage.CompleteUpload, control);
|
|
426
|
+
}
|
|
427
|
+
async waitUntilProcessed(fileId, receipt, control) {
|
|
428
|
+
const startedAt = this.clock();
|
|
429
|
+
const pollDeadlineMs = startedAt + this.pollTimeoutMs;
|
|
430
|
+
const pollOwnsDeadline = control.deadlineMs === void 0 || pollDeadlineMs < control.deadlineMs;
|
|
431
|
+
const pollContext = {
|
|
432
|
+
signal: control.signal,
|
|
433
|
+
deadlineMs: Math.min(control.deadlineMs ?? Number.POSITIVE_INFINITY, pollDeadlineMs)
|
|
434
|
+
};
|
|
435
|
+
for (let attempt = 1; attempt <= this.pollMaxAttempts; attempt += 1) {
|
|
436
|
+
if (this.clock() >= pollDeadlineMs) throw processingTimeout(receipt, attempt - 1);
|
|
437
|
+
let payload;
|
|
438
|
+
try {
|
|
439
|
+
payload = await this.callSlackApi("files.info", encodeForm([["file", fileId]]), SlackUploadStage.FileInfo, pollContext);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
if (pollOwnsDeadline && isOperationDeadline(error)) throw processingTimeout(receipt, attempt);
|
|
442
|
+
throw error;
|
|
443
|
+
}
|
|
444
|
+
if (isProcessedImageFile(payload["file"])) return;
|
|
445
|
+
if (attempt === this.pollMaxAttempts || this.clock() - startedAt >= this.pollTimeoutMs) throw processingTimeout(receipt, attempt);
|
|
446
|
+
try {
|
|
447
|
+
await controlledSleep(this.sleep, Math.min(this.pollIntervalMs, Math.max(0, pollDeadlineMs - this.clock())), pollContext, this.clock);
|
|
448
|
+
} catch (error) {
|
|
449
|
+
if (pollOwnsDeadline && isOperationDeadline(error)) throw processingTimeout(receipt, attempt);
|
|
450
|
+
throw error;
|
|
451
|
+
}
|
|
452
|
+
if (this.clock() - startedAt >= this.pollTimeoutMs) throw processingTimeout(receipt, attempt);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
async callSlackApi(method, body, stage, control) {
|
|
456
|
+
const stageSignal = createStageSignal(control, this.requestTimeoutMs, this.clock);
|
|
457
|
+
let response;
|
|
458
|
+
try {
|
|
459
|
+
response = await this.fetcher(`${SLACK_API_BASE_URL}/${method}`, {
|
|
460
|
+
method: "POST",
|
|
461
|
+
headers: {
|
|
462
|
+
authorization: `Bearer ${this.token}`,
|
|
463
|
+
"content-type": "application/x-www-form-urlencoded;charset=UTF-8"
|
|
464
|
+
},
|
|
465
|
+
body,
|
|
466
|
+
signal: stageSignal.signal
|
|
467
|
+
});
|
|
468
|
+
} catch (cause) {
|
|
469
|
+
const operationError = stageSignal.operationError(stage, cause);
|
|
470
|
+
stageSignal.cleanup();
|
|
471
|
+
if (operationError !== void 0) throw operationError;
|
|
472
|
+
throw new SlackUploadError("Slack API request failed", {
|
|
473
|
+
stage,
|
|
474
|
+
code: stageSignal.code(),
|
|
475
|
+
cause
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
let payload;
|
|
479
|
+
try {
|
|
480
|
+
payload = await raceWithSignal(response.json(), stageSignal.signal);
|
|
481
|
+
} catch (cause) {
|
|
482
|
+
const operationError = stageSignal.operationError(stage, cause);
|
|
483
|
+
const signalCode = stageSignal.code();
|
|
484
|
+
if (operationError !== void 0) throw operationError;
|
|
485
|
+
throw new SlackUploadError("Slack API returned invalid JSON", {
|
|
486
|
+
stage,
|
|
487
|
+
status: response.status,
|
|
488
|
+
code: signalCode === "fetch_failed" ? "invalid_json" : signalCode,
|
|
489
|
+
cause
|
|
490
|
+
});
|
|
491
|
+
} finally {
|
|
492
|
+
stageSignal.cleanup();
|
|
493
|
+
}
|
|
494
|
+
const slackCode = getSlackErrorCode(payload);
|
|
495
|
+
if (!response.ok || !isRecord(payload) || payload["ok"] !== true) throw new SlackUploadError("Slack API rejected the request", {
|
|
496
|
+
stage,
|
|
497
|
+
status: response.status,
|
|
498
|
+
code: slackCode ?? (response.ok ? "invalid_response" : `http_${String(response.status)}`),
|
|
499
|
+
retryAfterMs: retryAfterMs(response)
|
|
500
|
+
});
|
|
501
|
+
return payload;
|
|
502
|
+
}
|
|
190
503
|
};
|
|
191
|
-
function
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
504
|
+
function resolveUploaderOptions(options) {
|
|
505
|
+
assertKnownPlainObject(options, [
|
|
506
|
+
"token",
|
|
507
|
+
"fetch",
|
|
508
|
+
"channelId",
|
|
509
|
+
"requestTimeoutMs",
|
|
510
|
+
"waitForProcessing",
|
|
511
|
+
"poll"
|
|
512
|
+
], "FetchSlackUploader options");
|
|
513
|
+
let token;
|
|
514
|
+
let fetcher;
|
|
515
|
+
let channelId;
|
|
516
|
+
let requestTimeoutMs;
|
|
517
|
+
let waitForProcessing;
|
|
518
|
+
let poll;
|
|
519
|
+
try {
|
|
520
|
+
token = Reflect.get(options, "token");
|
|
521
|
+
fetcher = Reflect.get(options, "fetch");
|
|
522
|
+
channelId = Reflect.get(options, "channelId");
|
|
523
|
+
requestTimeoutMs = Reflect.get(options, "requestTimeoutMs");
|
|
524
|
+
waitForProcessing = Reflect.get(options, "waitForProcessing");
|
|
525
|
+
poll = Reflect.get(options, "poll");
|
|
526
|
+
} catch (cause) {
|
|
527
|
+
throw invalidUploaderOptions("Uploader options could not be read", cause);
|
|
528
|
+
}
|
|
529
|
+
if (typeof token !== "string" || token.length === 0 || token !== token.trim()) throw invalidUploaderOptions("token must be a nonempty trimmed string");
|
|
530
|
+
if (channelId !== void 0 && (typeof channelId !== "string" || channelId.length === 0 || channelId !== channelId.trim())) throw invalidUploaderOptions("channelId must be a nonempty trimmed string");
|
|
531
|
+
if (fetcher !== void 0 && typeof fetcher !== "function") throw invalidUploaderOptions("fetch must be a function");
|
|
532
|
+
if (requestTimeoutMs !== void 0 && (typeof requestTimeoutMs !== "number" || !Number.isFinite(requestTimeoutMs) || requestTimeoutMs <= 0)) throw invalidUploaderOptions("requestTimeoutMs must be positive");
|
|
533
|
+
if (waitForProcessing !== void 0 && typeof waitForProcessing !== "boolean") throw invalidUploaderOptions("waitForProcessing must be boolean");
|
|
534
|
+
const resolvedPoll = resolvePollOptions(poll);
|
|
535
|
+
return {
|
|
536
|
+
token,
|
|
537
|
+
fetcher: fetcher ?? defaultFetch,
|
|
538
|
+
channelId,
|
|
539
|
+
requestTimeoutMs: requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
540
|
+
waitForProcessing: waitForProcessing ?? false,
|
|
541
|
+
pollIntervalMs: resolvedPoll.intervalMs,
|
|
542
|
+
pollMaxAttempts: resolvedPoll.maxAttempts,
|
|
543
|
+
pollTimeoutMs: resolvedPoll.timeoutMs,
|
|
544
|
+
sleep: resolvedPoll.sleep,
|
|
545
|
+
clock: resolvedPoll.clock
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
function resolvePollOptions(poll) {
|
|
549
|
+
if (poll === void 0) return {
|
|
550
|
+
intervalMs: DEFAULT_POLL_INTERVAL_MS,
|
|
551
|
+
maxAttempts: DEFAULT_POLL_MAX_ATTEMPTS,
|
|
552
|
+
timeoutMs: DEFAULT_POLL_TIMEOUT_MS,
|
|
553
|
+
sleep: defaultSleep,
|
|
554
|
+
clock: defaultClock
|
|
555
|
+
};
|
|
556
|
+
assertKnownPlainObject(poll, [
|
|
557
|
+
"intervalMs",
|
|
558
|
+
"maxAttempts",
|
|
559
|
+
"timeoutMs",
|
|
560
|
+
"sleep",
|
|
561
|
+
"clock"
|
|
562
|
+
], "poll options");
|
|
563
|
+
let intervalMs;
|
|
564
|
+
let maxAttempts;
|
|
565
|
+
let timeoutMs;
|
|
566
|
+
let sleep;
|
|
567
|
+
let clock;
|
|
568
|
+
try {
|
|
569
|
+
intervalMs = Reflect.get(poll, "intervalMs");
|
|
570
|
+
maxAttempts = Reflect.get(poll, "maxAttempts");
|
|
571
|
+
timeoutMs = Reflect.get(poll, "timeoutMs");
|
|
572
|
+
sleep = Reflect.get(poll, "sleep");
|
|
573
|
+
clock = Reflect.get(poll, "clock");
|
|
574
|
+
} catch (cause) {
|
|
575
|
+
throw invalidUploaderOptions("poll options could not be read", cause);
|
|
576
|
+
}
|
|
577
|
+
const resolvedInterval = intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
578
|
+
const resolvedAttempts = maxAttempts ?? DEFAULT_POLL_MAX_ATTEMPTS;
|
|
579
|
+
const resolvedTimeout = timeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
|
|
580
|
+
if (!Number.isInteger(resolvedInterval) || resolvedInterval < 0) throw invalidUploaderOptions("poll.intervalMs must be a non-negative integer");
|
|
581
|
+
if (!Number.isInteger(resolvedAttempts) || resolvedAttempts <= 0) throw invalidUploaderOptions("poll.maxAttempts must be a positive integer");
|
|
582
|
+
if (!Number.isFinite(resolvedTimeout) || resolvedTimeout <= 0) throw invalidUploaderOptions("poll.timeoutMs must be positive");
|
|
583
|
+
if (sleep !== void 0 && typeof sleep !== "function") throw invalidUploaderOptions("poll.sleep must be a function");
|
|
584
|
+
if (clock !== void 0 && typeof clock !== "function") throw invalidUploaderOptions("poll.clock must be a function");
|
|
585
|
+
return {
|
|
586
|
+
intervalMs: resolvedInterval,
|
|
587
|
+
maxAttempts: resolvedAttempts,
|
|
588
|
+
timeoutMs: resolvedTimeout,
|
|
589
|
+
sleep: sleep ?? defaultSleep,
|
|
590
|
+
clock: clock ?? defaultClock
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
function assertKnownPlainObject(value, allowed, label) {
|
|
594
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw invalidUploaderOptions(`${label} must be a plain object`);
|
|
595
|
+
let prototype;
|
|
596
|
+
let keys;
|
|
597
|
+
try {
|
|
598
|
+
prototype = Object.getPrototypeOf(value);
|
|
599
|
+
keys = Object.keys(value);
|
|
600
|
+
} catch (cause) {
|
|
601
|
+
throw invalidUploaderOptions(`${label} could not be inspected`, cause);
|
|
602
|
+
}
|
|
603
|
+
if (prototype !== Object.prototype && prototype !== null) throw invalidUploaderOptions(`${label} must be a plain object`);
|
|
604
|
+
const allowedKeys = new Set(allowed);
|
|
605
|
+
for (const key of keys) if (!allowedKeys.has(key)) throw invalidUploaderOptions(`Unknown ${label} key: ${key}`);
|
|
606
|
+
}
|
|
607
|
+
function invalidUploaderOptions(message, cause) {
|
|
608
|
+
return new ConfigurationError(message, {
|
|
609
|
+
code: "invalid_slack_uploader_options",
|
|
610
|
+
stage: "configuration",
|
|
611
|
+
cause
|
|
612
|
+
});
|
|
204
613
|
}
|
|
205
614
|
function encodeForm(fields) {
|
|
206
|
-
|
|
615
|
+
return fields.map(([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`).join("&");
|
|
207
616
|
}
|
|
208
617
|
function isRecord(value) {
|
|
209
|
-
|
|
618
|
+
return typeof value === "object" && value !== null;
|
|
210
619
|
}
|
|
211
620
|
function isNonEmptyString(value) {
|
|
212
|
-
|
|
621
|
+
return typeof value === "string" && value.length > 0;
|
|
213
622
|
}
|
|
214
623
|
function isPositiveNumber(value) {
|
|
215
|
-
|
|
624
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
216
625
|
}
|
|
217
626
|
function isProcessedImageFile(value) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
return isNonEmptyString(value["mimetype"]) && isNonEmptyString(value["filetype"]) && isPositiveNumber(value["original_w"]) && isPositiveNumber(value["original_h"]);
|
|
627
|
+
if (!isRecord(value)) return false;
|
|
628
|
+
return isNonEmptyString(value["mimetype"]) && isNonEmptyString(value["filetype"]) && isPositiveNumber(value["original_w"]) && isPositiveNumber(value["original_h"]);
|
|
222
629
|
}
|
|
223
630
|
function getSlackErrorCode(payload) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
631
|
+
if (!isRecord(payload)) return;
|
|
632
|
+
const error = payload["error"];
|
|
633
|
+
return typeof error === "string" ? error : void 0;
|
|
634
|
+
}
|
|
635
|
+
function processingTimeout(receipt, attempts) {
|
|
636
|
+
return new SlackUploadError(`Slack file processing did not complete after ${String(attempts)} polls`, {
|
|
637
|
+
stage: SlackUploadStage.ProcessingTimeout,
|
|
638
|
+
code: "processing_timeout",
|
|
639
|
+
receipt: slackReceipt({
|
|
640
|
+
...receipt,
|
|
641
|
+
processing: UploadProcessing.Unverified,
|
|
642
|
+
providerCode: "processing_timeout"
|
|
643
|
+
})
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Subclass identity is as bundle-sensitive as the base class, so discriminate on `code`.
|
|
648
|
+
*/
|
|
649
|
+
function isOperationDeadline(error) {
|
|
650
|
+
return isOperationError(error) && error.code === OperationErrorCode.DeadlineExceeded;
|
|
651
|
+
}
|
|
652
|
+
function createStageSignal(control, requestTimeoutMs, clock) {
|
|
653
|
+
const controller = new AbortController();
|
|
654
|
+
let timeoutKind;
|
|
655
|
+
const abortFromCaller = () => {
|
|
656
|
+
controller.abort(control.signal?.reason);
|
|
657
|
+
};
|
|
658
|
+
if (control.signal?.aborted === true) abortFromCaller();
|
|
659
|
+
else control.signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
660
|
+
const requestDeadlineMs = clock() + requestTimeoutMs;
|
|
661
|
+
const operationDeadlineMs = control.deadlineMs ?? Number.POSITIVE_INFINITY;
|
|
662
|
+
const remaining = Math.max(0, Math.min(requestDeadlineMs, operationDeadlineMs) - clock());
|
|
663
|
+
const timeout = setTimeout(() => {
|
|
664
|
+
timeoutKind = operationDeadlineMs <= requestDeadlineMs ? "operation" : "request";
|
|
665
|
+
controller.abort(/* @__PURE__ */ new Error("Slack request deadline exceeded"));
|
|
666
|
+
}, remaining);
|
|
667
|
+
return {
|
|
668
|
+
signal: controller.signal,
|
|
669
|
+
cleanup: () => {
|
|
670
|
+
clearTimeout(timeout);
|
|
671
|
+
control.signal?.removeEventListener("abort", abortFromCaller);
|
|
672
|
+
},
|
|
673
|
+
code: () => {
|
|
674
|
+
if (timeoutKind !== void 0) return "request_timeout";
|
|
675
|
+
return control.signal?.aborted === true ? "aborted" : "fetch_failed";
|
|
676
|
+
},
|
|
677
|
+
operationError: (stage, cause) => {
|
|
678
|
+
if (control.signal?.aborted === true) return new OperationAbortedError(stage, { cause });
|
|
679
|
+
if (timeoutKind === "operation") return new OperationDeadlineExceededError(stage, { cause });
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
function controlledSleep(sleep, milliseconds, control, clock) {
|
|
684
|
+
if (control.signal?.aborted === true) return Promise.reject(new OperationAbortedError(SlackUploadStage.ProcessingTimeout, { cause: control.signal.reason }));
|
|
685
|
+
if (control.deadlineMs !== void 0 && clock() >= control.deadlineMs) return Promise.reject(new OperationDeadlineExceededError(SlackUploadStage.ProcessingTimeout));
|
|
686
|
+
return new Promise((resolve, reject) => {
|
|
687
|
+
const onAbort = () => {
|
|
688
|
+
cleanup();
|
|
689
|
+
reject(new OperationAbortedError(SlackUploadStage.ProcessingTimeout, { cause: control.signal?.reason }));
|
|
690
|
+
};
|
|
691
|
+
const onDeadline = () => {
|
|
692
|
+
cleanup();
|
|
693
|
+
reject(new OperationDeadlineExceededError(SlackUploadStage.ProcessingTimeout));
|
|
694
|
+
};
|
|
695
|
+
const deadlineTimer = control.deadlineMs === void 0 ? void 0 : setTimeout(onDeadline, Math.max(0, control.deadlineMs - clock()));
|
|
696
|
+
const cleanup = () => {
|
|
697
|
+
if (deadlineTimer !== void 0) clearTimeout(deadlineTimer);
|
|
698
|
+
control.signal?.removeEventListener("abort", onAbort);
|
|
699
|
+
};
|
|
700
|
+
control.signal?.addEventListener("abort", onAbort, { once: true });
|
|
701
|
+
sleep(milliseconds).then(() => {
|
|
702
|
+
cleanup();
|
|
703
|
+
resolve();
|
|
704
|
+
}, (error) => {
|
|
705
|
+
cleanup();
|
|
706
|
+
reject(error);
|
|
707
|
+
});
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
function raceWithSignal(promise, signal) {
|
|
711
|
+
if (signal.aborted) return Promise.reject(signal.reason);
|
|
712
|
+
return new Promise((resolve, reject) => {
|
|
713
|
+
const onAbort = () => {
|
|
714
|
+
signal.removeEventListener("abort", onAbort);
|
|
715
|
+
reject(signal.reason);
|
|
716
|
+
};
|
|
717
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
718
|
+
promise.then((value) => {
|
|
719
|
+
signal.removeEventListener("abort", onAbort);
|
|
720
|
+
resolve(value);
|
|
721
|
+
}, (error) => {
|
|
722
|
+
signal.removeEventListener("abort", onAbort);
|
|
723
|
+
reject(error);
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
function slackReceipt(options) {
|
|
728
|
+
return Object.freeze({
|
|
729
|
+
kind: "upload",
|
|
730
|
+
provider: "slack",
|
|
731
|
+
phase: options.phase,
|
|
732
|
+
fileId: options.fileId,
|
|
733
|
+
shared: options.shared ?? false,
|
|
734
|
+
processing: options.processing,
|
|
735
|
+
status: options.status,
|
|
736
|
+
retryAfterMs: options.retryAfterMs,
|
|
737
|
+
providerCode: options.providerCode,
|
|
738
|
+
retrySafety: options.retrySafety ?? RetrySafety.Never
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
function withReceipt(error, receipt) {
|
|
742
|
+
return new SlackUploadError(error.message, {
|
|
743
|
+
stage: error.stage,
|
|
744
|
+
status: error.status,
|
|
745
|
+
code: error.code,
|
|
746
|
+
retryable: error.retryable,
|
|
747
|
+
retryAfterMs: error.retryAfterMs,
|
|
748
|
+
receipt: slackReceipt({
|
|
749
|
+
...receipt,
|
|
750
|
+
status: error.status,
|
|
751
|
+
retryAfterMs: error.retryAfterMs,
|
|
752
|
+
providerCode: error.code,
|
|
753
|
+
retrySafety: receipt.phase === UploadPhase.Requested ? RetrySafety.Safe : receipt.phase === UploadPhase.Completed || receipt.phase === UploadPhase.Shared ? RetrySafety.Never : RetrySafety.MayDuplicate
|
|
754
|
+
}),
|
|
755
|
+
cause: error.cause
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
function retryAfterMs(response) {
|
|
759
|
+
const raw = response.headers?.get("retry-after");
|
|
760
|
+
if (raw === void 0 || raw === null) return;
|
|
761
|
+
const seconds = Number(raw);
|
|
762
|
+
return Number.isFinite(seconds) && seconds >= 0 ? seconds * 1e3 : void 0;
|
|
238
763
|
}
|
|
239
764
|
function defaultFetch(input, init) {
|
|
240
|
-
|
|
765
|
+
return globalThis.fetch(input, init);
|
|
241
766
|
}
|
|
242
767
|
function defaultSleep(milliseconds) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
768
|
+
return new Promise((resolve) => {
|
|
769
|
+
setTimeout(resolve, milliseconds);
|
|
770
|
+
});
|
|
246
771
|
}
|
|
247
772
|
function defaultClock() {
|
|
248
|
-
|
|
773
|
+
return Date.now();
|
|
249
774
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
SlackUploadStage
|
|
254
|
-
};
|
|
775
|
+
//#endregion
|
|
776
|
+
export { FetchSlackUploader, SlackUploadError, SlackUploadStage, isConfigurationError, isOperationError, isSlackUploadError };
|
|
777
|
+
|
|
255
778
|
//# sourceMappingURL=slack.js.map
|