@honua/react 0.1.2-beta.0 → 0.1.7-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 +88 -19
- package/contract/types.d.ts.map +1 -1
- package/contract/types.js +4 -0
- package/contract/widget-source.d.ts.map +1 -1
- package/core/client.d.ts +51 -21
- package/core/client.d.ts.map +1 -1
- package/core/client.js +234 -54
- package/core/error-classifications.d.ts +23 -0
- package/core/error-classifications.d.ts.map +1 -1
- package/core/error-classifications.js +23 -0
- package/core/error-code-registry.d.ts +24 -1
- package/core/error-code-registry.d.ts.map +1 -1
- package/core/error-code-registry.js +23 -0
- package/core/error-envelope.js +2 -0
- package/core/errors.d.ts +2 -2
- package/core/errors.d.ts.map +1 -1
- package/core/errors.js +1 -1
- package/core/geoparquet-sql.d.ts +69 -5
- package/core/geoparquet-sql.d.ts.map +1 -1
- package/core/geoparquet-sql.js +327 -10
- 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 +32 -0
- package/core/stac.d.ts.map +1 -1
- package/core/stac.js +158 -23
- package/core/surfaces.d.ts.map +1 -1
- package/core/surfaces.js +273 -68
- package/core/types.d.ts +138 -11
- 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/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/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/maplibre-target.js +1 -1
- 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 +153 -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/runtime/style-compose.d.ts.map +1 -1
- package/runtime/style-compose.js +78 -9
- 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
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport-neutral, resumable delivery gate for realtime feature events.
|
|
3
|
+
*
|
|
4
|
+
* This module deliberately does not reconnect a transport. It protects the
|
|
5
|
+
* boundary between a transport and a consumer: compatible checkpoints resume,
|
|
6
|
+
* ordered events advance durably, duplicates are bounded, and gaps or queue
|
|
7
|
+
* overflow require an explicit replacement snapshot.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
import { HonuaSdkError, mergeHonuaErrorContext } from "../core/error-envelope.js";
|
|
12
|
+
export const REALTIME_DURABLE_CHECKPOINT_VERSION = 1;
|
|
13
|
+
/**
|
|
14
|
+
* Public realtime failure with the original detailed reason on `.code` and a
|
|
15
|
+
* stable common-envelope recovery class on `.sdkCode`. Classification metadata
|
|
16
|
+
* does not reconnect a transport or request a replacement snapshot.
|
|
17
|
+
*/
|
|
18
|
+
export class HonuaRealtimeResumeError extends HonuaSdkError {
|
|
19
|
+
code;
|
|
20
|
+
constructor(code, message, options) {
|
|
21
|
+
super(realtimeResumeSdkCode(code), message, {
|
|
22
|
+
...options,
|
|
23
|
+
context: mergeHonuaErrorContext({ reasonCode: realtimeResumeContextReason(code) }),
|
|
24
|
+
});
|
|
25
|
+
this.code = code;
|
|
26
|
+
this.name = "HonuaRealtimeResumeError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const REALTIME_RESUME_ERROR_CODES = {
|
|
30
|
+
compatible: "realtime.protocol.terminal",
|
|
31
|
+
"invalid-checkpoint": "realtime.checkpoint.invalid",
|
|
32
|
+
"source-changed": "realtime.checkpoint.invalid",
|
|
33
|
+
"query-changed": "realtime.checkpoint.invalid",
|
|
34
|
+
"source-version-changed": "realtime.checkpoint.invalid",
|
|
35
|
+
"schema-version-changed": "realtime.checkpoint.invalid",
|
|
36
|
+
"authorization-scope-changed": "realtime.checkpoint.invalid",
|
|
37
|
+
"snapshot-required": "realtime.sequence.gap",
|
|
38
|
+
"replacement-snapshot-required": "realtime.sequence.gap",
|
|
39
|
+
"invalid-event": "realtime.protocol.terminal",
|
|
40
|
+
"sequence-missing": "realtime.sequence.gap",
|
|
41
|
+
"checkpoint-conflict": "realtime.checkpoint.invalid",
|
|
42
|
+
"sequence-gap": "realtime.sequence.gap",
|
|
43
|
+
"event-id-reused": "realtime.sequence.gap",
|
|
44
|
+
"buffer-overflow": "realtime.sequence.gap",
|
|
45
|
+
"consumer-failed": "realtime.protocol.terminal",
|
|
46
|
+
"delivery-failed": "realtime.protocol.terminal",
|
|
47
|
+
"checkpoint-load-failed": "realtime.protocol.terminal",
|
|
48
|
+
"checkpoint-save-failed": "realtime.protocol.terminal",
|
|
49
|
+
"cursor-expired": "realtime.transport.reconnectable",
|
|
50
|
+
"resume-unsupported": "realtime.transport.reconnectable",
|
|
51
|
+
"transport-gap": "realtime.transport.reconnectable",
|
|
52
|
+
cancelled: "realtime.cancelled",
|
|
53
|
+
closed: "realtime.cancelled",
|
|
54
|
+
};
|
|
55
|
+
function realtimeResumeSdkCode(code) {
|
|
56
|
+
return isRegisteredRealtimeResumeReason(code) ? REALTIME_RESUME_ERROR_CODES[code] : "realtime.protocol.terminal";
|
|
57
|
+
}
|
|
58
|
+
function realtimeResumeContextReason(code) {
|
|
59
|
+
return isRegisteredRealtimeResumeReason(code) ? code : "invalid-event";
|
|
60
|
+
}
|
|
61
|
+
function isRegisteredRealtimeResumeReason(code) {
|
|
62
|
+
return typeof code === "string" && Object.hasOwn(REALTIME_RESUME_ERROR_CODES, code);
|
|
63
|
+
}
|
|
64
|
+
const DEFAULT_MAX_PENDING_EVENTS = 64;
|
|
65
|
+
const DEFAULT_MAX_RECENT_EVENT_IDS = 256;
|
|
66
|
+
const MAX_RECENT_EVENT_IDS = 4096;
|
|
67
|
+
/** Validate a checkpoint against the exact source/query/schema/ACL context. */
|
|
68
|
+
export function evaluateRealtimeCheckpoint(context, checkpoint) {
|
|
69
|
+
if (!isCheckpointShape(checkpoint)) {
|
|
70
|
+
return compatibility(false, "invalid-checkpoint", "Checkpoint shape or version is invalid.");
|
|
71
|
+
}
|
|
72
|
+
if (checkpoint.context.sourceId !== context.sourceId) {
|
|
73
|
+
return compatibility(false, "source-changed", "Checkpoint source identity does not match the subscription.");
|
|
74
|
+
}
|
|
75
|
+
if (checkpoint.context.queryFingerprint !== context.queryFingerprint) {
|
|
76
|
+
return compatibility(false, "query-changed", "Checkpoint query identity does not match the accepted query.");
|
|
77
|
+
}
|
|
78
|
+
if (checkpoint.context.sourceVersion !== context.sourceVersion) {
|
|
79
|
+
return compatibility(false, "source-version-changed", "Checkpoint source version requires a new snapshot.");
|
|
80
|
+
}
|
|
81
|
+
if (checkpoint.context.schemaVersion !== context.schemaVersion) {
|
|
82
|
+
return compatibility(false, "schema-version-changed", "Checkpoint schema version requires a new snapshot.");
|
|
83
|
+
}
|
|
84
|
+
if (checkpoint.context.authorizationScopeFingerprint !== context.authorizationScopeFingerprint) {
|
|
85
|
+
return compatibility(false, "authorization-scope-changed", "Checkpoint authorization scope does not match the current grants.");
|
|
86
|
+
}
|
|
87
|
+
return compatibility(true, "compatible", "Checkpoint is compatible with the subscription context.");
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Create a serial, bounded delivery gate. Loading a durable checkpoint is the
|
|
91
|
+
* only asynchronous construction step; no transport is opened here.
|
|
92
|
+
*/
|
|
93
|
+
export async function createResumableRealtimeSubscription(options) {
|
|
94
|
+
const context = normalizeContext(options.context);
|
|
95
|
+
const maxPendingEvents = positiveInteger(options.maxPendingEvents ?? DEFAULT_MAX_PENDING_EVENTS, "maxPendingEvents");
|
|
96
|
+
const maxRecentEventIds = positiveInteger(options.maxRecentEventIds ?? DEFAULT_MAX_RECENT_EVENT_IDS, "maxRecentEventIds");
|
|
97
|
+
if (maxRecentEventIds > MAX_RECENT_EVENT_IDS) {
|
|
98
|
+
throw new HonuaRealtimeResumeError("invalid-checkpoint", `maxRecentEventIds cannot exceed the ${MAX_RECENT_EVENT_IDS}-entry safety ceiling.`);
|
|
99
|
+
}
|
|
100
|
+
const lifecycle = new AbortController();
|
|
101
|
+
const externalAbort = () => lifecycle.abort(options.signal?.reason);
|
|
102
|
+
if (options.signal?.aborted)
|
|
103
|
+
externalAbort();
|
|
104
|
+
else
|
|
105
|
+
options.signal?.addEventListener("abort", externalAbort, { once: true });
|
|
106
|
+
let loadedCheckpoint = options.initialCheckpoint;
|
|
107
|
+
if (!loadedCheckpoint && options.checkpointStore && !lifecycle.signal.aborted) {
|
|
108
|
+
try {
|
|
109
|
+
loadedCheckpoint = await options.checkpointStore.load(context, lifecycle.signal);
|
|
110
|
+
}
|
|
111
|
+
catch (cause) {
|
|
112
|
+
if (lifecycle.signal.aborted)
|
|
113
|
+
loadedCheckpoint = undefined;
|
|
114
|
+
else {
|
|
115
|
+
options.signal?.removeEventListener("abort", externalAbort);
|
|
116
|
+
throw new HonuaRealtimeResumeError("checkpoint-load-failed", "Unable to load the realtime checkpoint.", {
|
|
117
|
+
cause,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const compatibilityResult = loadedCheckpoint ? evaluateRealtimeCheckpoint(context, loadedCheckpoint) : undefined;
|
|
123
|
+
const initialCheckpoint = loadedCheckpoint && compatibilityResult?.compatible
|
|
124
|
+
? cloneCheckpoint(loadedCheckpoint, maxRecentEventIds)
|
|
125
|
+
: undefined;
|
|
126
|
+
let state = freezeState({
|
|
127
|
+
phase: lifecycle.signal.aborted
|
|
128
|
+
? "closed"
|
|
129
|
+
: compatibilityResult && !compatibilityResult.compatible
|
|
130
|
+
? "resnapshot-required"
|
|
131
|
+
: initialCheckpoint
|
|
132
|
+
? "resuming"
|
|
133
|
+
: "awaiting-snapshot",
|
|
134
|
+
...(lifecycle.signal.aborted
|
|
135
|
+
? { reason: "cancelled", detail: "Subscription was cancelled before it started." }
|
|
136
|
+
: compatibilityResult && !compatibilityResult.compatible
|
|
137
|
+
? { reason: compatibilityResult.code, detail: compatibilityResult.reason }
|
|
138
|
+
: {}),
|
|
139
|
+
...(initialCheckpoint ? { checkpoint: initialCheckpoint } : {}),
|
|
140
|
+
checkpointPersisted: initialCheckpoint !== undefined,
|
|
141
|
+
pendingEvents: 0,
|
|
142
|
+
acceptedEventCount: 0,
|
|
143
|
+
duplicateEventCount: 0,
|
|
144
|
+
gapCount: 0,
|
|
145
|
+
overflowCount: 0,
|
|
146
|
+
});
|
|
147
|
+
const queue = [];
|
|
148
|
+
let active;
|
|
149
|
+
let activeAbort;
|
|
150
|
+
let generation = 0;
|
|
151
|
+
const subscription = {
|
|
152
|
+
get state() {
|
|
153
|
+
return state;
|
|
154
|
+
},
|
|
155
|
+
enqueue(event) {
|
|
156
|
+
if (state.phase === "closed")
|
|
157
|
+
return Promise.resolve(delivery("cancelled", state.reason ?? "closed", state));
|
|
158
|
+
if (state.phase === "error")
|
|
159
|
+
return Promise.resolve(delivery("error", state.reason, state));
|
|
160
|
+
const capture = captureRealtimeEvent(event);
|
|
161
|
+
if ("conflict" in capture) {
|
|
162
|
+
const reason = capture.reason ?? "invalid-event";
|
|
163
|
+
transitionToResnapshot(reason, capture.conflict);
|
|
164
|
+
return Promise.resolve(delivery("resnapshot-required", reason, state));
|
|
165
|
+
}
|
|
166
|
+
const replacementSnapshot = capture.replacementSnapshot;
|
|
167
|
+
if (state.phase === "resnapshot-required" && !replacementSnapshot) {
|
|
168
|
+
return Promise.resolve(delivery("resnapshot-required", state.reason, state));
|
|
169
|
+
}
|
|
170
|
+
const recoveringFromGap = state.phase === "resnapshot-required" && replacementSnapshot;
|
|
171
|
+
if (recoveringFromGap) {
|
|
172
|
+
invalidatePending("replacement-snapshot-required");
|
|
173
|
+
setState({ phase: "awaiting-snapshot", reason: undefined, detail: undefined });
|
|
174
|
+
}
|
|
175
|
+
// A replacement snapshot is the sole recovery path. Let one recovery
|
|
176
|
+
// item wait behind an abort-ignoring active consumer even when the data
|
|
177
|
+
// queue is full; ordinary events remain strictly bounded.
|
|
178
|
+
if (!recoveringFromGap && queue.length + (active ? 1 : 0) >= maxPendingEvents) {
|
|
179
|
+
transitionToResnapshot("buffer-overflow", `Pending realtime delivery exceeded the configured ${maxPendingEvents}-event bound.`, true);
|
|
180
|
+
return Promise.resolve(delivery("resnapshot-required", "buffer-overflow", state));
|
|
181
|
+
}
|
|
182
|
+
return new Promise((resolve) => {
|
|
183
|
+
queue.push({ captured: Object.freeze({ ...capture, recoverySnapshot: recoveringFromGap }), resolve });
|
|
184
|
+
setState({ pendingEvents: queue.length + (active ? 1 : 0) });
|
|
185
|
+
pump();
|
|
186
|
+
});
|
|
187
|
+
},
|
|
188
|
+
requireResnapshot(reason, detail = "The transport requires a replacement snapshot before delivery can continue.") {
|
|
189
|
+
if (state.phase === "closed" || state.phase === "error")
|
|
190
|
+
return;
|
|
191
|
+
transitionToResnapshot(reason, detail);
|
|
192
|
+
},
|
|
193
|
+
close(reason = "Subscription closed by caller.") {
|
|
194
|
+
closeSubscription(reason, "closed", true);
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
if (lifecycle.signal.aborted) {
|
|
198
|
+
options.signal?.removeEventListener("abort", externalAbort);
|
|
199
|
+
if (state.phase !== "closed")
|
|
200
|
+
closeSubscription("Subscription signal was aborted.", "cancelled", false);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
lifecycle.signal.addEventListener("abort", () => closeSubscription("Subscription signal was aborted.", "cancelled", false), { once: true });
|
|
204
|
+
}
|
|
205
|
+
return subscription;
|
|
206
|
+
function pump() {
|
|
207
|
+
if (active || state.phase === "closed" || state.phase === "error")
|
|
208
|
+
return;
|
|
209
|
+
const next = queue.shift();
|
|
210
|
+
if (!next) {
|
|
211
|
+
setState({ pendingEvents: 0 });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
active = next;
|
|
215
|
+
activeAbort = new AbortController();
|
|
216
|
+
const operationGeneration = generation;
|
|
217
|
+
setState({ pendingEvents: queue.length + 1 });
|
|
218
|
+
void process(next.captured, activeAbort.signal, operationGeneration)
|
|
219
|
+
.then(next.resolve, (cause) => {
|
|
220
|
+
transitionToError("delivery-failed", errorMessage(cause));
|
|
221
|
+
next.resolve(delivery("error", "delivery-failed", state));
|
|
222
|
+
})
|
|
223
|
+
.finally(() => {
|
|
224
|
+
active = undefined;
|
|
225
|
+
activeAbort = undefined;
|
|
226
|
+
setState({ pendingEvents: queue.length });
|
|
227
|
+
pump();
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
async function process(captured, signal, operationGeneration) {
|
|
231
|
+
if (signal.aborted || lifecycle.signal.aborted || operationGeneration !== generation) {
|
|
232
|
+
return delivery("cancelled", "cancelled", state);
|
|
233
|
+
}
|
|
234
|
+
const { event, eventId, position, replacementSnapshot, recoverySnapshot } = captured;
|
|
235
|
+
if (position.conflict) {
|
|
236
|
+
transitionToResnapshot("checkpoint-conflict", position.conflict);
|
|
237
|
+
return delivery("resnapshot-required", "checkpoint-conflict", state);
|
|
238
|
+
}
|
|
239
|
+
if (!isSequence(position.sequence)) {
|
|
240
|
+
transitionToResnapshot("sequence-missing", "Sequenced snapshot and delta events require a safe integer sequence.");
|
|
241
|
+
return delivery("resnapshot-required", "sequence-missing", state);
|
|
242
|
+
}
|
|
243
|
+
const previousSequence = state.checkpoint?.resume.sequence;
|
|
244
|
+
if (!state.checkpoint && !replacementSnapshot) {
|
|
245
|
+
transitionToResnapshot("snapshot-required", "A replacement snapshot is required before delta delivery.");
|
|
246
|
+
return delivery("resnapshot-required", "snapshot-required", state);
|
|
247
|
+
}
|
|
248
|
+
if (event.type === "snapshot" && !replacementSnapshot && state.phase !== "live") {
|
|
249
|
+
transitionToResnapshot("replacement-snapshot-required", "A merging snapshot cannot establish a resume baseline.");
|
|
250
|
+
return delivery("resnapshot-required", "replacement-snapshot-required", state);
|
|
251
|
+
}
|
|
252
|
+
if (previousSequence !== undefined && position.sequence === previousSequence) {
|
|
253
|
+
const conflict = sameSequenceCheckpointConflict(state.checkpoint?.resume, position.resume);
|
|
254
|
+
if (conflict) {
|
|
255
|
+
transitionToResnapshot("checkpoint-conflict", conflict);
|
|
256
|
+
return delivery("resnapshot-required", "checkpoint-conflict", state);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (previousSequence !== undefined && !recoverySnapshot) {
|
|
260
|
+
if (position.sequence <= previousSequence) {
|
|
261
|
+
setState({ duplicateEventCount: state.duplicateEventCount + 1 });
|
|
262
|
+
return delivery("duplicate", undefined, state);
|
|
263
|
+
}
|
|
264
|
+
if (!replacementSnapshot &&
|
|
265
|
+
(previousSequence === Number.MAX_SAFE_INTEGER || position.sequence !== previousSequence + 1)) {
|
|
266
|
+
transitionToResnapshot("sequence-gap", `Expected realtime sequence ${previousSequence + 1}, received ${position.sequence}.`);
|
|
267
|
+
return delivery("resnapshot-required", "sequence-gap", state);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const recentIds = recoverySnapshot ? [] : (state.checkpoint?.recentEventIds ?? []);
|
|
271
|
+
if (eventId && recentIds.includes(eventId)) {
|
|
272
|
+
transitionToResnapshot("event-id-reused", `Event id "${eventId}" was reused at a new sequence.`);
|
|
273
|
+
return delivery("resnapshot-required", "event-id-reused", state);
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
await options.apply(event, signal);
|
|
277
|
+
}
|
|
278
|
+
catch (cause) {
|
|
279
|
+
if (signal.aborted || lifecycle.signal.aborted || operationGeneration !== generation) {
|
|
280
|
+
return delivery("cancelled", "cancelled", state);
|
|
281
|
+
}
|
|
282
|
+
transitionToError("consumer-failed", errorMessage(cause));
|
|
283
|
+
return delivery("error", "consumer-failed", state);
|
|
284
|
+
}
|
|
285
|
+
if (signal.aborted || lifecycle.signal.aborted || operationGeneration !== generation) {
|
|
286
|
+
return delivery("cancelled", "cancelled", state);
|
|
287
|
+
}
|
|
288
|
+
const checkpoint = createCheckpoint(context, recoverySnapshot ? undefined : state.checkpoint, { ...position, sequence: position.sequence }, eventId, maxRecentEventIds, options.now?.() ?? Date.now());
|
|
289
|
+
setState({
|
|
290
|
+
phase: "live",
|
|
291
|
+
reason: undefined,
|
|
292
|
+
detail: undefined,
|
|
293
|
+
checkpoint,
|
|
294
|
+
checkpointPersisted: false,
|
|
295
|
+
acceptedEventCount: state.acceptedEventCount + 1,
|
|
296
|
+
});
|
|
297
|
+
if (options.checkpointStore) {
|
|
298
|
+
try {
|
|
299
|
+
await options.checkpointStore.save(checkpoint, signal);
|
|
300
|
+
}
|
|
301
|
+
catch (cause) {
|
|
302
|
+
if (signal.aborted || lifecycle.signal.aborted || operationGeneration !== generation) {
|
|
303
|
+
return delivery("cancelled", "cancelled", state);
|
|
304
|
+
}
|
|
305
|
+
transitionToError("checkpoint-save-failed", errorMessage(cause), false);
|
|
306
|
+
return delivery("error", "checkpoint-save-failed", state);
|
|
307
|
+
}
|
|
308
|
+
if (signal.aborted || lifecycle.signal.aborted || operationGeneration !== generation) {
|
|
309
|
+
return delivery("cancelled", "cancelled", state);
|
|
310
|
+
}
|
|
311
|
+
setState({ checkpointPersisted: true });
|
|
312
|
+
}
|
|
313
|
+
return delivery("applied", undefined, state);
|
|
314
|
+
}
|
|
315
|
+
function transitionToResnapshot(reason, detail, overflow = false) {
|
|
316
|
+
generation += 1;
|
|
317
|
+
activeAbort?.abort(detail);
|
|
318
|
+
drainQueue("resnapshot-required", reason);
|
|
319
|
+
setState({
|
|
320
|
+
phase: "resnapshot-required",
|
|
321
|
+
reason,
|
|
322
|
+
detail,
|
|
323
|
+
gapCount: state.gapCount + (reason === "sequence-gap" || reason === "transport-gap" ? 1 : 0),
|
|
324
|
+
overflowCount: state.overflowCount + (overflow ? 1 : 0),
|
|
325
|
+
pendingEvents: active ? 1 : 0,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
function invalidatePending(reason) {
|
|
329
|
+
generation += 1;
|
|
330
|
+
activeAbort?.abort(reason);
|
|
331
|
+
drainQueue("resnapshot-required", reason);
|
|
332
|
+
}
|
|
333
|
+
function transitionToError(reason, detail, checkpointPersisted = state.checkpointPersisted) {
|
|
334
|
+
generation += 1;
|
|
335
|
+
activeAbort?.abort(detail);
|
|
336
|
+
drainQueue("error", reason);
|
|
337
|
+
setState({
|
|
338
|
+
phase: "error",
|
|
339
|
+
reason,
|
|
340
|
+
detail,
|
|
341
|
+
checkpointPersisted,
|
|
342
|
+
pendingEvents: active ? 1 : 0,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
function closeSubscription(reason, reasonCode, abortLifecycle) {
|
|
346
|
+
if (state.phase === "closed")
|
|
347
|
+
return;
|
|
348
|
+
generation += 1;
|
|
349
|
+
activeAbort?.abort(reason);
|
|
350
|
+
options.signal?.removeEventListener("abort", externalAbort);
|
|
351
|
+
drainQueue("cancelled", reasonCode);
|
|
352
|
+
setState({ phase: "closed", reason: reasonCode, detail: reason, pendingEvents: active ? 1 : 0 });
|
|
353
|
+
if (abortLifecycle && !lifecycle.signal.aborted)
|
|
354
|
+
lifecycle.abort(reason);
|
|
355
|
+
}
|
|
356
|
+
function drainQueue(status, reason) {
|
|
357
|
+
for (const pending of queue.splice(0))
|
|
358
|
+
pending.resolve(delivery(status, reason, state));
|
|
359
|
+
}
|
|
360
|
+
function setState(patch) {
|
|
361
|
+
state = freezeState({ ...state, ...patch });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function normalizeContext(context) {
|
|
365
|
+
if (!isResumeContextShape(context)) {
|
|
366
|
+
throw new HonuaRealtimeResumeError("invalid-checkpoint", "Realtime resume context kind or version is invalid.");
|
|
367
|
+
}
|
|
368
|
+
return Object.freeze({
|
|
369
|
+
kind: context.kind,
|
|
370
|
+
version: context.version,
|
|
371
|
+
sourceId: requiredText(String(context.sourceId), "sourceId"),
|
|
372
|
+
queryFingerprint: requiredText(context.queryFingerprint, "queryFingerprint"),
|
|
373
|
+
sourceVersion: requiredText(context.sourceVersion, "sourceVersion"),
|
|
374
|
+
schemaVersion: requiredText(context.schemaVersion, "schemaVersion"),
|
|
375
|
+
authorizationScopeFingerprint: requiredText(context.authorizationScopeFingerprint, "authorizationScopeFingerprint"),
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
function createCheckpoint(context, previous, position, eventId, maxRecentEventIds, now) {
|
|
379
|
+
const recentEventIds = eventId
|
|
380
|
+
? [...(previous?.recentEventIds ?? []), eventId].slice(-maxRecentEventIds)
|
|
381
|
+
: [...(previous?.recentEventIds ?? [])];
|
|
382
|
+
return freezeCheckpoint({
|
|
383
|
+
kind: "honua.realtime-checkpoint",
|
|
384
|
+
version: REALTIME_DURABLE_CHECKPOINT_VERSION,
|
|
385
|
+
context,
|
|
386
|
+
resume: Object.freeze({
|
|
387
|
+
...projectResumeCheckpoint(previous?.resume),
|
|
388
|
+
...projectResumeCheckpoint(position.resume),
|
|
389
|
+
sequence: position.sequence,
|
|
390
|
+
}),
|
|
391
|
+
recentEventIds: Object.freeze(recentEventIds),
|
|
392
|
+
savedAt: new Date(finiteNow(now)).toISOString(),
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
function captureRealtimeEvent(input) {
|
|
396
|
+
if (!isRecord(input))
|
|
397
|
+
return { conflict: "Realtime delivery must be an event object." };
|
|
398
|
+
if (!isSequencedEventType(input.type)) {
|
|
399
|
+
return { conflict: `Realtime event type "${String(input.type)}" is not a sequenced data event.` };
|
|
400
|
+
}
|
|
401
|
+
if (input.eventId !== undefined && typeof input.eventId !== "string") {
|
|
402
|
+
return { conflict: "Realtime eventId must be a string when provided." };
|
|
403
|
+
}
|
|
404
|
+
if (input.checkpoint !== undefined && !isRecord(input.checkpoint)) {
|
|
405
|
+
return { conflict: "Realtime event checkpoint must be an object when provided." };
|
|
406
|
+
}
|
|
407
|
+
if (input.receivedAt !== undefined && typeof input.receivedAt !== "number") {
|
|
408
|
+
return { conflict: "Realtime event receivedAt must be a number when provided." };
|
|
409
|
+
}
|
|
410
|
+
const checkpoint = input.checkpoint ? Object.freeze(projectResumeCheckpoint(input.checkpoint)) : undefined;
|
|
411
|
+
const base = {
|
|
412
|
+
...(input.eventId !== undefined ? { eventId: input.eventId } : {}),
|
|
413
|
+
...(input.cursor !== undefined ? { cursor: input.cursor } : {}),
|
|
414
|
+
...(input.watermark !== undefined ? { watermark: input.watermark } : {}),
|
|
415
|
+
...(input.timestamp !== undefined ? { timestamp: input.timestamp } : {}),
|
|
416
|
+
...(input.deltaToken !== undefined ? { deltaToken: input.deltaToken } : {}),
|
|
417
|
+
...(checkpoint ? { checkpoint } : {}),
|
|
418
|
+
...(input.sequence !== undefined ? { sequence: input.sequence } : {}),
|
|
419
|
+
...(input.receivedAt !== undefined ? { receivedAt: input.receivedAt } : {}),
|
|
420
|
+
};
|
|
421
|
+
let event;
|
|
422
|
+
switch (input.type) {
|
|
423
|
+
case "snapshot":
|
|
424
|
+
if (!Array.isArray(input.features))
|
|
425
|
+
return { conflict: "Realtime snapshot features must be an array." };
|
|
426
|
+
if (input.replace !== undefined && typeof input.replace !== "boolean") {
|
|
427
|
+
return { conflict: "Realtime snapshot replace must be a boolean when provided." };
|
|
428
|
+
}
|
|
429
|
+
event = Object.freeze({
|
|
430
|
+
...base,
|
|
431
|
+
type: "snapshot",
|
|
432
|
+
features: Object.freeze(input.features.slice()),
|
|
433
|
+
...(input.replace !== undefined ? { replace: input.replace } : {}),
|
|
434
|
+
});
|
|
435
|
+
break;
|
|
436
|
+
case "upsert":
|
|
437
|
+
if (!isRecord(input.feature))
|
|
438
|
+
return { conflict: "Realtime upsert feature must be an object." };
|
|
439
|
+
event = Object.freeze({
|
|
440
|
+
...base,
|
|
441
|
+
type: "upsert",
|
|
442
|
+
feature: input.feature,
|
|
443
|
+
});
|
|
444
|
+
break;
|
|
445
|
+
case "delete":
|
|
446
|
+
if (!isFeatureId(input.id))
|
|
447
|
+
return { conflict: "Realtime delete id must be a string or number." };
|
|
448
|
+
event = Object.freeze({
|
|
449
|
+
...base,
|
|
450
|
+
type: "delete",
|
|
451
|
+
id: input.id,
|
|
452
|
+
...(typeof input.sourceId === "string" ? { sourceId: input.sourceId } : {}),
|
|
453
|
+
...(typeof input.version === "number" ? { version: input.version } : {}),
|
|
454
|
+
...(typeof input.updatedAt === "string" ? { updatedAt: input.updatedAt } : {}),
|
|
455
|
+
...(typeof input.deletedAt === "number" ? { deletedAt: input.deletedAt } : {}),
|
|
456
|
+
});
|
|
457
|
+
break;
|
|
458
|
+
case "delta":
|
|
459
|
+
if (input.upserts !== undefined && !Array.isArray(input.upserts)) {
|
|
460
|
+
return { conflict: "Realtime delta upserts must be an array when provided." };
|
|
461
|
+
}
|
|
462
|
+
if (input.deletes !== undefined && !Array.isArray(input.deletes)) {
|
|
463
|
+
return { conflict: "Realtime delta deletes must be an array when provided." };
|
|
464
|
+
}
|
|
465
|
+
event = Object.freeze({
|
|
466
|
+
...base,
|
|
467
|
+
type: "delta",
|
|
468
|
+
...(input.upserts !== undefined
|
|
469
|
+
? {
|
|
470
|
+
upserts: Object.freeze(input.upserts.slice()),
|
|
471
|
+
}
|
|
472
|
+
: {}),
|
|
473
|
+
...(input.deletes !== undefined
|
|
474
|
+
? {
|
|
475
|
+
deletes: Object.freeze(input.deletes.slice()),
|
|
476
|
+
}
|
|
477
|
+
: {}),
|
|
478
|
+
});
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
const position = eventPosition(event);
|
|
482
|
+
if (position.conflict)
|
|
483
|
+
return { conflict: position.conflict, reason: "checkpoint-conflict" };
|
|
484
|
+
return Object.freeze({
|
|
485
|
+
event,
|
|
486
|
+
...(input.eventId !== undefined ? { eventId: input.eventId } : {}),
|
|
487
|
+
position,
|
|
488
|
+
replacementSnapshot: event.type === "snapshot" && event.replace !== false,
|
|
489
|
+
recoverySnapshot: false,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
function eventPosition(event) {
|
|
493
|
+
const pairs = [
|
|
494
|
+
["cursor", event.cursor, event.checkpoint?.cursor],
|
|
495
|
+
["watermark", event.watermark, event.checkpoint?.watermark],
|
|
496
|
+
["timestamp", event.timestamp, event.checkpoint?.timestamp],
|
|
497
|
+
["sequence", event.sequence, event.checkpoint?.sequence],
|
|
498
|
+
["deltaToken", event.deltaToken, event.checkpoint?.deltaToken],
|
|
499
|
+
];
|
|
500
|
+
for (const [name, direct, nested] of pairs) {
|
|
501
|
+
const expectedType = name === "sequence" ? "number" : "string";
|
|
502
|
+
const directInvalid = name === "sequence" ? typeof direct !== "number" : typeof direct !== "string";
|
|
503
|
+
if (direct !== undefined && directInvalid) {
|
|
504
|
+
return { resume: {}, conflict: `Event ${name} must be a ${expectedType}.` };
|
|
505
|
+
}
|
|
506
|
+
const nestedInvalid = name === "sequence" ? typeof nested !== "number" : typeof nested !== "string";
|
|
507
|
+
if (nested !== undefined && nestedInvalid) {
|
|
508
|
+
return { resume: {}, conflict: `Nested checkpoint ${name} must be a ${expectedType}.` };
|
|
509
|
+
}
|
|
510
|
+
if (direct !== undefined && nested !== undefined && direct !== nested) {
|
|
511
|
+
return { resume: {}, conflict: `Event ${name} conflicts with its nested checkpoint value.` };
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const sequence = event.sequence ?? event.checkpoint?.sequence;
|
|
515
|
+
return {
|
|
516
|
+
sequence,
|
|
517
|
+
resume: Object.freeze({
|
|
518
|
+
...projectResumeCheckpoint(event.checkpoint),
|
|
519
|
+
...(event.cursor !== undefined ? { cursor: event.cursor } : {}),
|
|
520
|
+
...(event.watermark !== undefined ? { watermark: event.watermark } : {}),
|
|
521
|
+
...(event.timestamp !== undefined ? { timestamp: event.timestamp } : {}),
|
|
522
|
+
...(event.deltaToken !== undefined ? { deltaToken: event.deltaToken } : {}),
|
|
523
|
+
...(sequence !== undefined ? { sequence } : {}),
|
|
524
|
+
}),
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
function sameSequenceCheckpointConflict(previous, incoming) {
|
|
528
|
+
for (const name of ["cursor", "deltaToken"]) {
|
|
529
|
+
const previousValue = previous?.[name];
|
|
530
|
+
const incomingValue = incoming[name];
|
|
531
|
+
if (previousValue !== undefined && incomingValue !== undefined && previousValue !== incomingValue) {
|
|
532
|
+
return `Realtime sequence ${String(incoming.sequence)} reuses ${name} with a conflicting value.`;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return undefined;
|
|
536
|
+
}
|
|
537
|
+
function projectResumeCheckpoint(value) {
|
|
538
|
+
if (!isRecord(value))
|
|
539
|
+
return {};
|
|
540
|
+
return {
|
|
541
|
+
...(value.cursor !== undefined ? { cursor: value.cursor } : {}),
|
|
542
|
+
...(value.watermark !== undefined ? { watermark: value.watermark } : {}),
|
|
543
|
+
...(value.timestamp !== undefined ? { timestamp: value.timestamp } : {}),
|
|
544
|
+
...(value.sequence !== undefined ? { sequence: value.sequence } : {}),
|
|
545
|
+
...(value.deltaToken !== undefined ? { deltaToken: value.deltaToken } : {}),
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
function isSequencedEventType(value) {
|
|
549
|
+
return value === "snapshot" || value === "upsert" || value === "delete" || value === "delta";
|
|
550
|
+
}
|
|
551
|
+
function isFeatureId(value) {
|
|
552
|
+
return typeof value === "string" || typeof value === "number";
|
|
553
|
+
}
|
|
554
|
+
function cloneCheckpoint(checkpoint, maxRecentEventIds) {
|
|
555
|
+
return freezeCheckpoint({
|
|
556
|
+
kind: checkpoint.kind,
|
|
557
|
+
version: checkpoint.version,
|
|
558
|
+
context: normalizeContext(checkpoint.context),
|
|
559
|
+
resume: Object.freeze(projectResumeCheckpoint(checkpoint.resume)),
|
|
560
|
+
recentEventIds: Object.freeze(checkpoint.recentEventIds.slice(-maxRecentEventIds)),
|
|
561
|
+
savedAt: checkpoint.savedAt,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
function freezeCheckpoint(checkpoint) {
|
|
565
|
+
return Object.freeze(checkpoint);
|
|
566
|
+
}
|
|
567
|
+
function freezeState(state) {
|
|
568
|
+
return Object.freeze(state);
|
|
569
|
+
}
|
|
570
|
+
function compatibility(compatibleValue, code, reason) {
|
|
571
|
+
return Object.freeze({ compatible: compatibleValue, code, reason });
|
|
572
|
+
}
|
|
573
|
+
function delivery(status, reason, state) {
|
|
574
|
+
return Object.freeze({
|
|
575
|
+
status,
|
|
576
|
+
...(reason ? { reason } : {}),
|
|
577
|
+
...(state.checkpoint ? { checkpoint: state.checkpoint } : {}),
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
function positiveInteger(value, name) {
|
|
581
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
582
|
+
throw new HonuaRealtimeResumeError("invalid-checkpoint", `${name} must be a safe integer greater than zero.`);
|
|
583
|
+
}
|
|
584
|
+
return value;
|
|
585
|
+
}
|
|
586
|
+
function requiredText(value, name) {
|
|
587
|
+
const normalized = value.trim();
|
|
588
|
+
if (!normalized)
|
|
589
|
+
throw new HonuaRealtimeResumeError("invalid-checkpoint", `${name} must be non-empty.`);
|
|
590
|
+
return normalized;
|
|
591
|
+
}
|
|
592
|
+
function isSequence(value) {
|
|
593
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
594
|
+
}
|
|
595
|
+
function isCheckpointShape(value) {
|
|
596
|
+
if (!isRecord(value))
|
|
597
|
+
return false;
|
|
598
|
+
if (value.kind !== "honua.realtime-checkpoint" || value.version !== REALTIME_DURABLE_CHECKPOINT_VERSION)
|
|
599
|
+
return false;
|
|
600
|
+
if (!isResumeContextShape(value.context) || !isRecord(value.resume) || !isSequence(value.resume.sequence))
|
|
601
|
+
return false;
|
|
602
|
+
for (const name of ["cursor", "watermark", "timestamp", "deltaToken"]) {
|
|
603
|
+
if (value.resume[name] !== undefined && typeof value.resume[name] !== "string")
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
if (!Array.isArray(value.recentEventIds) ||
|
|
607
|
+
value.recentEventIds.length > MAX_RECENT_EVENT_IDS ||
|
|
608
|
+
value.recentEventIds.some((entry) => typeof entry !== "string"))
|
|
609
|
+
return false;
|
|
610
|
+
return typeof value.savedAt === "string" && Number.isFinite(Date.parse(value.savedAt));
|
|
611
|
+
}
|
|
612
|
+
function isResumeContextShape(value) {
|
|
613
|
+
return (isRecord(value) &&
|
|
614
|
+
value.kind === "honua.realtime-resume-context" &&
|
|
615
|
+
value.version === 1 &&
|
|
616
|
+
typeof value.sourceId === "string" &&
|
|
617
|
+
typeof value.queryFingerprint === "string" &&
|
|
618
|
+
typeof value.sourceVersion === "string" &&
|
|
619
|
+
typeof value.schemaVersion === "string" &&
|
|
620
|
+
typeof value.authorizationScopeFingerprint === "string");
|
|
621
|
+
}
|
|
622
|
+
function isRecord(value) {
|
|
623
|
+
return typeof value === "object" && value !== null;
|
|
624
|
+
}
|
|
625
|
+
function finiteNow(value) {
|
|
626
|
+
if (!Number.isFinite(value))
|
|
627
|
+
throw new HonuaRealtimeResumeError("invalid-checkpoint", "now() must return a finite time.");
|
|
628
|
+
return value;
|
|
629
|
+
}
|
|
630
|
+
function errorMessage(value) {
|
|
631
|
+
return value instanceof Error ? value.message : String(value);
|
|
632
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { RealtimeFeatureEvent, RealtimeFeatureTransport, RealtimeSubscriptionRequest } from "./types.js";
|
|
2
|
+
export interface RealtimeServerSentEventSource {
|
|
3
|
+
readonly readyState?: number;
|
|
4
|
+
onopen: ((event: Event) => void) | null;
|
|
5
|
+
onmessage: ((event: MessageEvent<string>) => void) | null;
|
|
6
|
+
onerror: ((event: Event) => void) | null;
|
|
7
|
+
close(): void;
|
|
8
|
+
addEventListener?(type: string, listener: (event: MessageEvent<string>) => void): void;
|
|
9
|
+
removeEventListener?(type: string, listener: (event: MessageEvent<string>) => void): void;
|
|
10
|
+
}
|
|
11
|
+
export type RealtimeServerSentEventSourceFactory = (url: string, init?: EventSourceInit) => RealtimeServerSentEventSource;
|
|
12
|
+
export interface RealtimeServerSentEventsTransportOptions<TFeature = unknown> {
|
|
13
|
+
readonly url: string;
|
|
14
|
+
readonly eventSourceFactory?: RealtimeServerSentEventSourceFactory;
|
|
15
|
+
readonly withCredentials?: boolean;
|
|
16
|
+
readonly eventTypes?: ReadonlyArray<string>;
|
|
17
|
+
readonly encodeRequest?: (url: URL, request: RealtimeSubscriptionRequest) => void;
|
|
18
|
+
readonly decodeEvent?: (payload: unknown) => RealtimeFeatureEvent<TFeature>;
|
|
19
|
+
}
|
|
20
|
+
export declare function createRealtimeServerSentEventsTransport<TFeature = unknown>(options: RealtimeServerSentEventsTransportOptions<TFeature>): RealtimeFeatureTransport<TFeature>;
|
|
21
|
+
export declare function encodeDefaultRealtimeRequest(url: URL, request: RealtimeSubscriptionRequest): URL;
|
|
22
|
+
export declare function decodeRealtimeServerSentEvent<TFeature = unknown>(payload: unknown): RealtimeFeatureEvent<TFeature>;
|
|
23
|
+
//# sourceMappingURL=sse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../../../src/realtime/sse.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,oBAAoB,EACpB,wBAAwB,EAExB,2BAA2B,EAC5B,MAAM,YAAY,CAAC;AAGpB,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,MAAM,EAAE,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IACxC,SAAS,EAAE,CAAC,CAAC,KAAK,EAAE,YAAY,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1D,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IACzC,KAAK,IAAI,IAAI,CAAC;IACd,gBAAgB,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,YAAY,CAAC,MAAM,CAAC,KAAK,IAAI,GAAG,IAAI,CAAC;IACvF,mBAAmB,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,YAAY,CAAC,MAAM,CAAC,KAAK,IAAI,GAAG,IAAI,CAAC;CAC3F;AAED,MAAM,MAAM,oCAAoC,GAAG,CACjD,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE,eAAe,KACnB,6BAA6B,CAAC;AAEnC,MAAM,WAAW,wCAAwC,CAAC,QAAQ,GAAG,OAAO;IAC1E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,kBAAkB,CAAC,EAAE,oCAAoC,CAAC;IACnE,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IACnC,QAAQ,CAAC,UAAU,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC5C,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,2BAA2B,KAAK,IAAI,CAAC;IAClF,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,oBAAoB,CAAC,QAAQ,CAAC,CAAC;CAC7E;AAID,wBAAgB,uCAAuC,CAAC,QAAQ,GAAG,OAAO,EACxE,OAAO,EAAE,wCAAwC,CAAC,QAAQ,CAAC,GAC1D,wBAAwB,CAAC,QAAQ,CAAC,CAqGpC;AAED,wBAAgB,4BAA4B,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,2BAA2B,GAAG,GAAG,CAsBhG;AAED,wBAAgB,6BAA6B,CAAC,QAAQ,GAAG,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,oBAAoB,CAAC,QAAQ,CAAC,CAElH"}
|