@honua/react 0.1.1-beta.0 → 0.1.4-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/discovery.d.ts +34 -0
- package/contract/discovery.d.ts.map +1 -1
- package/contract/index.d.ts +7 -5
- package/contract/index.d.ts.map +1 -1
- package/contract/index.js +3 -2
- package/contract/pmtiles.d.ts +10 -1
- package/contract/pmtiles.d.ts.map +1 -1
- package/contract/pmtiles.js +10 -2
- package/contract/protocol-module.d.ts +84 -9
- package/contract/protocol-module.d.ts.map +1 -1
- package/contract/query-filter-fes.d.ts +18 -0
- package/contract/query-filter-fes.d.ts.map +1 -0
- package/contract/query-filter-fes.js +122 -0
- package/contract/query-filter.d.ts +300 -0
- package/contract/query-filter.d.ts.map +1 -0
- package/contract/query-filter.js +883 -0
- package/contract/schema.d.ts +12 -0
- package/contract/schema.d.ts.map +1 -1
- package/contract/schema.js +34 -0
- package/contract/source-capability-support.d.ts.map +1 -1
- package/contract/source-capability-support.js +15 -3
- package/contract/source.d.ts +45 -25
- package/contract/source.d.ts.map +1 -1
- package/contract/source.js +719 -479
- package/contract/types.d.ts +87 -18
- package/contract/types.d.ts.map +1 -1
- package/contract/widget-source.d.ts.map +1 -1
- package/core/client.d.ts +45 -21
- package/core/client.d.ts.map +1 -1
- package/core/client.js +224 -46
- package/core/error-classifications.d.ts +6 -0
- package/core/error-classifications.d.ts.map +1 -1
- package/core/error-classifications.js +6 -0
- package/core/error-code-registry.d.ts +6 -0
- package/core/error-code-registry.d.ts.map +1 -1
- package/core/error-code-registry.js +6 -0
- package/core/error-envelope.js +2 -0
- package/core/errors.d.ts +1 -1
- package/core/errors.js +1 -1
- package/core/geoparquet-sql.d.ts +68 -4
- package/core/geoparquet-sql.d.ts.map +1 -1
- package/core/geoparquet-sql.js +325 -8
- package/core/odata-write-codec.d.ts.map +1 -1
- package/core/odata-write-codec.js +0 -1
- package/core/odata.d.ts +6 -0
- package/core/odata.d.ts.map +1 -1
- package/core/odata.js +6 -1
- package/core/ogc-processes.d.ts +182 -25
- package/core/ogc-processes.d.ts.map +1 -1
- package/core/ogc-processes.js +602 -30
- package/core/process-runner.d.ts +8 -1
- package/core/process-runner.d.ts.map +1 -1
- package/core/process-runner.js +1 -0
- package/core/protocol-transport.d.ts +2 -0
- package/core/protocol-transport.d.ts.map +1 -1
- package/core/stac.d.ts +31 -0
- package/core/stac.d.ts.map +1 -1
- package/core/stac.js +136 -19
- package/core/surfaces.d.ts.map +1 -1
- package/core/surfaces.js +273 -68
- package/core/types.d.ts +123 -9
- package/core/types.d.ts.map +1 -1
- package/core/wfs-axis-order.d.ts +18 -0
- package/core/wfs-axis-order.d.ts.map +1 -0
- package/core/wfs-axis-order.js +108 -0
- package/core/wfs-capabilities.d.ts +65 -24
- package/core/wfs-capabilities.d.ts.map +1 -1
- package/core/wfs-capabilities.js +237 -27
- package/core/wfs-filter.d.ts +26 -3
- package/core/wfs-filter.d.ts.map +1 -1
- package/core/wfs-filter.js +48 -53
- package/core/wfs-protocol-error.d.ts +12 -0
- package/core/wfs-protocol-error.d.ts.map +1 -0
- package/core/wfs-protocol-error.js +13 -0
- package/core/wfs.d.ts +53 -1
- package/core/wfs.d.ts.map +1 -1
- package/core/wfs.js +251 -29
- package/core/wms-feature-info.d.ts +92 -0
- package/core/wms-feature-info.d.ts.map +1 -0
- package/core/wms-feature-info.js +285 -0
- package/core/wms.d.ts +9 -0
- package/core/wms.d.ts.map +1 -1
- package/core/wms.js +17 -5
- package/esri-compat/event-bus.d.ts +20 -0
- package/esri-compat/event-bus.d.ts.map +1 -1
- package/esri-compat/feature-filter.d.ts +46 -1
- package/esri-compat/feature-filter.d.ts.map +1 -1
- package/esri-compat/feature-filter.js +49 -0
- package/esri-compat/geojson-layer.d.ts +4 -2
- package/esri-compat/geojson-layer.d.ts.map +1 -1
- package/esri-compat/geojson-layer.js +4 -2
- package/esri-compat/identity-manager.js +2 -2
- package/esri-compat/imagery-layer.d.ts +2 -1
- package/esri-compat/imagery-layer.d.ts.map +1 -1
- package/esri-compat/imagery-layer.js +2 -1
- package/esri-compat/layer-list.d.ts +8 -1
- package/esri-compat/layer-list.d.ts.map +1 -1
- package/esri-compat/layer-list.js +11 -4
- package/esri-compat/legend.d.ts.map +1 -1
- package/esri-compat/legend.js +1 -1
- package/esri-compat/locator.d.ts +297 -0
- package/esri-compat/locator.d.ts.map +1 -0
- package/esri-compat/locator.js +579 -0
- package/esri-compat/map-image-layer.d.ts.map +1 -1
- package/esri-compat/map-view.d.ts.map +1 -1
- package/esri-compat/map-view.js +1 -1
- package/esri-compat/scene-view.d.ts.map +1 -1
- package/esri-compat/time-slider.d.ts +29 -0
- package/esri-compat/time-slider.d.ts.map +1 -1
- package/esri-compat/time-slider.js +117 -0
- package/esri-compat/wfs-layer.d.ts +2 -1
- package/esri-compat/wfs-layer.d.ts.map +1 -1
- package/esri-compat/wfs-layer.js +2 -1
- package/esri-compat/widget-host.d.ts +42 -6
- package/esri-compat/widget-host.d.ts.map +1 -1
- package/esri-compat/widget-host.js +99 -10
- package/generated-app/projection.d.ts.map +1 -1
- package/generated-app/runtime.d.ts.map +1 -1
- package/map/data-to-map-bridge.d.ts +1 -1
- package/map/data-to-map-bridge.d.ts.map +1 -1
- package/map/data-to-map-bridge.js +1 -2
- package/map/honua-map.d.ts.map +1 -1
- package/map/honua-map.js +1 -1
- package/map/index.d.ts +2 -0
- package/map/index.d.ts.map +1 -1
- package/map/index.js +2 -0
- package/map/realtime-reconciliation-adapter.d.ts +98 -0
- package/map/realtime-reconciliation-adapter.d.ts.map +1 -0
- package/map/realtime-reconciliation-adapter.js +84 -0
- package/map/temporal-playback.d.ts +25 -0
- package/map/temporal-playback.d.ts.map +1 -1
- package/map/temporal-playback.js +29 -1
- package/package.json +4 -3
- package/query-planner/canonical-filter.d.ts +30 -0
- package/query-planner/canonical-filter.d.ts.map +1 -0
- package/query-planner/canonical-filter.js +30 -0
- package/query-planner/columnar-plan.d.ts +63 -0
- package/query-planner/columnar-plan.d.ts.map +1 -0
- package/query-planner/columnar-plan.js +94 -0
- package/query-planner/diagnostics.d.ts +3 -3
- package/query-planner/diagnostics.d.ts.map +1 -1
- package/query-planner/diagnostics.js +17 -4
- package/query-planner/duckdb.d.ts.map +1 -1
- package/query-planner/duckdb.js +16 -2
- package/query-planner/execution-context.d.ts.map +1 -1
- package/query-planner/execution-context.js +3 -1
- package/query-planner/executor.d.ts.map +1 -1
- package/query-planner/executor.js +7 -1
- package/query-planner/geoservices.d.ts.map +1 -1
- package/query-planner/geoservices.js +24 -5
- package/query-planner/grpc.d.ts.map +1 -1
- package/query-planner/grpc.js +29 -5
- package/query-planner/index.d.ts +20 -5
- package/query-planner/index.d.ts.map +1 -1
- package/query-planner/index.js +16 -3
- package/query-planner/ir.d.ts +6 -1
- package/query-planner/ir.d.ts.map +1 -1
- package/query-planner/ir.js +63 -1
- package/query-planner/odata-protocol-module.d.ts +54 -0
- package/query-planner/odata-protocol-module.d.ts.map +1 -0
- package/query-planner/odata-protocol-module.js +199 -0
- package/query-planner/odata-v1.d.ts +23 -0
- package/query-planner/odata-v1.d.ts.map +1 -0
- package/query-planner/odata-v1.js +202 -0
- package/query-planner/odata.d.ts +0 -3
- package/query-planner/odata.d.ts.map +1 -1
- package/query-planner/odata.js +0 -143
- package/query-planner/ogc-features.d.ts.map +1 -1
- package/query-planner/ogc-features.js +23 -1
- package/query-planner/planner.d.ts +2 -2
- package/query-planner/planner.d.ts.map +1 -1
- package/query-planner/planner.js +146 -27
- package/query-planner/representation.d.ts +59 -0
- package/query-planner/representation.d.ts.map +1 -0
- package/query-planner/representation.js +140 -0
- package/query-planner/types.d.ts +153 -3
- package/query-planner/types.d.ts.map +1 -1
- package/query-planner/wfs-protocol-module.d.ts +57 -0
- package/query-planner/wfs-protocol-module.d.ts.map +1 -0
- package/query-planner/wfs-protocol-module.js +731 -0
- package/query-planner/wfs-v1.d.ts +41 -0
- package/query-planner/wfs-v1.d.ts.map +1 -0
- package/query-planner/wfs-v1.js +366 -0
- package/query-planner/wfs.d.ts +1 -3
- package/query-planner/wfs.d.ts.map +1 -1
- package/query-planner/wfs.js +1 -70
- package/react/honua-map.d.ts +3 -1
- package/react/honua-map.d.ts.map +1 -1
- package/react/honua-map.js +11 -1
- package/react/hooks.d.ts +1 -1
- package/react/hooks.js +1 -1
- package/react/selection.d.ts.map +1 -1
- package/react/selection.js +0 -0
- package/react/use-mounted-source.d.ts +1 -1
- package/react/use-mounted-source.js +1 -1
- package/realtime/checkpoint-store.d.ts +206 -0
- package/realtime/checkpoint-store.d.ts.map +1 -0
- package/realtime/checkpoint-store.js +504 -0
- package/realtime/contract.d.ts +142 -0
- package/realtime/contract.d.ts.map +1 -0
- package/realtime/contract.js +154 -0
- package/realtime/exploration.d.ts +16 -0
- package/realtime/exploration.d.ts.map +1 -0
- package/realtime/exploration.js +32 -0
- package/realtime/honua-server.d.ts +131 -0
- package/realtime/honua-server.d.ts.map +1 -0
- package/realtime/honua-server.js +302 -0
- package/realtime/index.d.ts +39 -0
- package/realtime/index.d.ts.map +1 -0
- package/realtime/index.js +26 -0
- package/realtime/odata-delta.d.ts +114 -0
- package/realtime/odata-delta.d.ts.map +1 -0
- package/realtime/odata-delta.js +483 -0
- package/realtime/projections.d.ts +30 -0
- package/realtime/projections.d.ts.map +1 -0
- package/realtime/projections.js +51 -0
- package/realtime/reconciliation.d.ts +245 -0
- package/realtime/reconciliation.d.ts.map +1 -0
- package/realtime/reconciliation.js +377 -0
- package/realtime/reducer.d.ts +14 -0
- package/realtime/reducer.d.ts.map +1 -0
- package/realtime/reducer.js +351 -0
- package/realtime/resumable-transport.d.ts +111 -0
- package/realtime/resumable-transport.d.ts.map +1 -0
- package/realtime/resumable-transport.js +406 -0
- package/realtime/resumable.d.ts +104 -0
- package/realtime/resumable.d.ts.map +1 -0
- package/realtime/resumable.js +632 -0
- package/realtime/sse.d.ts +23 -0
- package/realtime/sse.d.ts.map +1 -0
- package/realtime/sse.js +147 -0
- package/realtime/store.d.ts +18 -0
- package/realtime/store.d.ts.map +1 -0
- package/realtime/store.js +63 -0
- package/realtime/types.d.ts +190 -0
- package/realtime/types.d.ts.map +1 -0
- package/realtime/types.js +10 -0
- package/realtime/websocket.d.ts +60 -0
- package/realtime/websocket.d.ts.map +1 -0
- package/realtime/websocket.js +157 -0
- package/realtime/wire.d.ts +28 -0
- package/realtime/wire.d.ts.map +1 -0
- package/realtime/wire.js +73 -0
- package/runtime/index.d.ts +6 -2
- package/runtime/index.d.ts.map +1 -1
- package/runtime/index.js +3 -1
- package/runtime/load-package.d.ts +24 -0
- package/runtime/load-package.d.ts.map +1 -1
- package/runtime/load-package.js +45 -1
- package/runtime/maplibre-renderer.d.ts +11 -0
- package/runtime/maplibre-renderer.d.ts.map +1 -1
- package/runtime/maplibre-renderer.js +32 -7
- package/runtime/offline-region-protocol.d.ts +73 -0
- package/runtime/offline-region-protocol.d.ts.map +1 -0
- package/runtime/offline-region-protocol.js +63 -0
- package/runtime/offline-region-style.d.ts +78 -0
- package/runtime/offline-region-style.d.ts.map +1 -0
- package/runtime/offline-region-style.js +147 -0
- package/runtime/pmtiles-protocol.d.ts +7 -8
- package/runtime/pmtiles-protocol.d.ts.map +1 -1
- package/runtime/pmtiles-protocol.js +19 -66
- package/runtime/protocol-registry.d.ts +55 -0
- package/runtime/protocol-registry.d.ts.map +1 -0
- package/runtime/protocol-registry.js +103 -0
- package/runtime/runtime.d.ts +2 -0
- package/runtime/runtime.d.ts.map +1 -1
- package/runtime/runtime.js +6 -1
- package/studio/index.d.ts +24 -7
- package/studio/index.d.ts.map +1 -1
- package/studio/index.js +21 -7
- package/studio/lifecycle-client.d.ts +164 -0
- package/studio/lifecycle-client.d.ts.map +1 -0
- package/studio/lifecycle-client.js +299 -0
- package/studio/lifecycle-errors.d.ts +91 -0
- package/studio/lifecycle-errors.d.ts.map +1 -0
- package/studio/lifecycle-errors.js +77 -0
- package/studio/lifecycle-types.d.ts +321 -0
- package/studio/lifecycle-types.d.ts.map +1 -0
- package/studio/lifecycle-types.js +22 -0
- package/webmap/convert-label.js +0 -1
- package/webmap/convert-symbol.d.ts.map +1 -1
package/core/ogc-processes.js
CHANGED
|
@@ -1,17 +1,92 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OGC API Processes surface. Process discovery, execution, and async
|
|
3
|
-
* job tracking
|
|
3
|
+
* job tracking against any conformant Part 1 (Core) server — a raw
|
|
4
|
+
* third-party deployment discovered by `discoverOgcProcesses()` just as much
|
|
5
|
+
* as the Honua facade. Per the ticket constraint, executions return an
|
|
4
6
|
* `IJobRun` (the canonical async-operation surface) rather than an
|
|
5
|
-
* OGC-specific job type
|
|
7
|
+
* OGC-specific job type; a synchronous execution returns an already-terminal
|
|
8
|
+
* `IJobRun` so callers read results through one surface either way.
|
|
9
|
+
*
|
|
10
|
+
* Three properties keep the standalone lane honest:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Nothing is assumed about the layout.** Routes template off the
|
|
13
|
+
* discovered service root, and the job lifecycle prefers the server's own
|
|
14
|
+
* `Location` header and `links[]` over the Core path template.
|
|
15
|
+
* 2. **Capability gaps fail closed.** When the server's conformance
|
|
16
|
+
* declaration (or the process's `jobControlOptions`) is known and does not
|
|
17
|
+
* declare what the caller asked for, the call throws
|
|
18
|
+
* `HonuaCapabilityNotSupportedError` naming the missing construct rather
|
|
19
|
+
* than posting and hoping.
|
|
20
|
+
* 3. **Polling is bounded.** `results()` always runs under a budget — the
|
|
21
|
+
* caller's, the handle's, or a default deadline — and honors an
|
|
22
|
+
* `AbortSignal`.
|
|
6
23
|
*
|
|
7
24
|
* @module
|
|
8
25
|
*/
|
|
9
26
|
import { HonuaJobPollTimeoutError, isJobTerminal } from "../contract/jobs.js";
|
|
10
27
|
import { HonuaSdkError } from "./error-envelope.js";
|
|
28
|
+
import { HonuaCapabilityNotSupportedError } from "./errors.js";
|
|
29
|
+
import { hasOgcConformanceClass } from "./ogc-conformance.js";
|
|
11
30
|
import { createOgcMetadataParams, mergeHeaders } from "./wire-shared.js";
|
|
12
31
|
const DEFAULT_POLL_INTERVAL_MS = 1_000;
|
|
32
|
+
/**
|
|
33
|
+
* Wall-clock ceiling applied to `results()` when neither the caller nor the
|
|
34
|
+
* handle set `deadlineMs` / `maxAttempts`. NFR-001: a status endpoint that
|
|
35
|
+
* never reaches a terminal state must not poll forever. Callers who genuinely
|
|
36
|
+
* want a longer wait pass their own `deadlineMs`.
|
|
37
|
+
*/
|
|
38
|
+
const DEFAULT_POLL_DEADLINE_MS = 600_000;
|
|
13
39
|
/** Honua facade path prefix for OGC API Processes. */
|
|
14
40
|
const PROCESSES_FACADE_BASE = "/ogc/processes";
|
|
41
|
+
/**
|
|
42
|
+
* OGC API — Processes Part 1 (1.0) conformance classes the SDK gates on, and
|
|
43
|
+
* the Core link relations the job lifecycle follows. These stay module-private
|
|
44
|
+
* per the `ogc-conformance` rule that conformance URIs are never top-level SDK
|
|
45
|
+
* types; they surface only as diagnostic strings inside capability errors, so a
|
|
46
|
+
* refusal can name the exact construct the server did not declare.
|
|
47
|
+
*/
|
|
48
|
+
const PROCESSES_CONFORMANCE = {
|
|
49
|
+
core: "http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/core",
|
|
50
|
+
dismiss: "http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/dismiss",
|
|
51
|
+
};
|
|
52
|
+
/** Substring matchers tolerant of OGC re-issuing the class URIs under a new part number. */
|
|
53
|
+
const PROCESSES_CONFORMANCE_MATCH = {
|
|
54
|
+
core: "processes-1/1.0/conf/core",
|
|
55
|
+
dismiss: "processes-1/1.0/conf/dismiss",
|
|
56
|
+
};
|
|
57
|
+
const PROCESSES_LINK_REL = {
|
|
58
|
+
results: "http://www.opengis.net/def/rel/ogc/1.0/results",
|
|
59
|
+
status: "self",
|
|
60
|
+
};
|
|
61
|
+
/** Job-control options a process declares in its description (Core §7.9). */
|
|
62
|
+
const JOB_CONTROL = {
|
|
63
|
+
sync: "sync-execute",
|
|
64
|
+
async: "async-execute",
|
|
65
|
+
dismiss: "dismiss",
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Fail-closed refusal naming the construct the server did not declare. The
|
|
69
|
+
* conformance-class URI / link relation rides in the error context so telemetry
|
|
70
|
+
* keeps it after redaction.
|
|
71
|
+
*
|
|
72
|
+
* The context key for a class URI is `missingClass`, not `conformanceClass`:
|
|
73
|
+
* the error-envelope redactor drops any key matching `form` (for form bodies),
|
|
74
|
+
* which "con**form**anceClass" hits — the value would ship as `[REDACTED]` and
|
|
75
|
+
* the refusal would name nothing.
|
|
76
|
+
*/
|
|
77
|
+
function capabilityRefusal(capability, sourceId, context) {
|
|
78
|
+
return new HonuaCapabilityNotSupportedError(capability, "ogc-processes", sourceId, { context });
|
|
79
|
+
}
|
|
80
|
+
/** `true` when the declaration list contains `option` (case-insensitive). */
|
|
81
|
+
function declares(options, option) {
|
|
82
|
+
if (!options)
|
|
83
|
+
return false;
|
|
84
|
+
for (const entry of options) {
|
|
85
|
+
if (typeof entry === "string" && entry.toLowerCase() === option)
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
15
90
|
/**
|
|
16
91
|
* Resolve the OGC API Processes path prefix: the caller-supplied raw `basePath`
|
|
17
92
|
* (a `discoverOgcProcesses()`-discovered third-party service root) or the Honua
|
|
@@ -29,56 +104,293 @@ function processesBase(request) {
|
|
|
29
104
|
end--;
|
|
30
105
|
return base.slice(0, end);
|
|
31
106
|
}
|
|
32
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* Cache-key discriminator so a discovered root never collides with the facade.
|
|
109
|
+
*
|
|
110
|
+
* The base path is percent-encoded because cache keys join their components
|
|
111
|
+
* with `:` and a service root may legally contain one. Without encoding,
|
|
112
|
+
* `{ basePath: "/a", processId: "b:c" }` and `{ basePath: "/a:b", processId: "c" }`
|
|
113
|
+
* would produce the same key for two different request URLs, so a cached
|
|
114
|
+
* `describe()` could answer with the wrong process's metadata.
|
|
115
|
+
*/
|
|
33
116
|
function processesBaseKey(request) {
|
|
34
117
|
const base = processesBase(request);
|
|
35
|
-
return base === PROCESSES_FACADE_BASE ? "" : `${base}:`;
|
|
118
|
+
return base === PROCESSES_FACADE_BASE ? "" : `${encodeURIComponent(base)}:`;
|
|
119
|
+
}
|
|
120
|
+
function isAbsoluteHttpUrl(value) {
|
|
121
|
+
return /^https?:\/\//i.test(value);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Absolute URL of a request the SDK issues against `baseUrl`. Link hrefs in the
|
|
125
|
+
* response are relative to the document that carried them, so this is the base
|
|
126
|
+
* a `links[]` entry resolves against.
|
|
127
|
+
*/
|
|
128
|
+
function requestUrlFor(baseUrl, path) {
|
|
129
|
+
if (!isAbsoluteHttpUrl(baseUrl))
|
|
130
|
+
return undefined;
|
|
131
|
+
try {
|
|
132
|
+
return new URL(`${baseUrl}${path}`).toString();
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Reduce a server-advertised href to a same-origin request path.
|
|
140
|
+
*
|
|
141
|
+
* The SDK attaches credentials by header, so a link that leaves the configured
|
|
142
|
+
* origin is dropped rather than followed — the caller falls back to the Core
|
|
143
|
+
* route template (or, under `"strict"`, gets a fail-closed refusal). Relative
|
|
144
|
+
* hrefs resolve against the document that carried them.
|
|
145
|
+
*/
|
|
146
|
+
function advertisedRoutePath(href, documentUrl, baseUrl) {
|
|
147
|
+
if (typeof href !== "string" || href.length === 0)
|
|
148
|
+
return undefined;
|
|
149
|
+
if (!isAbsoluteHttpUrl(baseUrl))
|
|
150
|
+
return undefined;
|
|
151
|
+
let resolved;
|
|
152
|
+
try {
|
|
153
|
+
resolved = new URL(href, documentUrl ?? `${baseUrl}/`);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
let origin;
|
|
159
|
+
try {
|
|
160
|
+
origin = new URL(baseUrl).origin;
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
if (resolved.origin !== origin)
|
|
166
|
+
return undefined;
|
|
167
|
+
return `${resolved.pathname}${resolved.search}`;
|
|
168
|
+
}
|
|
169
|
+
/** Core-mandated job route (§7.12) under a service root. */
|
|
170
|
+
function templatedJobPath(basePath, jobId) {
|
|
171
|
+
return `${processesBase({ ...(basePath !== undefined ? { basePath } : {}) })}/jobs/${encodeURIComponent(jobId)}`;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Append metadata params to a route that may already carry a query string —
|
|
175
|
+
* an advertised job link often does. The server's own query is preserved
|
|
176
|
+
* verbatim (never re-encoded) and a param it already declares is never
|
|
177
|
+
* duplicated, so following a link cannot corrupt the URL the server published.
|
|
178
|
+
*/
|
|
179
|
+
function withParams(path, params) {
|
|
180
|
+
const queryIndex = path.indexOf("?");
|
|
181
|
+
if (queryIndex < 0) {
|
|
182
|
+
const query = params.toString();
|
|
183
|
+
return query.length === 0 ? path : `${path}?${query}`;
|
|
184
|
+
}
|
|
185
|
+
const existing = new URLSearchParams(path.slice(queryIndex + 1));
|
|
186
|
+
const additions = new URLSearchParams();
|
|
187
|
+
for (const [key, value] of params) {
|
|
188
|
+
if (!existing.has(key))
|
|
189
|
+
additions.append(key, value);
|
|
190
|
+
}
|
|
191
|
+
const query = additions.toString();
|
|
192
|
+
return query.length === 0 ? path : `${path}&${query}`;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* First same-origin path advertised under one of `rels`, if any. A relation
|
|
196
|
+
* matches on the full URI or on its last path segment, the same tolerance
|
|
197
|
+
* `findOgcLink` applies — servers publish both
|
|
198
|
+
* `http://www.opengis.net/def/rel/ogc/1.0/results` and a bare `results`.
|
|
199
|
+
*/
|
|
200
|
+
function linkPath(links, documentUrl, baseUrl, ...rels) {
|
|
201
|
+
if (!links)
|
|
202
|
+
return undefined;
|
|
203
|
+
const wanted = new Set();
|
|
204
|
+
for (const rel of rels) {
|
|
205
|
+
const lower = rel.toLowerCase();
|
|
206
|
+
wanted.add(lower);
|
|
207
|
+
wanted.add(lower.slice(lower.lastIndexOf("/") + 1));
|
|
208
|
+
}
|
|
209
|
+
for (const link of links) {
|
|
210
|
+
const rel = (link.rel ?? "").toLowerCase();
|
|
211
|
+
const tail = rel.slice(rel.lastIndexOf("/") + 1);
|
|
212
|
+
if (!wanted.has(rel) && !(tail && wanted.has(tail)))
|
|
213
|
+
continue;
|
|
214
|
+
const path = advertisedRoutePath(link.href, documentUrl, baseUrl);
|
|
215
|
+
if (path)
|
|
216
|
+
return path;
|
|
217
|
+
}
|
|
218
|
+
return undefined;
|
|
36
219
|
}
|
|
37
220
|
/** Top-level OGC API Processes handle. */
|
|
38
221
|
export class HonuaOgcProcesses {
|
|
39
222
|
client;
|
|
223
|
+
basePath;
|
|
224
|
+
capabilityPolicy;
|
|
225
|
+
pollBudget;
|
|
226
|
+
declaredConformance;
|
|
227
|
+
conformancePromise;
|
|
228
|
+
/**
|
|
229
|
+
* `jobControlOptions` learned from `describe()` calls made through this
|
|
230
|
+
* handle. Callers that follow the natural discover → describe → execute flow
|
|
231
|
+
* therefore get process-level gating for free, with no extra round trip.
|
|
232
|
+
*/
|
|
233
|
+
jobControlByProcess = new Map();
|
|
40
234
|
constructor(options) {
|
|
41
235
|
this.client = options.client;
|
|
236
|
+
this.basePath = options.basePath;
|
|
237
|
+
this.capabilityPolicy = options.capabilityPolicy ?? "advertised";
|
|
238
|
+
this.pollBudget = options.pollBudget;
|
|
239
|
+
this.declaredConformance = options.conformance;
|
|
240
|
+
}
|
|
241
|
+
withBase(request) {
|
|
242
|
+
return this.basePath !== undefined ? { ...request, basePath: this.basePath } : request;
|
|
42
243
|
}
|
|
43
244
|
async landing(request = {}) {
|
|
44
|
-
return this.client.getOgcProcessesLanding(request);
|
|
245
|
+
return this.client.getOgcProcessesLanding(this.withBase(request));
|
|
45
246
|
}
|
|
46
247
|
async conformance(request = {}) {
|
|
47
|
-
|
|
248
|
+
const response = await this.client.getOgcProcessesConformance(this.withBase(request));
|
|
249
|
+
if (Array.isArray(response?.conformsTo))
|
|
250
|
+
this.declaredConformance = response;
|
|
251
|
+
return response;
|
|
48
252
|
}
|
|
49
253
|
async list(request = {}) {
|
|
50
|
-
|
|
254
|
+
const response = await this.client.listOgcProcesses(this.withBase(request));
|
|
255
|
+
for (const process of response?.processes ?? []) {
|
|
256
|
+
if (process?.id && process.jobControlOptions) {
|
|
257
|
+
this.jobControlByProcess.set(process.id, [...process.jobControlOptions]);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return response;
|
|
51
261
|
}
|
|
52
262
|
async describe(processId, request = {}) {
|
|
53
|
-
|
|
263
|
+
const description = await this.client.getOgcProcess(this.withBase({ ...request, processId }));
|
|
264
|
+
if (description?.jobControlOptions) {
|
|
265
|
+
this.jobControlByProcess.set(processId, [...description.jobControlOptions]);
|
|
266
|
+
}
|
|
267
|
+
return description;
|
|
54
268
|
}
|
|
55
269
|
/**
|
|
56
|
-
* Submit a process for execution
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
270
|
+
* Submit a process for execution and return an `IJobRun` — always, so the
|
|
271
|
+
* caller reads results through one surface whichever Core response shape the
|
|
272
|
+
* server chose:
|
|
273
|
+
*
|
|
274
|
+
* - asynchronous (`Prefer: respond-async`, or the server's own default) →
|
|
275
|
+
* a pollable run bound to the advertised job routes.
|
|
276
|
+
* - synchronous (`200` with the results document) → an already-terminal run
|
|
277
|
+
* whose `results()` resolves immediately with zero further requests.
|
|
278
|
+
*
|
|
279
|
+
* Before anything is posted, `mode` is gated against what the server has
|
|
280
|
+
* declared: the Core conformance class when a conformance declaration is in
|
|
281
|
+
* hand, and the process's `jobControlOptions` when the description is (from
|
|
282
|
+
* `request.jobControlOptions`, an earlier `describe()`/`list()` on this
|
|
283
|
+
* handle, or — under `capabilityPolicy: "strict"` — a cached `describe()`
|
|
284
|
+
* performed here). An undeclared mode fails closed with
|
|
285
|
+
* `HonuaCapabilityNotSupportedError`.
|
|
60
286
|
*/
|
|
61
287
|
async execute(request) {
|
|
62
|
-
const
|
|
288
|
+
const executeRequest = this.withBase(request);
|
|
289
|
+
const conformance = await this.assertExecutionDeclared(request);
|
|
290
|
+
const accepted = await this.client.executeOgcProcess(executeRequest);
|
|
291
|
+
const processId = accepted.processID ?? request.processId;
|
|
292
|
+
const jobControlOptions = request.jobControlOptions ?? this.jobControlByProcess.get(request.processId);
|
|
293
|
+
if (accepted.synchronous === true) {
|
|
294
|
+
// Core assigns no job identifier to a synchronous execution; the results
|
|
295
|
+
// document is already in hand, so the run starts terminal.
|
|
296
|
+
return new HonuaOgcProcessSyncRun(processId, (accepted.results ?? {}));
|
|
297
|
+
}
|
|
63
298
|
return new HonuaOgcProcessJobRun({
|
|
64
299
|
client: this.client,
|
|
65
300
|
jobId: accepted.jobID,
|
|
66
|
-
processId
|
|
301
|
+
processId,
|
|
67
302
|
initialStatus: accepted.statusInfo ?? {
|
|
68
303
|
jobID: accepted.jobID,
|
|
69
|
-
processID:
|
|
304
|
+
processID: processId,
|
|
70
305
|
status: accepted.status,
|
|
71
306
|
},
|
|
307
|
+
// The job routes live under the same root the execution was posted to;
|
|
308
|
+
// a job created against a discovered root must never poll the facade.
|
|
309
|
+
...(executeRequest.basePath !== undefined ? { basePath: executeRequest.basePath } : {}),
|
|
310
|
+
...(accepted.statusPath !== undefined ? { statusPath: accepted.statusPath } : {}),
|
|
311
|
+
...(this.pollBudget !== undefined ? { pollBudget: this.pollBudget } : {}),
|
|
312
|
+
...(conformance !== undefined ? { conformance } : {}),
|
|
313
|
+
...(jobControlOptions !== undefined ? { jobControlOptions } : {}),
|
|
314
|
+
capabilityPolicy: this.capabilityPolicy,
|
|
72
315
|
});
|
|
73
316
|
}
|
|
74
317
|
/** Adopt an existing job by id (useful when reconnecting after navigation). */
|
|
75
318
|
job(jobId, options = {}) {
|
|
319
|
+
const { basePath } = this.withBase(options);
|
|
320
|
+
const jobControlOptions = options.processId ? this.jobControlByProcess.get(options.processId) : undefined;
|
|
76
321
|
return new HonuaOgcProcessJobRun({
|
|
77
322
|
client: this.client,
|
|
78
323
|
jobId,
|
|
79
324
|
processId: options.processId,
|
|
325
|
+
...(basePath !== undefined ? { basePath } : {}),
|
|
326
|
+
...(options.statusPath !== undefined ? { statusPath: options.statusPath } : {}),
|
|
327
|
+
...(options.resultsPath !== undefined ? { resultsPath: options.resultsPath } : {}),
|
|
328
|
+
...(this.pollBudget !== undefined ? { pollBudget: this.pollBudget } : {}),
|
|
329
|
+
...(this.declaredConformance !== undefined ? { conformance: this.declaredConformance } : {}),
|
|
330
|
+
...(jobControlOptions !== undefined ? { jobControlOptions } : {}),
|
|
331
|
+
capabilityPolicy: this.capabilityPolicy,
|
|
80
332
|
});
|
|
81
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* Resolve the conformance declaration. Under `"advertised"` only what the
|
|
336
|
+
* caller already supplied (or a `conformance()` call made through this
|
|
337
|
+
* handle) counts; under `"strict"` the declaration is fetched once through
|
|
338
|
+
* the metadata cache so a refusal can be grounded in the server's own words.
|
|
339
|
+
*/
|
|
340
|
+
async resolveConformance(signal) {
|
|
341
|
+
if (this.declaredConformance)
|
|
342
|
+
return this.declaredConformance;
|
|
343
|
+
if (this.capabilityPolicy !== "strict")
|
|
344
|
+
return undefined;
|
|
345
|
+
if (!this.conformancePromise) {
|
|
346
|
+
const request = signal ? { signal } : {};
|
|
347
|
+
this.conformancePromise = this.client
|
|
348
|
+
.getOgcProcessesConformance(this.withBase(request))
|
|
349
|
+
.then((response) => {
|
|
350
|
+
this.declaredConformance = response;
|
|
351
|
+
return response;
|
|
352
|
+
})
|
|
353
|
+
.catch((error) => {
|
|
354
|
+
this.conformancePromise = undefined;
|
|
355
|
+
throw error;
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return this.conformancePromise;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Fail closed when the server has not declared what the caller asked for.
|
|
362
|
+
* Returns the conformance declaration (when one is in hand) so the created
|
|
363
|
+
* job run can reuse it to gate `cancel()`.
|
|
364
|
+
*/
|
|
365
|
+
async assertExecutionDeclared(request) {
|
|
366
|
+
const conformance = await this.resolveConformance(request.signal);
|
|
367
|
+
if (conformance?.conformsTo && !hasOgcConformanceClass(conformance, PROCESSES_CONFORMANCE_MATCH.core)) {
|
|
368
|
+
throw capabilityRefusal("processes.execute", request.processId, {
|
|
369
|
+
missingClass: PROCESSES_CONFORMANCE.core,
|
|
370
|
+
construct: "execute",
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
const mode = request.mode ?? "auto";
|
|
374
|
+
// `auto` asserts nothing about sync vs async: the server picks, and both
|
|
375
|
+
// shapes land on the same `IJobRun`. Only an explicit preference is gated.
|
|
376
|
+
if (mode === "auto")
|
|
377
|
+
return conformance;
|
|
378
|
+
let jobControlOptions = request.jobControlOptions ?? this.jobControlByProcess.get(request.processId);
|
|
379
|
+
if (!jobControlOptions && this.capabilityPolicy === "strict") {
|
|
380
|
+
const description = await this.describe(request.processId, request.signal ? { signal: request.signal } : {});
|
|
381
|
+
jobControlOptions = description.jobControlOptions;
|
|
382
|
+
}
|
|
383
|
+
if (!jobControlOptions)
|
|
384
|
+
return conformance;
|
|
385
|
+
const required = mode === "sync" ? JOB_CONTROL.sync : JOB_CONTROL.async;
|
|
386
|
+
if (!declares(jobControlOptions, required)) {
|
|
387
|
+
throw capabilityRefusal(`processes.${required}`, request.processId, {
|
|
388
|
+
construct: required,
|
|
389
|
+
declaredJobControlOptions: jobControlOptions.join(","),
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
return conformance;
|
|
393
|
+
}
|
|
82
394
|
}
|
|
83
395
|
/**
|
|
84
396
|
* `IJobRun` implementation backed by OGC API Processes 1.0 status / result
|
|
@@ -91,8 +403,16 @@ export class HonuaOgcProcessJobRun {
|
|
|
91
403
|
id;
|
|
92
404
|
type;
|
|
93
405
|
client;
|
|
406
|
+
basePath;
|
|
94
407
|
pollIntervalMs;
|
|
95
408
|
pollFn;
|
|
409
|
+
pollBudget;
|
|
410
|
+
conformance;
|
|
411
|
+
jobControlOptions;
|
|
412
|
+
capabilityPolicy;
|
|
413
|
+
/** Server-advertised routes, refreshed from every `links[]` the job reports. */
|
|
414
|
+
statusPath;
|
|
415
|
+
resultsPath;
|
|
96
416
|
currentStatus;
|
|
97
417
|
currentProgress;
|
|
98
418
|
terminalSnapshot;
|
|
@@ -102,12 +422,53 @@ export class HonuaOgcProcessJobRun {
|
|
|
102
422
|
this.client = options.client;
|
|
103
423
|
this.id = options.jobId;
|
|
104
424
|
this.type = options.processId ?? options.initialStatus?.processID ?? "unknown";
|
|
425
|
+
this.basePath = options.basePath;
|
|
105
426
|
this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
106
|
-
this.
|
|
427
|
+
this.pollBudget = options.pollBudget;
|
|
428
|
+
this.conformance = options.conformance;
|
|
429
|
+
this.jobControlOptions = options.jobControlOptions;
|
|
430
|
+
this.capabilityPolicy = options.capabilityPolicy ?? "advertised";
|
|
431
|
+
this.statusPath = options.statusPath;
|
|
432
|
+
this.resultsPath = options.resultsPath;
|
|
433
|
+
this.pollFn = options.pollFn ?? ((jobId, signal) => this.fetchStatus(jobId, signal));
|
|
107
434
|
const initial = options.initialStatus;
|
|
435
|
+
this.absorbLinks(initial?.links);
|
|
108
436
|
this.currentStatus = initial?.status ?? "accepted";
|
|
109
437
|
this.currentProgress = progressFromOgcStatus(initial);
|
|
110
438
|
}
|
|
439
|
+
async fetchStatus(jobId, signal) {
|
|
440
|
+
if (this.capabilityPolicy === "strict" && this.statusPath === undefined) {
|
|
441
|
+
throw capabilityRefusal("processes.jobStatusLink", this.type, {
|
|
442
|
+
construct: "job status route",
|
|
443
|
+
linkRelation: PROCESSES_LINK_REL.status,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
return this.client.getOgcProcessJob({
|
|
447
|
+
jobId,
|
|
448
|
+
...(signal ? { signal } : {}),
|
|
449
|
+
...(this.basePath !== undefined ? { basePath: this.basePath } : {}),
|
|
450
|
+
...(this.statusPath !== undefined ? { routePath: this.statusPath } : {}),
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Adopt the routes the server advertises on a job document. Every poll can
|
|
455
|
+
* refresh them, so a server that only publishes its results link once the
|
|
456
|
+
* job succeeds is still followed rather than path-guessed.
|
|
457
|
+
*/
|
|
458
|
+
absorbLinks(links) {
|
|
459
|
+
if (!links || links.length === 0)
|
|
460
|
+
return;
|
|
461
|
+
const baseUrl = this.client.serverBaseUrl;
|
|
462
|
+
const documentUrl = requestUrlFor(baseUrl, this.statusPath ?? templatedJobPath(this.basePath, this.id));
|
|
463
|
+
const results = linkPath(links, documentUrl, baseUrl, PROCESSES_LINK_REL.results);
|
|
464
|
+
if (results)
|
|
465
|
+
this.resultsPath = results;
|
|
466
|
+
if (this.statusPath === undefined) {
|
|
467
|
+
const status = linkPath(links, documentUrl, baseUrl, PROCESSES_LINK_REL.status, "status", "monitor");
|
|
468
|
+
if (status)
|
|
469
|
+
this.statusPath = status;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
111
472
|
get status() {
|
|
112
473
|
return this.currentStatus;
|
|
113
474
|
}
|
|
@@ -130,19 +491,34 @@ export class HonuaOgcProcessJobRun {
|
|
|
130
491
|
// Reset the cached promise if the poll loop rejects (abort / deadline /
|
|
131
492
|
// attempt cap) so a later results() call can retry rather than being
|
|
132
493
|
// permanently poisoned by a transient cancellation.
|
|
133
|
-
this.terminalPromise = this.runUntilTerminal(options).catch((error) => {
|
|
494
|
+
this.terminalPromise = this.runUntilTerminal(this.resolveBudget(options)).catch((error) => {
|
|
134
495
|
this.terminalPromise = undefined;
|
|
135
496
|
throw error;
|
|
136
497
|
});
|
|
137
498
|
}
|
|
138
499
|
return this.terminalPromise;
|
|
139
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 };
|
|
512
|
+
}
|
|
140
513
|
async cancel() {
|
|
141
514
|
if (this.terminalSnapshot) {
|
|
142
515
|
return this.currentStatus;
|
|
143
516
|
}
|
|
517
|
+
this.assertDismissDeclared();
|
|
144
518
|
try {
|
|
145
|
-
|
|
519
|
+
// DELETE targets the job resource itself, so it follows the same
|
|
520
|
+
// advertised route the status polls use rather than re-templating it.
|
|
521
|
+
const cancelled = await this.client.cancelOgcProcessJob(this.jobRequest(this.statusPath));
|
|
146
522
|
const snapshot = await this.handleOgcStatus(cancelled);
|
|
147
523
|
return snapshot.status;
|
|
148
524
|
}
|
|
@@ -238,13 +614,21 @@ export class HonuaOgcProcessJobRun {
|
|
|
238
614
|
const progress = progressFromOgcStatus(ogcStatus);
|
|
239
615
|
this.currentStatus = status;
|
|
240
616
|
this.currentProgress = progress;
|
|
617
|
+
// Routes the server publishes on this document win over the Core template.
|
|
618
|
+
this.absorbLinks(ogcStatus.links);
|
|
241
619
|
if (status === "successful") {
|
|
242
620
|
try {
|
|
621
|
+
if (this.capabilityPolicy === "strict" && this.resultsPath === undefined) {
|
|
622
|
+
throw capabilityRefusal("processes.jobResultsLink", this.type, {
|
|
623
|
+
construct: "job results route",
|
|
624
|
+
linkRelation: PROCESSES_LINK_REL.results,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
243
627
|
// Per OGC API Processes §7.11.1 the document-mode result body is the
|
|
244
628
|
// outputs map itself (honua-server returns `{}` for the canonical
|
|
245
629
|
// V1 process; populated maps are keyed by output id). Wrap it into
|
|
246
630
|
// the canonical `JobResult.outputs` envelope.
|
|
247
|
-
const results = await this.client.getOgcProcessJobResults(
|
|
631
|
+
const results = await this.client.getOgcProcessJobResults(this.jobRequest(this.resultsPath));
|
|
248
632
|
const snapshot = {
|
|
249
633
|
status: "successful",
|
|
250
634
|
progress,
|
|
@@ -255,6 +639,12 @@ export class HonuaOgcProcessJobRun {
|
|
|
255
639
|
return snapshot;
|
|
256
640
|
}
|
|
257
641
|
catch (error) {
|
|
642
|
+
// A fail-closed capability refusal is not a job failure: the job may
|
|
643
|
+
// well have succeeded, the SDK simply refuses to guess the route.
|
|
644
|
+
// Surfacing it as `failed` would launder a client-side refusal into a
|
|
645
|
+
// server-side outcome.
|
|
646
|
+
if (error instanceof HonuaCapabilityNotSupportedError)
|
|
647
|
+
throw error;
|
|
258
648
|
const failure = {
|
|
259
649
|
status: "failed",
|
|
260
650
|
progress,
|
|
@@ -284,6 +674,44 @@ export class HonuaOgcProcessJobRun {
|
|
|
284
674
|
this.notify(snapshot);
|
|
285
675
|
return snapshot;
|
|
286
676
|
}
|
|
677
|
+
/**
|
|
678
|
+
* Job-route envelope pinned to the root the job was created against,
|
|
679
|
+
* preferring a route the server advertised over the Core path template.
|
|
680
|
+
*/
|
|
681
|
+
jobRequest(routePath) {
|
|
682
|
+
return {
|
|
683
|
+
jobId: this.id,
|
|
684
|
+
...(this.basePath !== undefined ? { basePath: this.basePath } : {}),
|
|
685
|
+
...(routePath !== undefined ? { routePath } : {}),
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Dismissal is an optional Core extension. When the server's declaration is
|
|
690
|
+
* in hand and neither the Dismiss conformance class nor the process's
|
|
691
|
+
* `jobControlOptions` includes `dismiss`, refuse before issuing a DELETE the
|
|
692
|
+
* server never advertised.
|
|
693
|
+
*/
|
|
694
|
+
assertDismissDeclared() {
|
|
695
|
+
// Under `"strict"` the DELETE route must have been advertised, exactly as
|
|
696
|
+
// the status route must be, so dismissal is never sent to a guessed path.
|
|
697
|
+
if (this.capabilityPolicy === "strict" && this.statusPath === undefined) {
|
|
698
|
+
throw capabilityRefusal("processes.jobStatusLink", this.type, {
|
|
699
|
+
construct: "job status route",
|
|
700
|
+
linkRelation: PROCESSES_LINK_REL.status,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
if (declares(this.jobControlOptions, JOB_CONTROL.dismiss))
|
|
704
|
+
return;
|
|
705
|
+
const conformsTo = this.conformance?.conformsTo;
|
|
706
|
+
if (!conformsTo)
|
|
707
|
+
return;
|
|
708
|
+
if (hasOgcConformanceClass(this.conformance, PROCESSES_CONFORMANCE_MATCH.dismiss))
|
|
709
|
+
return;
|
|
710
|
+
throw capabilityRefusal("processes.dismiss", this.type, {
|
|
711
|
+
missingClass: PROCESSES_CONFORMANCE.dismiss,
|
|
712
|
+
construct: JOB_CONTROL.dismiss,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
287
715
|
notify(snapshot) {
|
|
288
716
|
for (const listener of this.listeners) {
|
|
289
717
|
try {
|
|
@@ -295,6 +723,42 @@ export class HonuaOgcProcessJobRun {
|
|
|
295
723
|
}
|
|
296
724
|
}
|
|
297
725
|
}
|
|
726
|
+
/**
|
|
727
|
+
* `IJobRun` for a synchronous execution (Core §7.11: `200` with the results
|
|
728
|
+
* document inline). No job resource exists on the server, so the run starts
|
|
729
|
+
* terminal: `results()` resolves from memory, `poll()` replays the terminal
|
|
730
|
+
* snapshot, and `cancel()` is a no-op that reports the outcome that already
|
|
731
|
+
* happened. Callers therefore read a sync and an async execution through one
|
|
732
|
+
* surface.
|
|
733
|
+
*/
|
|
734
|
+
export class HonuaOgcProcessSyncRun {
|
|
735
|
+
/** Empty: OGC Processes assigns no job identifier to a synchronous execution. */
|
|
736
|
+
id = "";
|
|
737
|
+
type;
|
|
738
|
+
status = "successful";
|
|
739
|
+
progress = { percent: 100 };
|
|
740
|
+
snapshot;
|
|
741
|
+
constructor(processId, outputs) {
|
|
742
|
+
this.type = processId;
|
|
743
|
+
this.snapshot = { status: "successful", progress: this.progress, result: { outputs } };
|
|
744
|
+
}
|
|
745
|
+
async poll() {
|
|
746
|
+
return this.snapshot;
|
|
747
|
+
}
|
|
748
|
+
watch(listener) {
|
|
749
|
+
// The only transition already happened; replay it so watchers registered
|
|
750
|
+
// after a synchronous execution still observe the terminal snapshot.
|
|
751
|
+
listener(this.snapshot);
|
|
752
|
+
return () => { };
|
|
753
|
+
}
|
|
754
|
+
async results() {
|
|
755
|
+
// Non-null: the constructor always builds a `successful` snapshot with a result.
|
|
756
|
+
return this.snapshot.result;
|
|
757
|
+
}
|
|
758
|
+
async cancel() {
|
|
759
|
+
return "successful";
|
|
760
|
+
}
|
|
761
|
+
}
|
|
298
762
|
/** Error thrown when `IJobRun.results()` resolves a non-success terminal. */
|
|
299
763
|
export class HonuaJobFailedError extends HonuaSdkError {
|
|
300
764
|
status;
|
|
@@ -411,37 +875,145 @@ export async function listOgcProcesses(transport, request = {}) {
|
|
|
411
875
|
}
|
|
412
876
|
export async function getOgcProcess(transport, request) {
|
|
413
877
|
const params = createOgcMetadataParams(request);
|
|
414
|
-
|
|
878
|
+
const base = processesBase(request);
|
|
879
|
+
return transport.requestCachedMetadataJson(
|
|
880
|
+
// Both components are percent-encoded so neither can smuggle the `:`
|
|
881
|
+
// separator and alias a different (basePath, processId) pair.
|
|
882
|
+
`ogc-processes:process:${processesBaseKey(request)}${encodeURIComponent(request.processId)}:${params.toString()}`, `${base}/processes/${encodeURIComponent(request.processId)}?${params.toString()}`, request);
|
|
415
883
|
}
|
|
884
|
+
/**
|
|
885
|
+
* `POST /processes/{processId}/execution` (Core §7.11).
|
|
886
|
+
*
|
|
887
|
+
* Core defines two success shapes and a conformant server may choose either:
|
|
888
|
+
* `200` with the results document (synchronous) or `201` + `Location` with a
|
|
889
|
+
* `statusInfo` body (asynchronous). This reads the response rather than
|
|
890
|
+
* assuming one, so the same call works against a raw third-party server and
|
|
891
|
+
* against the async-only Honua facade, and reports which shape came back.
|
|
892
|
+
*/
|
|
416
893
|
export async function executeOgcProcess(transport, request) {
|
|
417
894
|
const headers = mergeHeaders({ "Content-Type": "application/json", Accept: "application/json" }, request.headers, preferHeaderForExecute(request));
|
|
418
|
-
const path =
|
|
419
|
-
//
|
|
420
|
-
//
|
|
895
|
+
const path = `${processesBase(request)}/processes/${encodeURIComponent(request.processId)}/execution`;
|
|
896
|
+
// `document` is the only response mode the SDK decodes: its body is the
|
|
897
|
+
// output-id-keyed map that `JobResult.outputs` is defined over. (honua-server
|
|
898
|
+
// additionally rejects `raw` with HTTP 501.)
|
|
421
899
|
const body = JSON.stringify({
|
|
422
900
|
inputs: request.inputs ?? {},
|
|
423
901
|
outputs: request.outputs,
|
|
424
902
|
response: "document",
|
|
425
903
|
});
|
|
426
|
-
|
|
904
|
+
// pipelineFetch over requestJson: Core distinguishes the synchronous and
|
|
905
|
+
// asynchronous outcomes by status code and `Location`, both of which a
|
|
906
|
+
// JSON-only helper discards.
|
|
907
|
+
const response = await transport.pipelineFetch("POST", path, { headers, body }, request.signal);
|
|
908
|
+
const payload = await readExecutionPayload(response);
|
|
909
|
+
const statusInfo = asJobStatusInfo(payload, request.processId);
|
|
910
|
+
const documentUrl = requestUrlFor(transport.baseUrl, path);
|
|
911
|
+
const location = advertisedRoutePath(response.headers.get("Location") ?? undefined, documentUrl, transport.baseUrl);
|
|
912
|
+
if (statusInfo) {
|
|
913
|
+
const statusPath = location ?? linkPath(statusInfo.links, documentUrl, transport.baseUrl, PROCESSES_LINK_REL.status, "status");
|
|
914
|
+
return {
|
|
915
|
+
jobID: statusInfo.jobID,
|
|
916
|
+
status: statusInfo.status,
|
|
917
|
+
processID: statusInfo.processID ?? request.processId,
|
|
918
|
+
...(statusInfo.links ? { links: [...statusInfo.links] } : {}),
|
|
919
|
+
statusInfo,
|
|
920
|
+
...(statusPath !== undefined ? { statusPath } : {}),
|
|
921
|
+
synchronous: false,
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
if (location !== undefined) {
|
|
925
|
+
// `201 Location` with no (or an unrecognized) body: the job exists and the
|
|
926
|
+
// server named its route. Derive the id from the advertised route's last
|
|
927
|
+
// segment rather than inventing one.
|
|
928
|
+
const jobId = jobIdFromRoute(location);
|
|
929
|
+
return {
|
|
930
|
+
jobID: jobId,
|
|
931
|
+
status: "accepted",
|
|
932
|
+
processID: request.processId,
|
|
933
|
+
statusPath: location,
|
|
934
|
+
synchronous: false,
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
// No job identifier and no job route: Core leaves only the synchronous shape,
|
|
938
|
+
// whose body IS the results document.
|
|
939
|
+
return {
|
|
940
|
+
jobID: "",
|
|
941
|
+
status: "successful",
|
|
942
|
+
processID: request.processId,
|
|
943
|
+
results: (payload ?? {}),
|
|
944
|
+
synchronous: true,
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Decode an execution response body. A `204` (or empty body) yields
|
|
949
|
+
* `undefined`; a non-JSON body is refused rather than guessed at, because the
|
|
950
|
+
* SDK asked for `response: "document"` and can only project a JSON outputs map
|
|
951
|
+
* onto `JobResult.outputs`.
|
|
952
|
+
*/
|
|
953
|
+
async function readExecutionPayload(response) {
|
|
954
|
+
if (response.status === 204)
|
|
955
|
+
return undefined;
|
|
956
|
+
const text = await response.text();
|
|
957
|
+
if (text.trim().length === 0)
|
|
958
|
+
return undefined;
|
|
959
|
+
try {
|
|
960
|
+
return JSON.parse(text);
|
|
961
|
+
}
|
|
962
|
+
catch {
|
|
963
|
+
throw capabilityRefusal("processes.documentResponse", undefined, {
|
|
964
|
+
construct: "document-mode execution response",
|
|
965
|
+
contentType: response.headers.get("Content-Type") ?? "unknown",
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
/** Recognize an async `statusInfo` body: Core requires a non-empty `jobID`. */
|
|
970
|
+
function asJobStatusInfo(payload, processId) {
|
|
971
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
972
|
+
return undefined;
|
|
973
|
+
const candidate = payload;
|
|
974
|
+
if (typeof candidate.jobID !== "string" || candidate.jobID.length === 0)
|
|
975
|
+
return undefined;
|
|
976
|
+
return {
|
|
977
|
+
...candidate,
|
|
978
|
+
jobID: candidate.jobID,
|
|
979
|
+
processID: candidate.processID ?? processId,
|
|
980
|
+
status: candidate.status ?? "accepted",
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
/** Last path segment of an advertised job route, decoded. */
|
|
984
|
+
function jobIdFromRoute(routePath) {
|
|
985
|
+
const withoutQuery = routePath.split("?", 1)[0] ?? routePath;
|
|
986
|
+
const trimmed = withoutQuery.endsWith("/") ? withoutQuery.slice(0, -1) : withoutQuery;
|
|
987
|
+
const segment = trimmed.slice(trimmed.lastIndexOf("/") + 1);
|
|
988
|
+
try {
|
|
989
|
+
return decodeURIComponent(segment);
|
|
990
|
+
}
|
|
991
|
+
catch {
|
|
992
|
+
return segment;
|
|
993
|
+
}
|
|
427
994
|
}
|
|
428
995
|
export async function getOgcProcessJob(transport, request) {
|
|
429
996
|
const params = createOgcMetadataParams(request);
|
|
430
|
-
|
|
997
|
+
const route = request.routePath ?? `${processesBase(request)}/jobs/${encodeURIComponent(request.jobId)}`;
|
|
998
|
+
return transport.requestJson("GET", withParams(route, params), undefined, request.signal);
|
|
431
999
|
}
|
|
432
1000
|
export async function getOgcProcessJobResults(transport, request) {
|
|
433
1001
|
const params = createOgcMetadataParams(request);
|
|
434
|
-
|
|
1002
|
+
const route = request.routePath ?? `${processesBase(request)}/jobs/${encodeURIComponent(request.jobId)}/results`;
|
|
1003
|
+
return transport.requestJson("GET", withParams(route, params), undefined, request.signal);
|
|
435
1004
|
}
|
|
436
1005
|
export async function cancelOgcProcessJob(transport, request) {
|
|
437
1006
|
const params = createOgcMetadataParams(request);
|
|
438
|
-
|
|
1007
|
+
const route = request.routePath ?? `${processesBase(request)}/jobs/${encodeURIComponent(request.jobId)}`;
|
|
1008
|
+
return transport.requestJson("DELETE", withParams(route, params), undefined, request.signal);
|
|
439
1009
|
}
|
|
440
1010
|
/**
|
|
441
1011
|
* Build the `Prefer` header for an OGC API Processes execution request.
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
* `
|
|
1012
|
+
* `mode: "async"` sends an explicit `Prefer: respond-async` (RFC 7240) so a
|
|
1013
|
+
* server that supports both shapes returns a job and OGC conformance checkers
|
|
1014
|
+
* see the header. `"sync"` and `"auto"` omit it: Core defines no header that
|
|
1015
|
+
* forces synchronous execution, so `"sync"` is expressed by *not* preferring
|
|
1016
|
+
* async plus the `sync-execute` capability assertion made before the post.
|
|
445
1017
|
*/
|
|
446
1018
|
function preferHeaderForExecute(request) {
|
|
447
1019
|
if (request.mode === "async") {
|