@honua/react 0.1.4-beta.0 → 0.1.9-beta.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/contract/types.d.ts +1 -1
- package/contract/types.d.ts.map +1 -1
- package/contract/types.js +4 -0
- package/control-plane/admin-client.d.ts +87 -0
- package/control-plane/admin-client.d.ts.map +1 -0
- package/control-plane/admin-client.js +169 -0
- package/control-plane/client.d.ts.map +1 -1
- package/control-plane/client.js +21 -3
- package/control-plane/commands/catalog.d.ts +133 -0
- package/control-plane/commands/catalog.d.ts.map +1 -0
- package/control-plane/commands/catalog.js +353 -0
- package/control-plane/commands/errors.d.ts +96 -0
- package/control-plane/commands/errors.d.ts.map +1 -0
- package/control-plane/commands/errors.js +130 -0
- package/control-plane/commands/identity.d.ts +71 -0
- package/control-plane/commands/identity.d.ts.map +1 -0
- package/control-plane/commands/identity.js +0 -0
- package/control-plane/commands/index.d.ts +39 -0
- package/control-plane/commands/index.d.ts.map +1 -0
- package/control-plane/commands/index.js +34 -0
- package/control-plane/commands/runtime.d.ts +146 -0
- package/control-plane/commands/runtime.d.ts.map +1 -0
- package/control-plane/commands/runtime.js +318 -0
- package/control-plane/commands/types.d.ts +226 -0
- package/control-plane/commands/types.d.ts.map +1 -0
- package/control-plane/commands/types.js +30 -0
- package/control-plane/commands/validate.d.ts +27 -0
- package/control-plane/commands/validate.d.ts.map +1 -0
- package/control-plane/commands/validate.js +132 -0
- package/control-plane/generated/admin-api.d.ts +30263 -0
- package/control-plane/generated/admin-api.d.ts.map +1 -0
- package/control-plane/generated/admin-api.js +5 -0
- package/control-plane/generated/admin-operations.d.ts +4053 -0
- package/control-plane/generated/admin-operations.d.ts.map +1 -0
- package/control-plane/generated/admin-operations.js +820 -0
- package/control-plane/index.d.ts +11 -0
- package/control-plane/index.d.ts.map +1 -1
- package/control-plane/index.js +8 -0
- package/control-plane/types.d.ts +14 -0
- package/control-plane/types.d.ts.map +1 -1
- package/core/client.d.ts +6 -0
- package/core/client.d.ts.map +1 -1
- package/core/client.js +12 -10
- package/core/credential-redaction.d.ts +163 -0
- package/core/credential-redaction.d.ts.map +1 -0
- package/core/credential-redaction.js +389 -0
- package/core/error-classifications.d.ts +32 -0
- package/core/error-classifications.d.ts.map +1 -1
- package/core/error-classifications.js +32 -0
- package/core/error-code-registry.d.ts +33 -1
- package/core/error-code-registry.d.ts.map +1 -1
- package/core/error-code-registry.js +32 -0
- package/core/errors.d.ts +1 -1
- package/core/errors.d.ts.map +1 -1
- package/core/geoparquet-sql.d.ts +1 -1
- package/core/geoparquet-sql.js +2 -2
- package/core/grpc-adapter.d.ts +2 -2
- package/core/grpc-adapter.d.ts.map +1 -1
- package/core/grpc-adapter.js +30 -6
- package/core/job-run-errors.d.ts +10 -0
- package/core/job-run-errors.d.ts.map +1 -0
- package/core/job-run-errors.js +14 -0
- package/core/job-run-lifecycle.d.ts +34 -0
- package/core/job-run-lifecycle.d.ts.map +1 -0
- package/core/job-run-lifecycle.js +193 -0
- package/core/ogc-processes.d.ts +38 -31
- package/core/ogc-processes.d.ts.map +1 -1
- package/core/ogc-processes.js +123 -173
- package/core/process-runner.js +15 -100
- package/core/stac.d.ts +2 -1
- package/core/stac.d.ts.map +1 -1
- package/core/stac.js +29 -11
- package/core/surfaces.d.ts +2 -8
- package/core/surfaces.d.ts.map +1 -1
- package/core/surfaces.js +21 -112
- package/core/types.d.ts +22 -8
- package/core/types.d.ts.map +1 -1
- package/gen/geospatial/v1/common_pb.d.ts +514 -0
- package/gen/geospatial/v1/common_pb.d.ts.map +1 -0
- package/gen/geospatial/v1/common_pb.js +335 -0
- package/gen/geospatial/v1/execution_types_pb.d.ts +1186 -0
- package/gen/geospatial/v1/execution_types_pb.d.ts.map +1 -0
- package/gen/geospatial/v1/execution_types_pb.js +396 -0
- package/gen/geospatial/v1/feature_service_pb.d.ts +403 -0
- package/gen/geospatial/v1/feature_service_pb.d.ts.map +1 -0
- package/gen/geospatial/v1/feature_service_pb.js +47 -0
- package/gen/geospatial/v1/index.d.ts +5 -0
- package/gen/geospatial/v1/index.d.ts.map +1 -0
- package/gen/geospatial/v1/index.js +9 -0
- package/gen/geospatial/v1/spatial_types_pb.d.ts +285 -0
- package/gen/geospatial/v1/spatial_types_pb.d.ts.map +1 -0
- package/gen/geospatial/v1/spatial_types_pb.js +58 -0
- package/gen/geospatial/v1/workspace_artifact_types_pb.d.ts +379 -0
- package/gen/geospatial/v1/workspace_artifact_types_pb.d.ts.map +1 -0
- package/gen/geospatial/v1/workspace_artifact_types_pb.js +151 -0
- package/generated-app/projection.d.ts.map +1 -1
- package/generated-app/projection.js +10 -1
- package/interactions/declarative-contract.d.ts +149 -0
- package/interactions/declarative-contract.d.ts.map +1 -0
- package/interactions/declarative-contract.js +293 -0
- package/interactions/declarative.d.ts +198 -0
- package/interactions/declarative.d.ts.map +1 -0
- package/interactions/declarative.js +470 -0
- package/interactions/feature-state.d.ts +7 -0
- package/interactions/feature-state.d.ts.map +1 -1
- package/interactions/feature-state.js +10 -2
- package/map/maplibre-target.js +1 -1
- package/package.json +2 -2
- package/query-planner/grpc.d.ts +4 -4
- package/query-planner/grpc.d.ts.map +1 -1
- package/query-planner/grpc.js +11 -11
- package/query-planner/types.d.ts +4 -4
- package/query-planner/types.d.ts.map +1 -1
- package/realtime/sse.d.ts.map +1 -1
- package/realtime/sse.js +7 -1
- package/runtime/errors.d.ts +1 -1
- package/runtime/errors.d.ts.map +1 -1
- package/runtime/errors.js +2 -0
- package/runtime/generated/map-package-schema-meta.d.ts +11 -0
- package/runtime/generated/map-package-schema-meta.d.ts.map +1 -0
- package/runtime/generated/map-package-schema-meta.js +12 -0
- package/runtime/generated/map-package-schema-validator.d.ts +16 -0
- package/runtime/generated/map-package-schema-validator.d.ts.map +1 -0
- package/runtime/generated/map-package-schema-validator.js +1422 -0
- package/runtime/index.d.ts +5 -1
- package/runtime/index.d.ts.map +1 -1
- package/runtime/index.js +6 -0
- package/runtime/map-package-export.d.ts +151 -0
- package/runtime/map-package-export.d.ts.map +1 -0
- package/runtime/map-package-export.js +636 -0
- package/runtime/map-package-validation.d.ts +21 -1
- package/runtime/map-package-validation.d.ts.map +1 -1
- package/runtime/map-package-validation.js +132 -95
- package/runtime/map-package.d.ts +80 -0
- package/runtime/map-package.d.ts.map +1 -1
- package/runtime/style-compose.d.ts.map +1 -1
- package/runtime/style-compose.js +78 -9
- package/studio/chart-spec.d.ts +6 -6
- package/studio/chart-spec.d.ts.map +1 -1
- package/studio/chart-spec.js +6 -6
- package/studio/index.d.ts +17 -7
- package/studio/index.d.ts.map +1 -1
- package/studio/index.js +15 -5
- package/studio/lifecycle-client.d.ts +262 -22
- package/studio/lifecycle-client.d.ts.map +1 -1
- package/studio/lifecycle-client.js +524 -24
- package/studio/lifecycle-errors.d.ts +8 -2
- package/studio/lifecycle-errors.d.ts.map +1 -1
- package/studio/lifecycle-errors.js +6 -1
- package/studio/lifecycle-types.d.ts +228 -8
- package/studio/lifecycle-types.d.ts.map +1 -1
- package/studio/publication-status.d.ts +55 -0
- package/studio/publication-status.d.ts.map +1 -0
- package/studio/publication-status.js +103 -0
- package/gen/honua/v1/feature_service_pb.d.ts +0 -918
- package/gen/honua/v1/feature_service_pb.d.ts.map +0 -1
- package/gen/honua/v1/feature_service_pb.js +0 -375
package/core/ogc-processes.js
CHANGED
|
@@ -23,10 +23,9 @@
|
|
|
23
23
|
*
|
|
24
24
|
* @module
|
|
25
25
|
*/
|
|
26
|
-
import {
|
|
27
|
-
import { HonuaSdkError } from "./error-envelope.js";
|
|
26
|
+
import { isJobTerminal } from "../contract/jobs.js";
|
|
28
27
|
import { HonuaCapabilityNotSupportedError } from "./errors.js";
|
|
29
|
-
import {
|
|
28
|
+
import { JobRunLifecycle } from "./job-run-lifecycle.js";
|
|
30
29
|
import { createOgcMetadataParams, mergeHeaders } from "./wire-shared.js";
|
|
31
30
|
const DEFAULT_POLL_INTERVAL_MS = 1_000;
|
|
32
31
|
/**
|
|
@@ -35,7 +34,6 @@ const DEFAULT_POLL_INTERVAL_MS = 1_000;
|
|
|
35
34
|
* never reaches a terminal state must not poll forever. Callers who genuinely
|
|
36
35
|
* want a longer wait pass their own `deadlineMs`.
|
|
37
36
|
*/
|
|
38
|
-
const DEFAULT_POLL_DEADLINE_MS = 600_000;
|
|
39
37
|
/** Honua facade path prefix for OGC API Processes. */
|
|
40
38
|
const PROCESSES_FACADE_BASE = "/ogc/processes";
|
|
41
39
|
/**
|
|
@@ -49,7 +47,11 @@ const PROCESSES_CONFORMANCE = {
|
|
|
49
47
|
core: "http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/core",
|
|
50
48
|
dismiss: "http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/dismiss",
|
|
51
49
|
};
|
|
52
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Suffix matchers tolerant of a server publishing the class under a different
|
|
52
|
+
* host or path prefix, but *not* of a different class that merely starts with
|
|
53
|
+
* one of these. See {@link declaresConformanceClass}.
|
|
54
|
+
*/
|
|
53
55
|
const PROCESSES_CONFORMANCE_MATCH = {
|
|
54
56
|
core: "processes-1/1.0/conf/core",
|
|
55
57
|
dismiss: "processes-1/1.0/conf/dismiss",
|
|
@@ -77,6 +79,36 @@ const JOB_CONTROL = {
|
|
|
77
79
|
function capabilityRefusal(capability, sourceId, context) {
|
|
78
80
|
return new HonuaCapabilityNotSupportedError(capability, "ogc-processes", sourceId, { context });
|
|
79
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* `true` when the server declares exactly the named conformance class.
|
|
84
|
+
*
|
|
85
|
+
* The public `hasOgcConformanceClass()` is a documented *substring* test, which
|
|
86
|
+
* is the right tool for a caller probing an extension they already recognise.
|
|
87
|
+
* It is the wrong tool for a fail-closed gate: an unrecognised class that
|
|
88
|
+
* happens to extend one of ours — `…/conf/dismiss-disabled`, `…/conf/core-lite`,
|
|
89
|
+
* or a vendor class carrying the URI in a query string — would widen what the
|
|
90
|
+
* client is willing to send. So the gate anchors the match at the end of the
|
|
91
|
+
* class URI (ignoring a trailing slash, query, or fragment): a prefix the
|
|
92
|
+
* server chose is still tolerated, an unknown longer class is not.
|
|
93
|
+
*/
|
|
94
|
+
function declaresConformanceClass(conformance, classSuffix) {
|
|
95
|
+
for (const uri of conformance?.conformsTo ?? []) {
|
|
96
|
+
if (typeof uri !== "string")
|
|
97
|
+
continue;
|
|
98
|
+
const withoutFragment = uri.split("#")[0] ?? "";
|
|
99
|
+
let path = withoutFragment.split("?")[0] ?? "";
|
|
100
|
+
// Trailing slashes are trimmed by scanning, not by a `/+$/` regex: this
|
|
101
|
+
// string is server input, and that pattern backtracks on a URI made of
|
|
102
|
+
// many slashes.
|
|
103
|
+
let end = path.length;
|
|
104
|
+
while (end > 0 && path.charCodeAt(end - 1) === 47)
|
|
105
|
+
end -= 1;
|
|
106
|
+
path = path.slice(0, end);
|
|
107
|
+
if (path.endsWith(classSuffix))
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
80
112
|
/** `true` when the declaration list contains `option` (case-insensitive). */
|
|
81
113
|
function declares(options, option) {
|
|
82
114
|
if (!options)
|
|
@@ -226,9 +258,12 @@ export class HonuaOgcProcesses {
|
|
|
226
258
|
declaredConformance;
|
|
227
259
|
conformancePromise;
|
|
228
260
|
/**
|
|
229
|
-
* `jobControlOptions` learned from `describe()` calls made through
|
|
230
|
-
* handle. Callers that follow the natural discover → describe → execute
|
|
231
|
-
* therefore get process-level gating for free, with no extra round trip.
|
|
261
|
+
* `jobControlOptions` learned from `describe()` / `list()` calls made through
|
|
262
|
+
* this handle. Callers that follow the natural discover → describe → execute
|
|
263
|
+
* flow therefore get process-level gating for free, with no extra round trip.
|
|
264
|
+
*
|
|
265
|
+
* Each entry remembers which source taught it, because a later listing must
|
|
266
|
+
* not be able to downgrade what a description already established.
|
|
232
267
|
*/
|
|
233
268
|
jobControlByProcess = new Map();
|
|
234
269
|
constructor(options) {
|
|
@@ -253,19 +288,36 @@ export class HonuaOgcProcesses {
|
|
|
253
288
|
async list(request = {}) {
|
|
254
289
|
const response = await this.client.listOgcProcesses(this.withBase(request));
|
|
255
290
|
for (const process of response?.processes ?? []) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
291
|
+
// A summary that omits `jobControlOptions` advertises no execution mode;
|
|
292
|
+
// recorded as an empty declaration so the mode gate refuses rather than
|
|
293
|
+
// reading silence as consent — but only where nothing better is known.
|
|
294
|
+
if (process?.id)
|
|
295
|
+
this.rememberJobControl(process.id, process.jobControlOptions, "summary");
|
|
259
296
|
}
|
|
260
297
|
return response;
|
|
261
298
|
}
|
|
262
299
|
async describe(processId, request = {}) {
|
|
263
300
|
const description = await this.client.getOgcProcess(this.withBase({ ...request, processId }));
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
301
|
+
// As in `list()`: an omitted `jobControlOptions` is an empty declaration.
|
|
302
|
+
// A description is the authoritative source, so this always wins.
|
|
303
|
+
this.rememberJobControl(processId, description?.jobControlOptions, "description");
|
|
267
304
|
return description;
|
|
268
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* Record what a process advertises, keeping the more authoritative source.
|
|
308
|
+
*
|
|
309
|
+
* Silence is still not consent — an omitted `jobControlOptions` is stored as
|
|
310
|
+
* an empty declaration, and an explicit mode against it is refused. But a
|
|
311
|
+
* process summary may legally omit what the full description declares, so a
|
|
312
|
+
* `list()` refresh after a `describe()` must not overwrite the description's
|
|
313
|
+
* answer with the listing's silence. Refusing an execution the server did
|
|
314
|
+
* declare is as wrong as permitting one it did not.
|
|
315
|
+
*/
|
|
316
|
+
rememberJobControl(processId, options, source) {
|
|
317
|
+
if (source === "summary" && this.jobControlByProcess.get(processId)?.source === "description")
|
|
318
|
+
return;
|
|
319
|
+
this.jobControlByProcess.set(processId, { options: [...(options ?? [])], source });
|
|
320
|
+
}
|
|
269
321
|
/**
|
|
270
322
|
* Submit a process for execution and return an `IJobRun` — always, so the
|
|
271
323
|
* caller reads results through one surface whichever Core response shape the
|
|
@@ -289,7 +341,7 @@ export class HonuaOgcProcesses {
|
|
|
289
341
|
const conformance = await this.assertExecutionDeclared(request);
|
|
290
342
|
const accepted = await this.client.executeOgcProcess(executeRequest);
|
|
291
343
|
const processId = accepted.processID ?? request.processId;
|
|
292
|
-
const jobControlOptions = request.jobControlOptions ?? this.jobControlByProcess.get(request.processId);
|
|
344
|
+
const jobControlOptions = request.jobControlOptions ?? this.jobControlByProcess.get(request.processId)?.options;
|
|
293
345
|
if (accepted.synchronous === true) {
|
|
294
346
|
// Core assigns no job identifier to a synchronous execution; the results
|
|
295
347
|
// document is already in hand, so the run starts terminal.
|
|
@@ -317,7 +369,7 @@ export class HonuaOgcProcesses {
|
|
|
317
369
|
/** Adopt an existing job by id (useful when reconnecting after navigation). */
|
|
318
370
|
job(jobId, options = {}) {
|
|
319
371
|
const { basePath } = this.withBase(options);
|
|
320
|
-
const jobControlOptions = options.processId ? this.jobControlByProcess.get(options.processId) : undefined;
|
|
372
|
+
const jobControlOptions = options.processId ? this.jobControlByProcess.get(options.processId)?.options : undefined;
|
|
321
373
|
return new HonuaOgcProcessJobRun({
|
|
322
374
|
client: this.client,
|
|
323
375
|
jobId,
|
|
@@ -364,7 +416,11 @@ export class HonuaOgcProcesses {
|
|
|
364
416
|
*/
|
|
365
417
|
async assertExecutionDeclared(request) {
|
|
366
418
|
const conformance = await this.resolveConformance(request.signal);
|
|
367
|
-
|
|
419
|
+
// Under `"strict"` the declaration was fetched, so silence is an answer:
|
|
420
|
+
// a document that declares no Core class — or none at all — refuses. Under
|
|
421
|
+
// `"advertised"` only a declaration actually in hand can refuse.
|
|
422
|
+
const declarationInHand = conformance?.conformsTo !== undefined || this.capabilityPolicy === "strict";
|
|
423
|
+
if (declarationInHand && !declaresConformanceClass(conformance, PROCESSES_CONFORMANCE_MATCH.core)) {
|
|
368
424
|
throw capabilityRefusal("processes.execute", request.processId, {
|
|
369
425
|
missingClass: PROCESSES_CONFORMANCE.core,
|
|
370
426
|
construct: "execute",
|
|
@@ -375,11 +431,15 @@ export class HonuaOgcProcesses {
|
|
|
375
431
|
// shapes land on the same `IJobRun`. Only an explicit preference is gated.
|
|
376
432
|
if (mode === "auto")
|
|
377
433
|
return conformance;
|
|
378
|
-
let jobControlOptions = request.jobControlOptions ?? this.jobControlByProcess.get(request.processId);
|
|
434
|
+
let jobControlOptions = request.jobControlOptions ?? this.jobControlByProcess.get(request.processId)?.options;
|
|
379
435
|
if (!jobControlOptions && this.capabilityPolicy === "strict") {
|
|
380
436
|
const description = await this.describe(request.processId, request.signal ? { signal: request.signal } : {});
|
|
381
|
-
jobControlOptions
|
|
437
|
+
// An omitted `jobControlOptions` is an empty declaration, never a wildcard:
|
|
438
|
+
// the server was asked and advertised no execution mode at all.
|
|
439
|
+
jobControlOptions = description.jobControlOptions ?? [];
|
|
382
440
|
}
|
|
441
|
+
// Only a process this handle has never seen described is unknown; every
|
|
442
|
+
// observed description contributes a declaration, empty or otherwise.
|
|
383
443
|
if (!jobControlOptions)
|
|
384
444
|
return conformance;
|
|
385
445
|
const required = mode === "sync" ? JOB_CONTROL.sync : JOB_CONTROL.async;
|
|
@@ -413,11 +473,7 @@ export class HonuaOgcProcessJobRun {
|
|
|
413
473
|
/** Server-advertised routes, refreshed from every `links[]` the job reports. */
|
|
414
474
|
statusPath;
|
|
415
475
|
resultsPath;
|
|
416
|
-
|
|
417
|
-
currentProgress;
|
|
418
|
-
terminalSnapshot;
|
|
419
|
-
terminalPromise;
|
|
420
|
-
listeners = new Set();
|
|
476
|
+
lifecycle;
|
|
421
477
|
constructor(options) {
|
|
422
478
|
this.client = options.client;
|
|
423
479
|
this.id = options.jobId;
|
|
@@ -433,8 +489,14 @@ export class HonuaOgcProcessJobRun {
|
|
|
433
489
|
this.pollFn = options.pollFn ?? ((jobId, signal) => this.fetchStatus(jobId, signal));
|
|
434
490
|
const initial = options.initialStatus;
|
|
435
491
|
this.absorbLinks(initial?.links);
|
|
436
|
-
this.
|
|
437
|
-
|
|
492
|
+
this.lifecycle = new JobRunLifecycle({
|
|
493
|
+
id: this.id,
|
|
494
|
+
initialStatus: initial?.status ?? "accepted",
|
|
495
|
+
initialProgress: progressFromOgcStatus(initial),
|
|
496
|
+
pollIntervalMs: () => this.pollIntervalMs,
|
|
497
|
+
pollBudget: this.pollBudget,
|
|
498
|
+
poll: async (signal) => this.translateOgcStatus(await this.pollFn(this.id, signal)),
|
|
499
|
+
});
|
|
438
500
|
}
|
|
439
501
|
async fetchStatus(jobId, signal) {
|
|
440
502
|
if (this.capabilityPolicy === "strict" && this.statusPath === undefined) {
|
|
@@ -470,57 +532,32 @@ export class HonuaOgcProcessJobRun {
|
|
|
470
532
|
}
|
|
471
533
|
}
|
|
472
534
|
get status() {
|
|
473
|
-
return this.
|
|
535
|
+
return this.lifecycle.status;
|
|
474
536
|
}
|
|
475
537
|
get progress() {
|
|
476
|
-
return this.
|
|
538
|
+
return this.lifecycle.progress;
|
|
477
539
|
}
|
|
478
540
|
async poll() {
|
|
479
|
-
|
|
480
|
-
return this.terminalSnapshot;
|
|
481
|
-
}
|
|
482
|
-
const ogcStatus = await this.pollFn(this.id);
|
|
483
|
-
return this.handleOgcStatus(ogcStatus);
|
|
541
|
+
return this.lifecycle.poll();
|
|
484
542
|
}
|
|
485
543
|
watch(listener) {
|
|
486
|
-
this.
|
|
487
|
-
return () => this.listeners.delete(listener);
|
|
544
|
+
return this.lifecycle.watch(listener);
|
|
488
545
|
}
|
|
489
546
|
async results(options = {}) {
|
|
490
|
-
|
|
491
|
-
// Reset the cached promise if the poll loop rejects (abort / deadline /
|
|
492
|
-
// attempt cap) so a later results() call can retry rather than being
|
|
493
|
-
// permanently poisoned by a transient cancellation.
|
|
494
|
-
this.terminalPromise = this.runUntilTerminal(this.resolveBudget(options)).catch((error) => {
|
|
495
|
-
this.terminalPromise = undefined;
|
|
496
|
-
throw error;
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
return this.terminalPromise;
|
|
500
|
-
}
|
|
501
|
-
/**
|
|
502
|
-
* Layer the caller's bounds over the handle's default budget, then guarantee
|
|
503
|
-
* a bound exists. NFR-001: `results()` is never an unbounded retry loop, so a
|
|
504
|
-
* caller that names neither a deadline nor an attempt cap still inherits
|
|
505
|
-
* {@link DEFAULT_POLL_DEADLINE_MS}.
|
|
506
|
-
*/
|
|
507
|
-
resolveBudget(options) {
|
|
508
|
-
const merged = { ...this.pollBudget, ...options };
|
|
509
|
-
if (merged.deadlineMs !== undefined || merged.maxAttempts !== undefined)
|
|
510
|
-
return merged;
|
|
511
|
-
return { ...merged, deadlineMs: DEFAULT_POLL_DEADLINE_MS };
|
|
547
|
+
return this.lifecycle.results(options);
|
|
512
548
|
}
|
|
513
549
|
async cancel() {
|
|
514
|
-
if (this.
|
|
515
|
-
return this.
|
|
516
|
-
}
|
|
550
|
+
if (this.lifecycle.terminal)
|
|
551
|
+
return this.lifecycle.status;
|
|
517
552
|
this.assertDismissDeclared();
|
|
553
|
+
return this.lifecycle.cancel(() => this.cancelSnapshot());
|
|
554
|
+
}
|
|
555
|
+
async cancelSnapshot() {
|
|
518
556
|
try {
|
|
519
557
|
// DELETE targets the job resource itself, so it follows the same
|
|
520
558
|
// advertised route the status polls use rather than re-templating it.
|
|
521
559
|
const cancelled = await this.client.cancelOgcProcessJob(this.jobRequest(this.statusPath));
|
|
522
|
-
|
|
523
|
-
return snapshot.status;
|
|
560
|
+
return this.translateOgcStatus(cancelled);
|
|
524
561
|
}
|
|
525
562
|
catch (error) {
|
|
526
563
|
const statusCode = error?.statusCode;
|
|
@@ -540,7 +577,7 @@ export class HonuaOgcProcessJobRun {
|
|
|
540
577
|
// "completed job" claim cannot be confirmed and the original 409
|
|
541
578
|
// is the most honest signal.
|
|
542
579
|
if (statusCode === 404) {
|
|
543
|
-
return this.
|
|
580
|
+
return { status: this.lifecycle.status, progress: this.lifecycle.progress };
|
|
544
581
|
}
|
|
545
582
|
if (statusCode === 409 && isCompletedJobConflict(error)) {
|
|
546
583
|
let fresh;
|
|
@@ -553,67 +590,24 @@ export class HonuaOgcProcessJobRun {
|
|
|
553
590
|
// 409 instead of letting the poll-side error swallow it.
|
|
554
591
|
throw error;
|
|
555
592
|
}
|
|
556
|
-
const snapshot = await this.
|
|
593
|
+
const snapshot = await this.translateOgcStatus(fresh);
|
|
557
594
|
if (!isJobTerminal(snapshot.status)) {
|
|
558
595
|
throw error;
|
|
559
596
|
}
|
|
560
|
-
return snapshot
|
|
597
|
+
return snapshot;
|
|
561
598
|
}
|
|
562
599
|
throw error;
|
|
563
600
|
}
|
|
564
601
|
}
|
|
565
|
-
async runUntilTerminal(options = {}) {
|
|
566
|
-
const { signal } = options;
|
|
567
|
-
const baseIntervalMs = options.pollIntervalMs ?? this.pollIntervalMs;
|
|
568
|
-
const maxIntervalMs = options.maxPollIntervalMs ?? Math.max(baseIntervalMs, 30_000);
|
|
569
|
-
const startedAt = Date.now();
|
|
570
|
-
let attempts = 0;
|
|
571
|
-
while (!this.terminalSnapshot) {
|
|
572
|
-
if (signal?.aborted) {
|
|
573
|
-
throw new HonuaJobPollTimeoutError(`Job ${this.id} poll aborted`, "aborted", this.id, this.currentStatus);
|
|
574
|
-
}
|
|
575
|
-
if (options.maxAttempts !== undefined && attempts >= options.maxAttempts) {
|
|
576
|
-
throw new HonuaJobPollTimeoutError(`Job ${this.id} did not reach a terminal state within ${options.maxAttempts} poll attempt(s)`, "max-attempts", this.id, this.currentStatus);
|
|
577
|
-
}
|
|
578
|
-
let ogcStatus;
|
|
579
|
-
try {
|
|
580
|
-
ogcStatus = await this.pollFn(this.id, signal);
|
|
581
|
-
}
|
|
582
|
-
catch (error) {
|
|
583
|
-
if (signal?.aborted) {
|
|
584
|
-
throw new HonuaJobPollTimeoutError(`Job ${this.id} poll aborted`, "aborted", this.id, this.currentStatus);
|
|
585
|
-
}
|
|
586
|
-
throw error;
|
|
587
|
-
}
|
|
588
|
-
attempts += 1;
|
|
589
|
-
await this.handleOgcStatus(ogcStatus);
|
|
590
|
-
if (this.terminalSnapshot)
|
|
591
|
-
break;
|
|
592
|
-
if (options.deadlineMs !== undefined && Date.now() - startedAt >= options.deadlineMs) {
|
|
593
|
-
throw new HonuaJobPollTimeoutError(`Job ${this.id} did not reach a terminal state within ${options.deadlineMs}ms`, "deadline", this.id, this.currentStatus);
|
|
594
|
-
}
|
|
595
|
-
// Capped exponential backoff instead of a fixed interval.
|
|
596
|
-
const intervalMs = Math.min(maxIntervalMs, baseIntervalMs * 2 ** (attempts - 1));
|
|
597
|
-
if (intervalMs > 0) {
|
|
598
|
-
await delay(intervalMs, signal);
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
if (this.terminalSnapshot.status === "successful" && this.terminalSnapshot.result) {
|
|
602
|
-
return this.terminalSnapshot.result;
|
|
603
|
-
}
|
|
604
|
-
throw makeJobFailedError(this.terminalSnapshot);
|
|
605
|
-
}
|
|
606
602
|
/**
|
|
607
603
|
* Translate an OGC `statusInfo` payload onto the canonical snapshot
|
|
608
604
|
* surface, fire watchers, and (for `successful` terminals) fetch the
|
|
609
605
|
* result document inline so the snapshot's `result.outputs` is
|
|
610
606
|
* populated by the time `runUntilTerminal` / `poll` resolves.
|
|
611
607
|
*/
|
|
612
|
-
async
|
|
608
|
+
async translateOgcStatus(ogcStatus) {
|
|
613
609
|
const status = ogcStatus.status ?? "accepted";
|
|
614
610
|
const progress = progressFromOgcStatus(ogcStatus);
|
|
615
|
-
this.currentStatus = status;
|
|
616
|
-
this.currentProgress = progress;
|
|
617
611
|
// Routes the server publishes on this document win over the Core template.
|
|
618
612
|
this.absorbLinks(ogcStatus.links);
|
|
619
613
|
if (status === "successful") {
|
|
@@ -634,8 +628,6 @@ export class HonuaOgcProcessJobRun {
|
|
|
634
628
|
progress,
|
|
635
629
|
result: { outputs: results },
|
|
636
630
|
};
|
|
637
|
-
this.terminalSnapshot = snapshot;
|
|
638
|
-
this.notify(snapshot);
|
|
639
631
|
return snapshot;
|
|
640
632
|
}
|
|
641
633
|
catch (error) {
|
|
@@ -653,9 +645,6 @@ export class HonuaOgcProcessJobRun {
|
|
|
653
645
|
message: error instanceof Error ? error.message : String(error),
|
|
654
646
|
},
|
|
655
647
|
};
|
|
656
|
-
this.currentStatus = "failed";
|
|
657
|
-
this.terminalSnapshot = failure;
|
|
658
|
-
this.notify(failure);
|
|
659
648
|
return failure;
|
|
660
649
|
}
|
|
661
650
|
}
|
|
@@ -666,12 +655,9 @@ export class HonuaOgcProcessJobRun {
|
|
|
666
655
|
progress,
|
|
667
656
|
...(error ? { error } : {}),
|
|
668
657
|
};
|
|
669
|
-
this.terminalSnapshot = snapshot;
|
|
670
|
-
this.notify(snapshot);
|
|
671
658
|
return snapshot;
|
|
672
659
|
}
|
|
673
660
|
const snapshot = { status, progress };
|
|
674
|
-
this.notify(snapshot);
|
|
675
661
|
return snapshot;
|
|
676
662
|
}
|
|
677
663
|
/**
|
|
@@ -690,6 +676,10 @@ export class HonuaOgcProcessJobRun {
|
|
|
690
676
|
* in hand and neither the Dismiss conformance class nor the process's
|
|
691
677
|
* `jobControlOptions` includes `dismiss`, refuse before issuing a DELETE the
|
|
692
678
|
* server never advertised.
|
|
679
|
+
*
|
|
680
|
+
* Under `"strict"` the absence of any declaration refuses too: a handle that
|
|
681
|
+
* resolves declarations for itself has no honest reason to DELETE a job on a
|
|
682
|
+
* server that never said it could be dismissed.
|
|
693
683
|
*/
|
|
694
684
|
assertDismissDeclared() {
|
|
695
685
|
// Under `"strict"` the DELETE route must have been advertised, exactly as
|
|
@@ -703,25 +693,15 @@ export class HonuaOgcProcessJobRun {
|
|
|
703
693
|
if (declares(this.jobControlOptions, JOB_CONTROL.dismiss))
|
|
704
694
|
return;
|
|
705
695
|
const conformsTo = this.conformance?.conformsTo;
|
|
706
|
-
if (!conformsTo)
|
|
696
|
+
if (!conformsTo && this.capabilityPolicy !== "strict")
|
|
707
697
|
return;
|
|
708
|
-
if (
|
|
698
|
+
if (declaresConformanceClass(this.conformance, PROCESSES_CONFORMANCE_MATCH.dismiss))
|
|
709
699
|
return;
|
|
710
700
|
throw capabilityRefusal("processes.dismiss", this.type, {
|
|
711
701
|
missingClass: PROCESSES_CONFORMANCE.dismiss,
|
|
712
702
|
construct: JOB_CONTROL.dismiss,
|
|
713
703
|
});
|
|
714
704
|
}
|
|
715
|
-
notify(snapshot) {
|
|
716
|
-
for (const listener of this.listeners) {
|
|
717
|
-
try {
|
|
718
|
-
listener(snapshot);
|
|
719
|
-
}
|
|
720
|
-
catch {
|
|
721
|
-
// Listener exceptions must not break the runner; log silently.
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
705
|
}
|
|
726
706
|
/**
|
|
727
707
|
* `IJobRun` for a synchronous execution (Core §7.11: `200` with the results
|
|
@@ -760,18 +740,7 @@ export class HonuaOgcProcessSyncRun {
|
|
|
760
740
|
}
|
|
761
741
|
}
|
|
762
742
|
/** Error thrown when `IJobRun.results()` resolves a non-success terminal. */
|
|
763
|
-
export
|
|
764
|
-
status;
|
|
765
|
-
errorCode;
|
|
766
|
-
details;
|
|
767
|
-
constructor(message, status, errorCode, details) {
|
|
768
|
-
super("core.job-failed", message, { context: { status, errorCode } });
|
|
769
|
-
this.name = "HonuaJobFailedError";
|
|
770
|
-
this.status = status;
|
|
771
|
-
this.errorCode = errorCode;
|
|
772
|
-
this.details = details;
|
|
773
|
-
}
|
|
774
|
-
}
|
|
743
|
+
export { HonuaJobFailedError } from "./job-run-errors.js";
|
|
775
744
|
/**
|
|
776
745
|
* Honua-server emits problem-details JSON for DELETE /jobs/{id} 409s. The
|
|
777
746
|
* `title` distinguishes the benign terminal race ("Cannot dismiss
|
|
@@ -832,28 +801,6 @@ function clampPercent(value) {
|
|
|
832
801
|
return 100;
|
|
833
802
|
return value;
|
|
834
803
|
}
|
|
835
|
-
function makeJobFailedError(snapshot) {
|
|
836
|
-
const error = snapshot.error;
|
|
837
|
-
const message = error?.message ?? `Job ended in non-success terminal state: ${snapshot.status}`;
|
|
838
|
-
return new HonuaJobFailedError(message, snapshot.status, error?.code, error?.details);
|
|
839
|
-
}
|
|
840
|
-
function delay(ms, signal) {
|
|
841
|
-
return new Promise((resolve) => {
|
|
842
|
-
if (signal?.aborted) {
|
|
843
|
-
resolve();
|
|
844
|
-
return;
|
|
845
|
-
}
|
|
846
|
-
const onAbort = () => {
|
|
847
|
-
clearTimeout(timer);
|
|
848
|
-
resolve();
|
|
849
|
-
};
|
|
850
|
-
const timer = setTimeout(() => {
|
|
851
|
-
signal?.removeEventListener("abort", onAbort);
|
|
852
|
-
resolve();
|
|
853
|
-
}, ms);
|
|
854
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
855
|
-
});
|
|
856
|
-
}
|
|
857
804
|
export function createHonuaOgcProcesses(client) {
|
|
858
805
|
return new HonuaOgcProcesses({ client });
|
|
859
806
|
}
|
|
@@ -891,7 +838,7 @@ export async function getOgcProcess(transport, request) {
|
|
|
891
838
|
* against the async-only Honua facade, and reports which shape came back.
|
|
892
839
|
*/
|
|
893
840
|
export async function executeOgcProcess(transport, request) {
|
|
894
|
-
const headers =
|
|
841
|
+
const headers = executionHeaders(request);
|
|
895
842
|
const path = `${processesBase(request)}/processes/${encodeURIComponent(request.processId)}/execution`;
|
|
896
843
|
// `document` is the only response mode the SDK decodes: its body is the
|
|
897
844
|
// output-id-keyed map that `JobResult.outputs` is defined over. (honua-server
|
|
@@ -1008,16 +955,19 @@ export async function cancelOgcProcessJob(transport, request) {
|
|
|
1008
955
|
return transport.requestJson("DELETE", withParams(route, params), undefined, request.signal);
|
|
1009
956
|
}
|
|
1010
957
|
/**
|
|
1011
|
-
* Build the
|
|
1012
|
-
*
|
|
1013
|
-
*
|
|
1014
|
-
*
|
|
1015
|
-
* forces synchronous execution, so `"sync"` is expressed by *not* preferring
|
|
1016
|
-
* async plus the `sync-execute` capability assertion made before the post.
|
|
958
|
+
* Build the execution preference header defined by OGC API Processes 1.0.
|
|
959
|
+
* Requirement 25 selects synchronous execution by omitting `Prefer`; only an
|
|
960
|
+
* explicit asynchronous request sends the standard `respond-async` token.
|
|
961
|
+
* Clients remain prepared for either response shape.
|
|
1017
962
|
*/
|
|
1018
|
-
function
|
|
963
|
+
function executionHeaders(request) {
|
|
964
|
+
const headers = new Headers(mergeHeaders({ "Content-Type": "application/json", Accept: "application/json" }, request.headers));
|
|
965
|
+
// `mode` is the sole authority for this protocol semantic. A shared caller
|
|
966
|
+
// header bag must not silently turn `auto` / `sync` into an async request or
|
|
967
|
+
// smuggle a different preference into the explicit async mode.
|
|
968
|
+
headers.delete("Prefer");
|
|
1019
969
|
if (request.mode === "async") {
|
|
1020
|
-
|
|
970
|
+
headers.set("Prefer", "respond-async");
|
|
1021
971
|
}
|
|
1022
|
-
return
|
|
972
|
+
return headers;
|
|
1023
973
|
}
|
package/core/process-runner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { JobRunLifecycle } from "./job-run-lifecycle.js";
|
|
2
2
|
export class HonuaProcessRunner {
|
|
3
3
|
adapter;
|
|
4
4
|
constructor(adapter) {
|
|
@@ -89,115 +89,51 @@ class GeospatialGrpcProcessJobRun {
|
|
|
89
89
|
id;
|
|
90
90
|
type = "geospatial-grpc";
|
|
91
91
|
client;
|
|
92
|
-
|
|
93
|
-
currentProgress;
|
|
94
|
-
terminalSnapshot;
|
|
95
|
-
terminalPromise;
|
|
96
|
-
listeners = new Set();
|
|
92
|
+
lifecycle;
|
|
97
93
|
constructor(options) {
|
|
98
94
|
this.client = options.client;
|
|
99
95
|
this.id = readJobId(options.accepted);
|
|
100
|
-
this.
|
|
96
|
+
this.lifecycle = new JobRunLifecycle({
|
|
97
|
+
id: this.id,
|
|
98
|
+
initialStatus: geospatialGrpcJobStateToStatus(options.accepted.state),
|
|
99
|
+
pollIntervalMs: 1_000,
|
|
100
|
+
poll: async (signal) => this.translateJob(await this.client.getJob({ jobId: this.id, signal })),
|
|
101
|
+
});
|
|
101
102
|
}
|
|
102
103
|
get status() {
|
|
103
|
-
return this.
|
|
104
|
+
return this.lifecycle.status;
|
|
104
105
|
}
|
|
105
106
|
get progress() {
|
|
106
|
-
return this.
|
|
107
|
+
return this.lifecycle.progress;
|
|
107
108
|
}
|
|
108
109
|
async poll() {
|
|
109
|
-
|
|
110
|
-
return this.terminalSnapshot;
|
|
111
|
-
const response = await this.client.getJob({ jobId: this.id });
|
|
112
|
-
return this.handleJob(response);
|
|
110
|
+
return this.lifecycle.poll();
|
|
113
111
|
}
|
|
114
112
|
watch(listener) {
|
|
115
|
-
this.
|
|
116
|
-
return () => this.listeners.delete(listener);
|
|
113
|
+
return this.lifecycle.watch(listener);
|
|
117
114
|
}
|
|
118
115
|
async results(options = {}) {
|
|
119
|
-
|
|
120
|
-
// Reset on failure so an aborted / timed-out poll does not permanently
|
|
121
|
-
// poison subsequent results() calls.
|
|
122
|
-
this.terminalPromise = this.runUntilTerminal(options).catch((error) => {
|
|
123
|
-
this.terminalPromise = undefined;
|
|
124
|
-
throw error;
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
return this.terminalPromise;
|
|
116
|
+
return this.lifecycle.results(options);
|
|
128
117
|
}
|
|
129
118
|
async cancel() {
|
|
130
|
-
|
|
131
|
-
return this.currentStatus;
|
|
132
|
-
const response = await this.client.cancelJob({ jobId: this.id });
|
|
133
|
-
const snapshot = await this.handleJob(response);
|
|
134
|
-
return snapshot.status;
|
|
135
|
-
}
|
|
136
|
-
async runUntilTerminal(options = {}) {
|
|
137
|
-
const { signal } = options;
|
|
138
|
-
const baseIntervalMs = options.pollIntervalMs ?? 1_000;
|
|
139
|
-
const maxIntervalMs = options.maxPollIntervalMs ?? Math.max(baseIntervalMs, 30_000);
|
|
140
|
-
const startedAt = Date.now();
|
|
141
|
-
let attempts = 0;
|
|
142
|
-
while (!this.terminalSnapshot) {
|
|
143
|
-
if (signal?.aborted) {
|
|
144
|
-
throw new HonuaJobPollTimeoutError(`Job ${this.id} poll aborted`, "aborted", this.id, this.currentStatus);
|
|
145
|
-
}
|
|
146
|
-
if (options.maxAttempts !== undefined && attempts >= options.maxAttempts) {
|
|
147
|
-
throw new HonuaJobPollTimeoutError(`Job ${this.id} did not reach a terminal state within ${options.maxAttempts} poll attempt(s)`, "max-attempts", this.id, this.currentStatus);
|
|
148
|
-
}
|
|
149
|
-
let response;
|
|
150
|
-
try {
|
|
151
|
-
response = await this.client.getJob({ jobId: this.id, signal });
|
|
152
|
-
}
|
|
153
|
-
catch (error) {
|
|
154
|
-
if (signal?.aborted) {
|
|
155
|
-
throw new HonuaJobPollTimeoutError(`Job ${this.id} poll aborted`, "aborted", this.id, this.currentStatus);
|
|
156
|
-
}
|
|
157
|
-
throw error;
|
|
158
|
-
}
|
|
159
|
-
attempts += 1;
|
|
160
|
-
await this.handleJob(response);
|
|
161
|
-
if (this.terminalSnapshot)
|
|
162
|
-
break;
|
|
163
|
-
if (options.deadlineMs !== undefined && Date.now() - startedAt >= options.deadlineMs) {
|
|
164
|
-
throw new HonuaJobPollTimeoutError(`Job ${this.id} did not reach a terminal state within ${options.deadlineMs}ms`, "deadline", this.id, this.currentStatus);
|
|
165
|
-
}
|
|
166
|
-
// Capped exponential backoff instead of a fixed 1s interval.
|
|
167
|
-
const intervalMs = Math.min(maxIntervalMs, baseIntervalMs * 2 ** (attempts - 1));
|
|
168
|
-
if (intervalMs > 0) {
|
|
169
|
-
await wait(intervalMs, signal);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
if (this.terminalSnapshot.status === "successful" && this.terminalSnapshot.result) {
|
|
173
|
-
return this.terminalSnapshot.result;
|
|
174
|
-
}
|
|
175
|
-
throw new Error(this.terminalSnapshot.error?.message ?? `geospatial-grpc job ended in ${this.terminalSnapshot.status}`);
|
|
119
|
+
return this.lifecycle.cancel(async () => this.translateJob(await this.client.cancelJob({ jobId: this.id })));
|
|
176
120
|
}
|
|
177
|
-
async
|
|
121
|
+
async translateJob(response) {
|
|
178
122
|
const jobProgress = "progress" in response ? response.progress : undefined;
|
|
179
123
|
const status = geospatialGrpcJobStateToStatus(response.state ?? jobProgress?.state);
|
|
180
124
|
const progress = geospatialGrpcProgress(jobProgress);
|
|
181
|
-
this.currentStatus = status;
|
|
182
|
-
this.currentProgress = progress;
|
|
183
125
|
if (status === "successful") {
|
|
184
126
|
const result = await this.client.getJobResult({ jobId: this.id });
|
|
185
127
|
const snapshot = geospatialGrpcResultSnapshot(result, progress);
|
|
186
|
-
this.terminalSnapshot = snapshot;
|
|
187
|
-
this.currentStatus = snapshot.status;
|
|
188
|
-
this.notify(snapshot);
|
|
189
128
|
return snapshot;
|
|
190
129
|
}
|
|
191
130
|
if (status === "failed" || status === "dismissed") {
|
|
192
131
|
const result = await this.safeGetTerminalResult();
|
|
193
132
|
const error = result ? geospatialGrpcError(result) : undefined;
|
|
194
133
|
const snapshot = { status, progress, ...(error ? { error } : {}) };
|
|
195
|
-
this.terminalSnapshot = snapshot;
|
|
196
|
-
this.notify(snapshot);
|
|
197
134
|
return snapshot;
|
|
198
135
|
}
|
|
199
136
|
const snapshot = { status, progress };
|
|
200
|
-
this.notify(snapshot);
|
|
201
137
|
return snapshot;
|
|
202
138
|
}
|
|
203
139
|
async safeGetTerminalResult() {
|
|
@@ -208,10 +144,6 @@ class GeospatialGrpcProcessJobRun {
|
|
|
208
144
|
return undefined;
|
|
209
145
|
}
|
|
210
146
|
}
|
|
211
|
-
notify(snapshot) {
|
|
212
|
-
for (const listener of this.listeners)
|
|
213
|
-
listener(snapshot);
|
|
214
|
-
}
|
|
215
147
|
}
|
|
216
148
|
function requirePlan(request) {
|
|
217
149
|
if (request.plan === undefined)
|
|
@@ -297,20 +229,3 @@ function timestampToIso(value) {
|
|
|
297
229
|
return new Date(numeric).toISOString();
|
|
298
230
|
return String(value);
|
|
299
231
|
}
|
|
300
|
-
function wait(ms, signal) {
|
|
301
|
-
return new Promise((resolve) => {
|
|
302
|
-
if (signal?.aborted) {
|
|
303
|
-
resolve();
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
const onAbort = () => {
|
|
307
|
-
clearTimeout(timer);
|
|
308
|
-
resolve();
|
|
309
|
-
};
|
|
310
|
-
const timer = setTimeout(() => {
|
|
311
|
-
signal?.removeEventListener("abort", onAbort);
|
|
312
|
-
resolve();
|
|
313
|
-
}, ms);
|
|
314
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
315
|
-
});
|
|
316
|
-
}
|