@honua/react 0.1.12 → 0.1.14
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/columnar/apache-arrow.d.ts +17 -0
- package/columnar/apache-arrow.d.ts.map +1 -0
- package/columnar/apache-arrow.js +1036 -0
- package/columnar/batch-cache-conformance.d.ts +66 -0
- package/columnar/batch-cache-conformance.d.ts.map +1 -0
- package/columnar/batch-cache-conformance.js +317 -0
- package/columnar/batch-cache-indexeddb.d.ts +31 -0
- package/columnar/batch-cache-indexeddb.d.ts.map +1 -0
- package/columnar/batch-cache-indexeddb.js +148 -0
- package/columnar/batch-cache.d.ts +298 -0
- package/columnar/batch-cache.d.ts.map +1 -0
- package/columnar/batch-cache.js +757 -0
- package/columnar/geoarrow-aggregate.d.ts +178 -0
- package/columnar/geoarrow-aggregate.d.ts.map +1 -0
- package/columnar/geoarrow-aggregate.js +976 -0
- package/columnar/geoarrow-patch.d.ts +382 -0
- package/columnar/geoarrow-patch.d.ts.map +1 -0
- package/columnar/geoarrow-patch.js +1656 -0
- package/columnar/geoarrow-reproject.d.ts +22 -0
- package/columnar/geoarrow-reproject.d.ts.map +1 -0
- package/columnar/geoarrow-reproject.js +119 -0
- package/columnar/geoarrow-serialization.d.ts +68 -0
- package/columnar/geoarrow-serialization.d.ts.map +1 -0
- package/columnar/geoarrow-serialization.js +331 -0
- package/columnar/geoarrow-types.d.ts +271 -0
- package/columnar/geoarrow-types.d.ts.map +1 -0
- package/columnar/geoarrow-types.js +15 -0
- package/columnar/geoarrow.d.ts +52 -0
- package/columnar/geoarrow.d.ts.map +1 -0
- package/columnar/geoarrow.js +1469 -0
- package/columnar/index.d.ts +43 -0
- package/columnar/index.d.ts.map +1 -0
- package/columnar/index.js +29 -0
- package/columnar/operators.d.ts +70 -0
- package/columnar/operators.d.ts.map +1 -0
- package/columnar/operators.js +307 -0
- package/columnar/result.d.ts +369 -0
- package/columnar/result.d.ts.map +1 -0
- package/columnar/result.js +815 -0
- package/columnar/telemetry.d.ts +152 -0
- package/columnar/telemetry.d.ts.map +1 -0
- package/columnar/telemetry.js +265 -0
- package/columnar/transfer.d.ts +28 -0
- package/columnar/transfer.d.ts.map +1 -0
- package/columnar/transfer.js +669 -0
- package/columnar/types.d.ts +175 -0
- package/columnar/types.d.ts.map +1 -0
- package/columnar/types.js +22 -0
- package/columnar/worker.d.ts +157 -0
- package/columnar/worker.d.ts.map +1 -0
- package/columnar/worker.js +1059 -0
- package/connect-url-safety.d.ts +53 -0
- package/connect-url-safety.js +189 -0
- package/contract/pmtiles.d.ts +6 -5
- package/contract/pmtiles.d.ts.map +1 -1
- package/contract/pmtiles.js +30 -5
- package/contract/source.d.ts.map +1 -1
- package/contract/source.js +11 -8
- package/core/client.d.ts.map +1 -1
- package/core/client.js +87 -19
- package/core/deprecation-notice.d.ts +28 -0
- package/core/deprecation-notice.d.ts.map +1 -0
- package/core/deprecation-notice.js +22 -0
- package/core/esri-geojson.d.ts.map +1 -1
- package/core/esri-geojson.js +14 -8
- package/core/optional-grpc-peer.d.ts +15 -0
- package/core/optional-grpc-peer.d.ts.map +1 -0
- package/core/optional-grpc-peer.js +60 -0
- package/esri-compat/basemap-gallery.d.ts +2 -0
- package/esri-compat/basemap-gallery.d.ts.map +1 -1
- package/esri-compat/basemap-gallery.js +12 -0
- package/esri-compat/basemap.d.ts +4 -0
- package/esri-compat/basemap.d.ts.map +1 -1
- package/esri-compat/basemap.js +70 -0
- package/esri-compat/bookmarks.d.ts +2 -0
- package/esri-compat/bookmarks.d.ts.map +1 -1
- package/esri-compat/bookmarks.js +12 -0
- package/esri-compat/controls.d.ts +18 -0
- package/esri-compat/controls.d.ts.map +1 -1
- package/esri-compat/controls.js +120 -57
- package/esri-compat/directions.d.ts +4 -0
- package/esri-compat/directions.d.ts.map +1 -1
- package/esri-compat/directions.js +30 -20
- package/esri-compat/editor.d.ts +4 -0
- package/esri-compat/editor.d.ts.map +1 -1
- package/esri-compat/editor.js +16 -0
- package/esri-compat/esri-config.d.ts +2 -1
- package/esri-compat/esri-config.d.ts.map +1 -1
- package/esri-compat/feature-effect.d.ts +23 -0
- package/esri-compat/feature-effect.d.ts.map +1 -0
- package/esri-compat/feature-effect.js +12 -0
- package/esri-compat/feature-form.d.ts +6 -0
- package/esri-compat/feature-form.d.ts.map +1 -1
- package/esri-compat/feature-form.js +63 -0
- package/esri-compat/feature-layer.d.ts +21 -2
- package/esri-compat/feature-layer.d.ts.map +1 -1
- package/esri-compat/feature-layer.js +172 -1
- package/esri-compat/feature-table.d.ts +5 -0
- package/esri-compat/feature-table.d.ts.map +1 -1
- package/esri-compat/feature-table.js +23 -0
- package/esri-compat/identity-manager.d.ts +1 -1
- package/esri-compat/identity-manager.js +1 -1
- package/esri-compat/legend.d.ts +2 -0
- package/esri-compat/legend.d.ts.map +1 -1
- package/esri-compat/legend.js +98 -0
- package/esri-compat/locator.d.ts +1 -1
- package/esri-compat/locator.js +1 -1
- package/esri-compat/map-view-mount.d.ts +1 -0
- package/esri-compat/map-view-mount.d.ts.map +1 -1
- package/esri-compat/map-view-mount.js +46 -25
- package/esri-compat/map-view.d.ts +13 -0
- package/esri-compat/map-view.d.ts.map +1 -1
- package/esri-compat/map-view.js +94 -8
- package/esri-compat/map.d.ts +5 -1
- package/esri-compat/map.d.ts.map +1 -1
- package/esri-compat/map.js +10 -0
- package/esri-compat/measurement.d.ts +2 -0
- package/esri-compat/measurement.d.ts.map +1 -1
- package/esri-compat/measurement.js +14 -0
- package/esri-compat/point.d.ts +7 -0
- package/esri-compat/point.d.ts.map +1 -1
- package/esri-compat/point.js +33 -3
- package/esri-compat/popup.d.ts +2 -0
- package/esri-compat/popup.d.ts.map +1 -1
- package/esri-compat/popup.js +12 -0
- package/esri-compat/portal.d.ts +1 -1
- package/esri-compat/portal.js +1 -1
- package/esri-compat/print.d.ts +3 -0
- package/esri-compat/print.d.ts.map +1 -1
- package/esri-compat/print.js +15 -0
- package/esri-compat/search.d.ts +4 -0
- package/esri-compat/search.d.ts.map +1 -1
- package/esri-compat/search.js +25 -28
- package/esri-compat/sketch.d.ts +2 -0
- package/esri-compat/sketch.d.ts.map +1 -1
- package/esri-compat/sketch.js +12 -0
- package/esri-compat/widget-host.d.ts +46 -17
- package/esri-compat/widget-host.d.ts.map +1 -1
- package/esri-compat/widget-host.js +84 -20
- package/esri-compat/widget-shell-hosts.d.ts +61 -0
- package/esri-compat/widget-shell-hosts.d.ts.map +1 -0
- package/esri-compat/widget-shell-hosts.js +76 -0
- package/geocoding/index.d.ts +114 -0
- package/geocoding/index.d.ts.map +1 -0
- package/geocoding/index.js +293 -0
- package/geocoding/provider-http.d.ts +37 -0
- package/geocoding/provider-http.d.ts.map +1 -0
- package/geocoding/provider-http.js +93 -0
- package/geocoding/provider.d.ts +145 -0
- package/geocoding/provider.d.ts.map +1 -0
- package/geocoding/provider.js +41 -0
- package/geocoding/providers/honua.d.ts +28 -0
- package/geocoding/providers/honua.d.ts.map +1 -0
- package/geocoding/providers/honua.js +68 -0
- package/geocoding/providers/nominatim.d.ts +47 -0
- package/geocoding/providers/nominatim.d.ts.map +1 -0
- package/geocoding/providers/nominatim.js +105 -0
- package/geocoding/providers/pelias.d.ts +40 -0
- package/geocoding/providers/pelias.d.ts.map +1 -0
- package/geocoding/providers/pelias.js +115 -0
- package/geocoding/providers/photon.d.ts +39 -0
- package/geocoding/providers/photon.d.ts.map +1 -0
- package/geocoding/providers/photon.js +107 -0
- package/geometry/convert.d.ts +13 -0
- package/geometry/convert.d.ts.map +1 -0
- package/geometry/convert.js +12 -0
- package/geometry/index.d.ts +17 -0
- package/geometry/index.d.ts.map +1 -0
- package/geometry/index.js +15 -0
- package/geometry/internal.d.ts +25 -0
- package/geometry/internal.d.ts.map +1 -0
- package/geometry/internal.js +40 -0
- package/geometry/ops.d.ts +71 -0
- package/geometry/ops.d.ts.map +1 -0
- package/geometry/ops.js +147 -0
- package/geometry/project.d.ts +40 -0
- package/geometry/project.d.ts.map +1 -0
- package/geometry/project.js +88 -0
- package/geometry/types.d.ts +36 -0
- package/geometry/types.d.ts.map +1 -0
- package/geometry/types.js +12 -0
- package/kernel/renderer.d.ts +82 -0
- package/kernel/renderer.js +1 -0
- package/offline/digest.d.ts +15 -0
- package/offline/digest.js +58 -0
- package/offline/quota.d.ts +161 -0
- package/offline/quota.js +370 -0
- package/offline/types.d.ts +349 -0
- package/offline/types.js +75 -0
- package/package.json +14 -2
- package/realtime/odata-delta.d.ts +1 -1
- package/realtime/odata-delta.d.ts.map +1 -1
- package/realtime/odata-delta.js +38 -10
- package/widget-capabilities.d.ts +67 -0
- package/widget-capabilities.js +152 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real browser storage-quota interrogation for offline-region admission.
|
|
3
|
+
*
|
|
4
|
+
* `logicalQuotaBytes` is an honest accounting of declared payload lengths, but
|
|
5
|
+
* it has no relationship to the space a browser will actually grant. This module
|
|
6
|
+
* closes that gap in the only way the platform supports: it reads
|
|
7
|
+
* `navigator.storage.estimate()` through an injectable interface, derives a
|
|
8
|
+
* conservative logical budget with an explicit reserve, and reports an explicit
|
|
9
|
+
* `unavailable` result rather than inventing a number when the platform cannot
|
|
10
|
+
* estimate.
|
|
11
|
+
*
|
|
12
|
+
* The estimate is deliberately imprecise and origin-scoped, and the accounting
|
|
13
|
+
* unit stays logical payload bytes: physical occupancy, deduplication, and index
|
|
14
|
+
* overhead are outside the contract and are exactly why a reserve exists. The
|
|
15
|
+
* derived budget is advisory, never a guarantee, so a device can still refuse a
|
|
16
|
+
* write that the budget admitted — which is why storage-pressure failures are
|
|
17
|
+
* classified here too.
|
|
18
|
+
*
|
|
19
|
+
* Nothing in this module performs network I/O, mutates the store, or reads or
|
|
20
|
+
* returns a credential, origin secret, or request URL. Persistence is never
|
|
21
|
+
* requested implicitly: `requestOfflinePersistentStorage()` is the only path to
|
|
22
|
+
* `navigator.storage.persist()` and it exists solely because the caller asked.
|
|
23
|
+
*/
|
|
24
|
+
/** Stable discriminator and version for storage-budget probe results. */
|
|
25
|
+
export declare const HONUA_OFFLINE_STORAGE_BUDGET_KIND: "honua.offline-storage-budget";
|
|
26
|
+
export declare const HONUA_OFFLINE_STORAGE_BUDGET_VERSION: "1.0";
|
|
27
|
+
/** Stable discriminator and version for explicit persistence-request results. */
|
|
28
|
+
export declare const HONUA_OFFLINE_STORAGE_PERSISTENCE_KIND: "honua.offline-storage-persistence";
|
|
29
|
+
export declare const HONUA_OFFLINE_STORAGE_PERSISTENCE_VERSION: "1.0";
|
|
30
|
+
/** Fraction of the remaining estimate held back from the derived logical budget. */
|
|
31
|
+
export declare const DEFAULT_OFFLINE_STORAGE_HEADROOM_RATIO = 0.2;
|
|
32
|
+
/** Floor for the reserve, so a large-quota origin still keeps real headroom. */
|
|
33
|
+
export declare const DEFAULT_OFFLINE_STORAGE_MIN_RESERVE_BYTES: number;
|
|
34
|
+
/**
|
|
35
|
+
* Whether the origin's storage survives eviction pressure. `unknown` is a real
|
|
36
|
+
* answer: a platform without `StorageManager.persisted()` has not told us.
|
|
37
|
+
*/
|
|
38
|
+
export type OfflineStoragePersistence = "persisted" | "best-effort" | "unknown";
|
|
39
|
+
/** Why no budget could be derived. Never accompanied by a fabricated number. */
|
|
40
|
+
export type OfflineStorageBudgetUnavailableReason = "storage-manager-unavailable" | "estimate-unsupported" | "estimate-failed" | "estimate-incomplete";
|
|
41
|
+
/**
|
|
42
|
+
* Injectable subset of the platform `StorageManager`.
|
|
43
|
+
*
|
|
44
|
+
* Every member is optional so a partial host implementation is describable
|
|
45
|
+
* rather than assumed; the probe degrades to an explicit `unavailable` result
|
|
46
|
+
* instead of throwing. A real `navigator.storage` satisfies this structurally.
|
|
47
|
+
*/
|
|
48
|
+
export interface OfflineStorageManagerLike {
|
|
49
|
+
estimate?: () => Promise<{
|
|
50
|
+
readonly quota?: number;
|
|
51
|
+
readonly usage?: number;
|
|
52
|
+
} | undefined>;
|
|
53
|
+
persisted?: () => Promise<boolean>;
|
|
54
|
+
persist?: () => Promise<boolean>;
|
|
55
|
+
}
|
|
56
|
+
/** A derived, advisory logical budget backed by a real platform estimate. */
|
|
57
|
+
export interface OfflineStorageBudgetAvailableV1 {
|
|
58
|
+
readonly kind: typeof HONUA_OFFLINE_STORAGE_BUDGET_KIND;
|
|
59
|
+
readonly version: typeof HONUA_OFFLINE_STORAGE_BUDGET_VERSION;
|
|
60
|
+
readonly status: "available";
|
|
61
|
+
readonly observedAt: string;
|
|
62
|
+
/** Platform-reported origin quota. Imprecise and subject to change. */
|
|
63
|
+
readonly quotaBytes: number;
|
|
64
|
+
readonly usageBytes: number;
|
|
65
|
+
/** `max(0, quota - usage)`; the hard upper bound on any derived budget. */
|
|
66
|
+
readonly remainingBytes: number;
|
|
67
|
+
readonly reserveBytes: number;
|
|
68
|
+
readonly headroomRatio: number;
|
|
69
|
+
/** Pass to `logicalQuotaBytes`. Never exceeds `remainingBytes`. */
|
|
70
|
+
readonly logicalBudgetBytes: number;
|
|
71
|
+
readonly persistence: OfflineStoragePersistence;
|
|
72
|
+
}
|
|
73
|
+
/** An explicit refusal to estimate. Carries no budget of any kind. */
|
|
74
|
+
export interface OfflineStorageBudgetUnavailableV1 {
|
|
75
|
+
readonly kind: typeof HONUA_OFFLINE_STORAGE_BUDGET_KIND;
|
|
76
|
+
readonly version: typeof HONUA_OFFLINE_STORAGE_BUDGET_VERSION;
|
|
77
|
+
readonly status: "unavailable";
|
|
78
|
+
readonly observedAt: string;
|
|
79
|
+
readonly reason: OfflineStorageBudgetUnavailableReason;
|
|
80
|
+
readonly persistence: OfflineStoragePersistence;
|
|
81
|
+
}
|
|
82
|
+
export type OfflineStorageBudgetV1 = OfflineStorageBudgetAvailableV1 | OfflineStorageBudgetUnavailableV1;
|
|
83
|
+
export interface ProbeOfflineStorageBudgetOptions {
|
|
84
|
+
/** Defaults to `navigator.storage`. Inject for tests and non-browser hosts. */
|
|
85
|
+
readonly storage?: OfflineStorageManagerLike;
|
|
86
|
+
/** Fraction of remaining space held in reserve. `0` to `1`; defaults to `0.2`. */
|
|
87
|
+
readonly headroomRatio?: number;
|
|
88
|
+
/** Reserve floor in bytes; defaults to 16 MiB. Clamped by remaining space. */
|
|
89
|
+
readonly minimumReserveBytes?: number;
|
|
90
|
+
/** Explicit clock input so a persisted probe result stays reproducible. */
|
|
91
|
+
readonly now?: Date;
|
|
92
|
+
}
|
|
93
|
+
export type OfflineStoragePersistenceRequestStatus = "granted" | "denied" | "unavailable";
|
|
94
|
+
export type OfflineStoragePersistenceUnavailableReason = "storage-manager-unavailable" | "persist-unsupported" | "persist-failed";
|
|
95
|
+
/** Result of an explicit, caller-initiated persistence request. */
|
|
96
|
+
export interface OfflineStoragePersistenceRequestV1 {
|
|
97
|
+
readonly kind: typeof HONUA_OFFLINE_STORAGE_PERSISTENCE_KIND;
|
|
98
|
+
readonly version: typeof HONUA_OFFLINE_STORAGE_PERSISTENCE_VERSION;
|
|
99
|
+
readonly status: OfflineStoragePersistenceRequestStatus;
|
|
100
|
+
readonly persistence: OfflineStoragePersistence;
|
|
101
|
+
readonly reason?: OfflineStoragePersistenceUnavailableReason;
|
|
102
|
+
}
|
|
103
|
+
export interface RequestOfflinePersistentStorageOptions {
|
|
104
|
+
/** Defaults to `navigator.storage`. Inject for tests and non-browser hosts. */
|
|
105
|
+
readonly storage?: OfflineStorageManagerLike;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Read the origin's real storage estimate and derive a conservative logical
|
|
109
|
+
* budget, or report explicitly that the platform cannot estimate.
|
|
110
|
+
*
|
|
111
|
+
* Performs no network I/O, writes nothing, and never calls
|
|
112
|
+
* `StorageManager.persist()`. `persisted()` is read when the platform offers it
|
|
113
|
+
* because persisted state changes the eviction risk of every cached region; a
|
|
114
|
+
* platform that does not offer it yields `persistence: "unknown"` rather than an
|
|
115
|
+
* assumption.
|
|
116
|
+
*
|
|
117
|
+
* Derivation is deterministic integer arithmetic over the reported values:
|
|
118
|
+
*
|
|
119
|
+
* ```text
|
|
120
|
+
* remaining = max(0, quota - usage)
|
|
121
|
+
* reserve = min(remaining, max(minimumReserveBytes, floor(remaining * headroomRatio)))
|
|
122
|
+
* budget = remaining - reserve
|
|
123
|
+
* ```
|
|
124
|
+
*
|
|
125
|
+
* so the budget can never exceed the platform-reported remaining quota, and a
|
|
126
|
+
* nearly full origin yields `0` instead of a number that cannot be honoured.
|
|
127
|
+
*/
|
|
128
|
+
export declare function probeOfflineStorageBudget(options?: ProbeOfflineStorageBudgetOptions): Promise<OfflineStorageBudgetV1>;
|
|
129
|
+
/**
|
|
130
|
+
* Ask the browser to make this origin's storage persistent.
|
|
131
|
+
*
|
|
132
|
+
* This is the only call site of `StorageManager.persist()` in the SDK and it
|
|
133
|
+
* runs only because an application called it. It is never invoked by
|
|
134
|
+
* `downloadOfflineRegion`, by the probe, or by any store adapter: the request
|
|
135
|
+
* can show a permission prompt, and a download is not consent to ask.
|
|
136
|
+
*/
|
|
137
|
+
export declare function requestOfflinePersistentStorage(options?: RequestOfflinePersistentStorageOptions): Promise<OfflineStoragePersistenceRequestV1>;
|
|
138
|
+
/**
|
|
139
|
+
* Report whether a thrown value is, or wraps, a platform storage-pressure
|
|
140
|
+
* failure — the device refusing a write for lack of space.
|
|
141
|
+
*
|
|
142
|
+
* Recognizes the `QuotaExceededError` `DOMException` every engine raises, the
|
|
143
|
+
* Firefox `NS_ERROR_DOM_QUOTA_REACHED` name, and the legacy numeric
|
|
144
|
+
* `DOMException` codes, then walks a bounded `cause` and `AggregateError` graph
|
|
145
|
+
* so a store adapter that wrapped the failure is still classified correctly.
|
|
146
|
+
* Bounded and cycle-safe: it inspects at most 32 nodes and visits none twice.
|
|
147
|
+
*
|
|
148
|
+
* Exported because a host-supplied `OfflineRegionStore` must classify the same
|
|
149
|
+
* condition the same way; without a shared predicate an adapter's wrapper would
|
|
150
|
+
* silently downgrade a full device to an internal store failure.
|
|
151
|
+
*/
|
|
152
|
+
export declare function isStorageQuotaPressureError(error: unknown): boolean;
|
|
153
|
+
/**
|
|
154
|
+
* Validate and take ownership of a caller-supplied budget envelope.
|
|
155
|
+
*
|
|
156
|
+
* Internal to the offline area: the diagnostic accepts a budget produced by
|
|
157
|
+
* `probeOfflineStorageBudget()`, and a public entry point cannot assume it was
|
|
158
|
+
* not hand-built.
|
|
159
|
+
*/
|
|
160
|
+
export declare function captureOfflineStorageBudget(value: unknown, path: string): OfflineStorageBudgetV1;
|
|
161
|
+
//# sourceMappingURL=quota.d.ts.map
|
package/offline/quota.js
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { HonuaOfflineRegionError } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Real browser storage-quota interrogation for offline-region admission.
|
|
4
|
+
*
|
|
5
|
+
* `logicalQuotaBytes` is an honest accounting of declared payload lengths, but
|
|
6
|
+
* it has no relationship to the space a browser will actually grant. This module
|
|
7
|
+
* closes that gap in the only way the platform supports: it reads
|
|
8
|
+
* `navigator.storage.estimate()` through an injectable interface, derives a
|
|
9
|
+
* conservative logical budget with an explicit reserve, and reports an explicit
|
|
10
|
+
* `unavailable` result rather than inventing a number when the platform cannot
|
|
11
|
+
* estimate.
|
|
12
|
+
*
|
|
13
|
+
* The estimate is deliberately imprecise and origin-scoped, and the accounting
|
|
14
|
+
* unit stays logical payload bytes: physical occupancy, deduplication, and index
|
|
15
|
+
* overhead are outside the contract and are exactly why a reserve exists. The
|
|
16
|
+
* derived budget is advisory, never a guarantee, so a device can still refuse a
|
|
17
|
+
* write that the budget admitted — which is why storage-pressure failures are
|
|
18
|
+
* classified here too.
|
|
19
|
+
*
|
|
20
|
+
* Nothing in this module performs network I/O, mutates the store, or reads or
|
|
21
|
+
* returns a credential, origin secret, or request URL. Persistence is never
|
|
22
|
+
* requested implicitly: `requestOfflinePersistentStorage()` is the only path to
|
|
23
|
+
* `navigator.storage.persist()` and it exists solely because the caller asked.
|
|
24
|
+
*/
|
|
25
|
+
/** Stable discriminator and version for storage-budget probe results. */
|
|
26
|
+
export const HONUA_OFFLINE_STORAGE_BUDGET_KIND = "honua.offline-storage-budget";
|
|
27
|
+
export const HONUA_OFFLINE_STORAGE_BUDGET_VERSION = "1.0";
|
|
28
|
+
/** Stable discriminator and version for explicit persistence-request results. */
|
|
29
|
+
export const HONUA_OFFLINE_STORAGE_PERSISTENCE_KIND = "honua.offline-storage-persistence";
|
|
30
|
+
export const HONUA_OFFLINE_STORAGE_PERSISTENCE_VERSION = "1.0";
|
|
31
|
+
/** Fraction of the remaining estimate held back from the derived logical budget. */
|
|
32
|
+
export const DEFAULT_OFFLINE_STORAGE_HEADROOM_RATIO = 0.2;
|
|
33
|
+
/** Floor for the reserve, so a large-quota origin still keeps real headroom. */
|
|
34
|
+
export const DEFAULT_OFFLINE_STORAGE_MIN_RESERVE_BYTES = 16 * 1024 * 1024;
|
|
35
|
+
const PROBE_OPTION_KEYS = new Set(["storage", "headroomRatio", "minimumReserveBytes", "now"]);
|
|
36
|
+
const PERSIST_OPTION_KEYS = new Set(["storage"]);
|
|
37
|
+
const BUDGET_KEYS = new Set([
|
|
38
|
+
"kind",
|
|
39
|
+
"version",
|
|
40
|
+
"status",
|
|
41
|
+
"observedAt",
|
|
42
|
+
"quotaBytes",
|
|
43
|
+
"usageBytes",
|
|
44
|
+
"remainingBytes",
|
|
45
|
+
"reserveBytes",
|
|
46
|
+
"headroomRatio",
|
|
47
|
+
"logicalBudgetBytes",
|
|
48
|
+
"persistence",
|
|
49
|
+
"reason",
|
|
50
|
+
]);
|
|
51
|
+
const PERSISTENCE_VALUES = new Set(["persisted", "best-effort", "unknown"]);
|
|
52
|
+
const UNAVAILABLE_REASONS = new Set([
|
|
53
|
+
"storage-manager-unavailable",
|
|
54
|
+
"estimate-unsupported",
|
|
55
|
+
"estimate-failed",
|
|
56
|
+
"estimate-incomplete",
|
|
57
|
+
]);
|
|
58
|
+
/**
|
|
59
|
+
* Read the origin's real storage estimate and derive a conservative logical
|
|
60
|
+
* budget, or report explicitly that the platform cannot estimate.
|
|
61
|
+
*
|
|
62
|
+
* Performs no network I/O, writes nothing, and never calls
|
|
63
|
+
* `StorageManager.persist()`. `persisted()` is read when the platform offers it
|
|
64
|
+
* because persisted state changes the eviction risk of every cached region; a
|
|
65
|
+
* platform that does not offer it yields `persistence: "unknown"` rather than an
|
|
66
|
+
* assumption.
|
|
67
|
+
*
|
|
68
|
+
* Derivation is deterministic integer arithmetic over the reported values:
|
|
69
|
+
*
|
|
70
|
+
* ```text
|
|
71
|
+
* remaining = max(0, quota - usage)
|
|
72
|
+
* reserve = min(remaining, max(minimumReserveBytes, floor(remaining * headroomRatio)))
|
|
73
|
+
* budget = remaining - reserve
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* so the budget can never exceed the platform-reported remaining quota, and a
|
|
77
|
+
* nearly full origin yields `0` instead of a number that cannot be honoured.
|
|
78
|
+
*/
|
|
79
|
+
export async function probeOfflineStorageBudget(options = {}) {
|
|
80
|
+
const record = plainRecord(options, "options");
|
|
81
|
+
allowedKeys(record, PROBE_OPTION_KEYS, "options");
|
|
82
|
+
const headroomRatio = normalizeRatio(record.headroomRatio, "options.headroomRatio");
|
|
83
|
+
const minimumReserveBytes = record.minimumReserveBytes === undefined
|
|
84
|
+
? DEFAULT_OFFLINE_STORAGE_MIN_RESERVE_BYTES
|
|
85
|
+
: nonNegativeInteger(record.minimumReserveBytes, "options.minimumReserveBytes");
|
|
86
|
+
const nowMs = record.now === undefined ? Date.now() : validDate(record.now, "options.now");
|
|
87
|
+
const observedAt = new Date(nowMs).toISOString();
|
|
88
|
+
const storage = resolveStorageManager(record.storage, "options.storage");
|
|
89
|
+
if (!storage)
|
|
90
|
+
return unavailable(observedAt, "storage-manager-unavailable", "unknown");
|
|
91
|
+
const persistence = await readPersistence(storage);
|
|
92
|
+
if (typeof storage.estimate !== "function")
|
|
93
|
+
return unavailable(observedAt, "estimate-unsupported", persistence);
|
|
94
|
+
let estimate;
|
|
95
|
+
try {
|
|
96
|
+
estimate = await storage.estimate();
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// An estimate that failed is not a budget of zero; it is no budget at all.
|
|
100
|
+
return unavailable(observedAt, "estimate-failed", persistence);
|
|
101
|
+
}
|
|
102
|
+
if (typeof estimate !== "object" || estimate === null) {
|
|
103
|
+
return unavailable(observedAt, "estimate-incomplete", persistence);
|
|
104
|
+
}
|
|
105
|
+
const quotaBytes = readByteCount(estimate.quota);
|
|
106
|
+
const usageBytes = readByteCount(estimate.usage);
|
|
107
|
+
if (quotaBytes === undefined || usageBytes === undefined) {
|
|
108
|
+
return unavailable(observedAt, "estimate-incomplete", persistence);
|
|
109
|
+
}
|
|
110
|
+
const remainingBytes = Math.max(0, quotaBytes - usageBytes);
|
|
111
|
+
const reserveBytes = Math.min(remainingBytes, Math.max(minimumReserveBytes, Math.floor(remainingBytes * headroomRatio)));
|
|
112
|
+
return deepFreeze({
|
|
113
|
+
kind: HONUA_OFFLINE_STORAGE_BUDGET_KIND,
|
|
114
|
+
version: HONUA_OFFLINE_STORAGE_BUDGET_VERSION,
|
|
115
|
+
status: "available",
|
|
116
|
+
observedAt,
|
|
117
|
+
quotaBytes,
|
|
118
|
+
usageBytes,
|
|
119
|
+
remainingBytes,
|
|
120
|
+
reserveBytes,
|
|
121
|
+
headroomRatio,
|
|
122
|
+
logicalBudgetBytes: remainingBytes - reserveBytes,
|
|
123
|
+
persistence,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Ask the browser to make this origin's storage persistent.
|
|
128
|
+
*
|
|
129
|
+
* This is the only call site of `StorageManager.persist()` in the SDK and it
|
|
130
|
+
* runs only because an application called it. It is never invoked by
|
|
131
|
+
* `downloadOfflineRegion`, by the probe, or by any store adapter: the request
|
|
132
|
+
* can show a permission prompt, and a download is not consent to ask.
|
|
133
|
+
*/
|
|
134
|
+
export async function requestOfflinePersistentStorage(options = {}) {
|
|
135
|
+
const record = plainRecord(options, "options");
|
|
136
|
+
allowedKeys(record, PERSIST_OPTION_KEYS, "options");
|
|
137
|
+
const storage = resolveStorageManager(record.storage, "options.storage");
|
|
138
|
+
if (!storage) {
|
|
139
|
+
return persistenceResult("unavailable", "unknown", "storage-manager-unavailable");
|
|
140
|
+
}
|
|
141
|
+
if (typeof storage.persist !== "function") {
|
|
142
|
+
return persistenceResult("unavailable", await readPersistence(storage), "persist-unsupported");
|
|
143
|
+
}
|
|
144
|
+
let granted;
|
|
145
|
+
try {
|
|
146
|
+
granted = (await storage.persist()) === true;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return persistenceResult("unavailable", await readPersistence(storage), "persist-failed");
|
|
150
|
+
}
|
|
151
|
+
return persistenceResult(granted ? "granted" : "denied", granted ? "persisted" : await readPersistence(storage));
|
|
152
|
+
}
|
|
153
|
+
const QUOTA_PRESSURE_NAMES = new Set(["QuotaExceededError", "NS_ERROR_DOM_QUOTA_REACHED", "QUOTA_EXCEEDED_ERR"]);
|
|
154
|
+
const QUOTA_PRESSURE_LEGACY_CODES = new Set([22, 1014]);
|
|
155
|
+
const MAX_CAUSE_NODES = 32;
|
|
156
|
+
/**
|
|
157
|
+
* Report whether a thrown value is, or wraps, a platform storage-pressure
|
|
158
|
+
* failure — the device refusing a write for lack of space.
|
|
159
|
+
*
|
|
160
|
+
* Recognizes the `QuotaExceededError` `DOMException` every engine raises, the
|
|
161
|
+
* Firefox `NS_ERROR_DOM_QUOTA_REACHED` name, and the legacy numeric
|
|
162
|
+
* `DOMException` codes, then walks a bounded `cause` and `AggregateError` graph
|
|
163
|
+
* so a store adapter that wrapped the failure is still classified correctly.
|
|
164
|
+
* Bounded and cycle-safe: it inspects at most 32 nodes and visits none twice.
|
|
165
|
+
*
|
|
166
|
+
* Exported because a host-supplied `OfflineRegionStore` must classify the same
|
|
167
|
+
* condition the same way; without a shared predicate an adapter's wrapper would
|
|
168
|
+
* silently downgrade a full device to an internal store failure.
|
|
169
|
+
*/
|
|
170
|
+
export function isStorageQuotaPressureError(error) {
|
|
171
|
+
const pending = [error];
|
|
172
|
+
const seen = new Set();
|
|
173
|
+
let inspected = 0;
|
|
174
|
+
while (pending.length > 0 && inspected < MAX_CAUSE_NODES) {
|
|
175
|
+
const current = pending.shift();
|
|
176
|
+
if (typeof current !== "object" || current === null || seen.has(current))
|
|
177
|
+
continue;
|
|
178
|
+
seen.add(current);
|
|
179
|
+
inspected += 1;
|
|
180
|
+
if (isQuotaPressureShape(current))
|
|
181
|
+
return true;
|
|
182
|
+
const cause = readOwnOrInherited(current, "cause");
|
|
183
|
+
if (cause !== undefined)
|
|
184
|
+
pending.push(cause);
|
|
185
|
+
const errors = readOwnOrInherited(current, "errors");
|
|
186
|
+
if (Array.isArray(errors)) {
|
|
187
|
+
for (const nested of errors.slice(0, MAX_CAUSE_NODES))
|
|
188
|
+
pending.push(nested);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
function isQuotaPressureShape(value) {
|
|
194
|
+
const name = readOwnOrInherited(value, "name");
|
|
195
|
+
if (typeof name === "string" && QUOTA_PRESSURE_NAMES.has(name))
|
|
196
|
+
return true;
|
|
197
|
+
const code = readOwnOrInherited(value, "code");
|
|
198
|
+
if (typeof code === "string" && QUOTA_PRESSURE_NAMES.has(code))
|
|
199
|
+
return true;
|
|
200
|
+
// Legacy numeric DOMException codes only count on a DOMException-shaped
|
|
201
|
+
// value; a bare `{ code: 22 }` from an unrelated API is not storage pressure.
|
|
202
|
+
return typeof code === "number" && typeof name === "string" && QUOTA_PRESSURE_LEGACY_CODES.has(code);
|
|
203
|
+
}
|
|
204
|
+
function readOwnOrInherited(value, key) {
|
|
205
|
+
try {
|
|
206
|
+
return value[key];
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// A hostile getter must not be able to break failure classification.
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Validate and take ownership of a caller-supplied budget envelope.
|
|
215
|
+
*
|
|
216
|
+
* Internal to the offline area: the diagnostic accepts a budget produced by
|
|
217
|
+
* `probeOfflineStorageBudget()`, and a public entry point cannot assume it was
|
|
218
|
+
* not hand-built.
|
|
219
|
+
*/
|
|
220
|
+
export function captureOfflineStorageBudget(value, path) {
|
|
221
|
+
const record = plainRecord(value, path);
|
|
222
|
+
allowedKeys(record, BUDGET_KEYS, path);
|
|
223
|
+
if (record.kind !== HONUA_OFFLINE_STORAGE_BUDGET_KIND)
|
|
224
|
+
invalid(`${path}.kind is unsupported.`, `${path}.kind`);
|
|
225
|
+
if (record.version !== HONUA_OFFLINE_STORAGE_BUDGET_VERSION) {
|
|
226
|
+
invalid(`${path}.version is unsupported.`, `${path}.version`);
|
|
227
|
+
}
|
|
228
|
+
const observedAt = normalizedTimestamp(record.observedAt, `${path}.observedAt`);
|
|
229
|
+
const persistence = memberOf(record.persistence, PERSISTENCE_VALUES, `${path}.persistence`);
|
|
230
|
+
if (record.status === "unavailable") {
|
|
231
|
+
return unavailable(observedAt, memberOf(record.reason, UNAVAILABLE_REASONS, `${path}.reason`), persistence);
|
|
232
|
+
}
|
|
233
|
+
if (record.status !== "available")
|
|
234
|
+
invalid(`${path}.status is invalid.`, `${path}.status`);
|
|
235
|
+
if (record.reason !== undefined)
|
|
236
|
+
invalid(`${path}.reason is not supported.`, `${path}.reason`);
|
|
237
|
+
const quotaBytes = nonNegativeInteger(record.quotaBytes, `${path}.quotaBytes`);
|
|
238
|
+
const usageBytes = nonNegativeInteger(record.usageBytes, `${path}.usageBytes`);
|
|
239
|
+
const remainingBytes = nonNegativeInteger(record.remainingBytes, `${path}.remainingBytes`);
|
|
240
|
+
const reserveBytes = nonNegativeInteger(record.reserveBytes, `${path}.reserveBytes`);
|
|
241
|
+
const logicalBudgetBytes = nonNegativeInteger(record.logicalBudgetBytes, `${path}.logicalBudgetBytes`);
|
|
242
|
+
const headroomRatio = normalizeRatio(record.headroomRatio, `${path}.headroomRatio`);
|
|
243
|
+
// The arithmetic identities are part of the contract, so a forged envelope
|
|
244
|
+
// cannot claim a budget larger than the remaining space it reports.
|
|
245
|
+
if (remainingBytes !== Math.max(0, quotaBytes - usageBytes) || reserveBytes > remainingBytes) {
|
|
246
|
+
invalid(`${path}.remainingBytes does not agree with the reported estimate.`, `${path}.remainingBytes`);
|
|
247
|
+
}
|
|
248
|
+
if (logicalBudgetBytes !== remainingBytes - reserveBytes) {
|
|
249
|
+
invalid(`${path}.logicalBudgetBytes does not agree with the reported reserve.`, `${path}.logicalBudgetBytes`);
|
|
250
|
+
}
|
|
251
|
+
return deepFreeze({
|
|
252
|
+
kind: HONUA_OFFLINE_STORAGE_BUDGET_KIND,
|
|
253
|
+
version: HONUA_OFFLINE_STORAGE_BUDGET_VERSION,
|
|
254
|
+
status: "available",
|
|
255
|
+
observedAt,
|
|
256
|
+
quotaBytes,
|
|
257
|
+
usageBytes,
|
|
258
|
+
remainingBytes,
|
|
259
|
+
reserveBytes,
|
|
260
|
+
headroomRatio,
|
|
261
|
+
logicalBudgetBytes,
|
|
262
|
+
persistence,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
function resolveStorageManager(value, path) {
|
|
266
|
+
if (value === undefined) {
|
|
267
|
+
const platform = globalThis.navigator?.storage;
|
|
268
|
+
return typeof platform === "object" && platform !== null ? platform : undefined;
|
|
269
|
+
}
|
|
270
|
+
if (typeof value !== "object" || value === null)
|
|
271
|
+
invalid(`${path} must be a StorageManager-like object.`, path);
|
|
272
|
+
return value;
|
|
273
|
+
}
|
|
274
|
+
async function readPersistence(storage) {
|
|
275
|
+
if (typeof storage.persisted !== "function")
|
|
276
|
+
return "unknown";
|
|
277
|
+
try {
|
|
278
|
+
return (await storage.persisted()) === true ? "persisted" : "best-effort";
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return "unknown";
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function readByteCount(value) {
|
|
285
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
|
|
286
|
+
return undefined;
|
|
287
|
+
const floored = Math.floor(value);
|
|
288
|
+
return Number.isSafeInteger(floored) ? floored : undefined;
|
|
289
|
+
}
|
|
290
|
+
function unavailable(observedAt, reason, persistence) {
|
|
291
|
+
return deepFreeze({
|
|
292
|
+
kind: HONUA_OFFLINE_STORAGE_BUDGET_KIND,
|
|
293
|
+
version: HONUA_OFFLINE_STORAGE_BUDGET_VERSION,
|
|
294
|
+
status: "unavailable",
|
|
295
|
+
observedAt,
|
|
296
|
+
reason,
|
|
297
|
+
persistence,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
function persistenceResult(status, persistence, reason) {
|
|
301
|
+
return deepFreeze({
|
|
302
|
+
kind: HONUA_OFFLINE_STORAGE_PERSISTENCE_KIND,
|
|
303
|
+
version: HONUA_OFFLINE_STORAGE_PERSISTENCE_VERSION,
|
|
304
|
+
status,
|
|
305
|
+
persistence,
|
|
306
|
+
...(reason === undefined ? {} : { reason }),
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
function plainRecord(value, path) {
|
|
310
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
311
|
+
invalid(`${path} must be a plain object.`, path);
|
|
312
|
+
}
|
|
313
|
+
const prototype = Object.getPrototypeOf(value);
|
|
314
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
315
|
+
invalid(`${path} must be a plain object.`, path);
|
|
316
|
+
if (Object.getOwnPropertySymbols(value).length > 0)
|
|
317
|
+
invalid(`${path} must not contain symbol properties.`, path);
|
|
318
|
+
return value;
|
|
319
|
+
}
|
|
320
|
+
function allowedKeys(record, allowed, path) {
|
|
321
|
+
for (const key in record) {
|
|
322
|
+
if (Object.hasOwn(record, key) && !allowed.has(key))
|
|
323
|
+
invalid(`${path}.${key} is not supported.`, `${path}.${key}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function memberOf(value, allowed, path) {
|
|
327
|
+
if (typeof value !== "string" || !allowed.has(value))
|
|
328
|
+
invalid(`${path} is invalid.`, path);
|
|
329
|
+
return value;
|
|
330
|
+
}
|
|
331
|
+
function normalizeRatio(value, path) {
|
|
332
|
+
if (value === undefined)
|
|
333
|
+
return DEFAULT_OFFLINE_STORAGE_HEADROOM_RATIO;
|
|
334
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
|
|
335
|
+
invalid(`${path} must be a finite ratio between 0 and 1.`, path);
|
|
336
|
+
}
|
|
337
|
+
return value;
|
|
338
|
+
}
|
|
339
|
+
function nonNegativeInteger(value, path) {
|
|
340
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
341
|
+
invalid(`${path} must be a non-negative safe integer.`, path);
|
|
342
|
+
}
|
|
343
|
+
return value;
|
|
344
|
+
}
|
|
345
|
+
function validDate(value, path) {
|
|
346
|
+
const time = value instanceof Date ? value.getTime() : Number.NaN;
|
|
347
|
+
if (!Number.isFinite(time))
|
|
348
|
+
invalid(`${path} must be a valid Date.`, path);
|
|
349
|
+
return time;
|
|
350
|
+
}
|
|
351
|
+
function normalizedTimestamp(value, path) {
|
|
352
|
+
if (typeof value !== "string" || value.length === 0)
|
|
353
|
+
invalid(`${path} must be an ISO-8601 timestamp.`, path);
|
|
354
|
+
const parsed = Date.parse(value);
|
|
355
|
+
if (!Number.isFinite(parsed) || new Date(parsed).toISOString() !== value) {
|
|
356
|
+
invalid(`${path} must be a normalized ISO-8601 timestamp.`, path);
|
|
357
|
+
}
|
|
358
|
+
return value;
|
|
359
|
+
}
|
|
360
|
+
function invalid(message, path) {
|
|
361
|
+
throw new HonuaOfflineRegionError("invalid-manifest", message, { path });
|
|
362
|
+
}
|
|
363
|
+
function deepFreeze(value) {
|
|
364
|
+
if (typeof value !== "object" || value === null || Object.isFrozen(value))
|
|
365
|
+
return value;
|
|
366
|
+
Object.freeze(value);
|
|
367
|
+
for (const child of Object.values(value))
|
|
368
|
+
deepFreeze(child);
|
|
369
|
+
return value;
|
|
370
|
+
}
|