@neilberkman/sidereon 0.39.1 → 1.0.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/CHANGELOG.md +16 -0
- package/README.md +22 -11
- package/exact-cache.js +390 -58
- package/package.json +1 -1
- package/pkg/sidereon.d.ts +1951 -1815
- package/pkg/sidereon.js +214 -0
- package/pkg/sidereon_bg.wasm +0 -0
- package/pkg/sidereon_bg.wasm.d.ts +1795 -1784
- package/pkg-node/sidereon.d.ts +127 -2
- package/pkg-node/sidereon.js +217 -0
- package/pkg-node/sidereon_bg.wasm +0 -0
- package/pkg-node/sidereon_bg.wasm.d.ts +1795 -1784
- package/types/exact-cache.d.ts +32 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 1.0.0 - 2026-08-21
|
|
6
|
+
|
|
7
|
+
Sidereon 1.0.0 across every interface; additions arrive without breaking
|
|
8
|
+
existing callers from here.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Exact-cache single-flight opens in the browser cache (IndexedDB
|
|
13
|
+
transaction election; liveness rules stay in Rust).
|
|
14
|
+
- Window-scoped continuity verdicts (`Sp3.stencilExtent`,
|
|
15
|
+
`continuityVerdict`, merge-report equivalent) and `nextIssueDue`.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Engine pinned to `sidereon-core` 1.0.0.
|
|
20
|
+
|
|
5
21
|
## 0.39.1 - 2026-08-11
|
|
6
22
|
|
|
7
23
|
### Fixed
|
package/README.md
CHANGED
|
@@ -243,19 +243,30 @@ commit builder/verifier as the other interfaces:
|
|
|
243
243
|
import { BrowserExactProductCache } from "@neilberkman/sidereon/exact-cache";
|
|
244
244
|
|
|
245
245
|
const cache = await BrowserExactProductCache.open();
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
246
|
+
const opened = await cache.openSingleFlight(identity, "nasa_cddis");
|
|
247
|
+
let entry;
|
|
248
|
+
if (opened.kind === "hit") {
|
|
249
|
+
entry = opened.entry;
|
|
250
|
+
} else {
|
|
251
|
+
const owner = opened.owner;
|
|
252
|
+
try {
|
|
253
|
+
// Only the owner fetches and validates. These are the resulting immutable bytes.
|
|
254
|
+
entry = await owner.publish(productBytes, archiveBytes, provenanceBytes);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
await owner.abandon().catch(() => {});
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
252
260
|
```
|
|
253
261
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
262
|
+
IndexedDB transactions elect one owner across same-origin tabs and workers.
|
|
263
|
+
The owner heartbeat and every takeover decision use the engine's bounded
|
|
264
|
+
single-flight policy. Publication writes the immutable entry and marker in one
|
|
265
|
+
strict-durability transaction; reads return bytes only after shared schema-v3
|
|
266
|
+
verification. Call `owner.abandon()` when acquisition stops before publication.
|
|
267
|
+
The legacy `withLock` method remains available for Web Locks callers. Node hosts
|
|
268
|
+
can call `buildExactCacheCommit` and `verifyExactCacheCommit` with their own
|
|
269
|
+
storage transaction.
|
|
259
270
|
|
|
260
271
|
Send credentials only to NASA's documented hosts;
|
|
261
272
|
remove URL queries from diagnostics; reject HTML success bodies; validate
|
package/exact-cache.js
CHANGED
|
@@ -1,7 +1,43 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
buildExactCacheCommit,
|
|
3
|
+
ExactCacheSingleFlightWait as CoreExactCacheSingleFlightWait,
|
|
4
|
+
verifyExactCacheCommit,
|
|
5
|
+
} from "./pkg/sidereon.js";
|
|
2
6
|
|
|
3
7
|
const MARKERS = "markers";
|
|
4
8
|
const ENTRIES = "entries";
|
|
9
|
+
const IN_FLIGHT = "inFlight";
|
|
10
|
+
const HEARTBEATS = "heartbeats";
|
|
11
|
+
const REVISION_ENCODER = new globalThis.TextEncoder();
|
|
12
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
13
|
+
|
|
14
|
+
const SINGLE_FLIGHT_DEFAULTS = Object.freeze({
|
|
15
|
+
pollIntervalMs: 50,
|
|
16
|
+
heartbeatIntervalMs: 5_000,
|
|
17
|
+
livenessTimeoutMs: 30_000,
|
|
18
|
+
waitTimeoutMs: 30 * 60 * 1_000,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export class ExactCacheSingleFlightOptionsError extends TypeError {
|
|
22
|
+
constructor(message = "invalid exact-cache single-flight options", options) {
|
|
23
|
+
super(message, options);
|
|
24
|
+
this.name = "ExactCacheSingleFlightOptionsError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class ExactCacheSingleFlightTimeoutError extends Error {
|
|
29
|
+
constructor(message = "timed out waiting for the exact-cache in-flight owner", options) {
|
|
30
|
+
super(message, options);
|
|
31
|
+
this.name = "ExactCacheSingleFlightTimeoutError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class ExactCacheSingleFlightOwnershipLostError extends Error {
|
|
36
|
+
constructor(message = "exact-cache single-flight ownership was lost", options) {
|
|
37
|
+
super(message, options);
|
|
38
|
+
this.name = "ExactCacheSingleFlightOwnershipLostError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
5
41
|
|
|
6
42
|
function request(request) {
|
|
7
43
|
return new Promise((resolve, reject) => {
|
|
@@ -14,10 +50,28 @@ function complete(transaction) {
|
|
|
14
50
|
return new Promise((resolve, reject) => {
|
|
15
51
|
transaction.oncomplete = () => resolve();
|
|
16
52
|
transaction.onerror = () => reject(transaction.error);
|
|
17
|
-
transaction.onabort = () =>
|
|
53
|
+
transaction.onabort = () =>
|
|
54
|
+
reject(transaction.error ?? new Error("exact-cache transaction aborted"));
|
|
18
55
|
});
|
|
19
56
|
}
|
|
20
57
|
|
|
58
|
+
async function runTransaction(transaction, operation) {
|
|
59
|
+
const completion = complete(transaction);
|
|
60
|
+
try {
|
|
61
|
+
const result = await operation();
|
|
62
|
+
await completion;
|
|
63
|
+
return result;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
try {
|
|
66
|
+
transaction.abort();
|
|
67
|
+
} catch {
|
|
68
|
+
// The request may already have aborted or completed the transaction.
|
|
69
|
+
}
|
|
70
|
+
await completion.catch(() => {});
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
21
75
|
function bytes(value) {
|
|
22
76
|
return value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
23
77
|
}
|
|
@@ -36,11 +90,83 @@ function entryKey(base, entry) {
|
|
|
36
90
|
return `${base}\0${entry}`;
|
|
37
91
|
}
|
|
38
92
|
|
|
93
|
+
function monotonicNow() {
|
|
94
|
+
if (typeof globalThis.performance?.now !== "function") {
|
|
95
|
+
throw new Error("BrowserExactProductCache requires performance.now()");
|
|
96
|
+
}
|
|
97
|
+
return globalThis.performance.now();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function timerDelay(milliseconds) {
|
|
101
|
+
return Math.min(milliseconds, MAX_TIMER_DELAY_MS);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function waitFor(milliseconds) {
|
|
105
|
+
return new Promise((resolve) => globalThis.setTimeout(resolve, timerDelay(milliseconds)));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function ownerRevision(inFlight, heartbeat) {
|
|
109
|
+
return JSON.stringify([inFlight, heartbeat]);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function singleFlightWait(options, startedMs) {
|
|
113
|
+
if (options === undefined) options = {};
|
|
114
|
+
if (options === null || typeof options !== "object" || Array.isArray(options)) {
|
|
115
|
+
throw new ExactCacheSingleFlightOptionsError();
|
|
116
|
+
}
|
|
117
|
+
const normalized = {
|
|
118
|
+
pollIntervalMs: options.pollIntervalMs ?? SINGLE_FLIGHT_DEFAULTS.pollIntervalMs,
|
|
119
|
+
heartbeatIntervalMs: options.heartbeatIntervalMs ?? SINGLE_FLIGHT_DEFAULTS.heartbeatIntervalMs,
|
|
120
|
+
livenessTimeoutMs: options.livenessTimeoutMs ?? SINGLE_FLIGHT_DEFAULTS.livenessTimeoutMs,
|
|
121
|
+
waitTimeoutMs: options.waitTimeoutMs ?? SINGLE_FLIGHT_DEFAULTS.waitTimeoutMs,
|
|
122
|
+
};
|
|
123
|
+
try {
|
|
124
|
+
return {
|
|
125
|
+
heartbeatIntervalMs: normalized.heartbeatIntervalMs,
|
|
126
|
+
wait: new CoreExactCacheSingleFlightWait(
|
|
127
|
+
startedMs,
|
|
128
|
+
normalized.pollIntervalMs,
|
|
129
|
+
normalized.heartbeatIntervalMs,
|
|
130
|
+
normalized.livenessTimeoutMs,
|
|
131
|
+
normalized.waitTimeoutMs,
|
|
132
|
+
),
|
|
133
|
+
};
|
|
134
|
+
} catch (cause) {
|
|
135
|
+
throw new ExactCacheSingleFlightOptionsError(undefined, { cause });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function readCommitted(transaction, identity, source, base) {
|
|
140
|
+
const committed = await request(transaction.objectStore(MARKERS).get(base));
|
|
141
|
+
if (committed === undefined) return null;
|
|
142
|
+
const entry = await request(
|
|
143
|
+
transaction.objectStore(ENTRIES).get(entryKey(base, committed.entry)),
|
|
144
|
+
);
|
|
145
|
+
if (entry === undefined) {
|
|
146
|
+
throw new Error("committed exact-cache entry is incomplete");
|
|
147
|
+
}
|
|
148
|
+
const product = bytes(entry.product);
|
|
149
|
+
const archive = bytes(entry.archive);
|
|
150
|
+
const provenance = bytes(entry.provenance);
|
|
151
|
+
const marker = bytes(committed.marker);
|
|
152
|
+
const verified = verifyExactCacheCommit(identity, source, marker, product, archive, provenance);
|
|
153
|
+
if (verified !== committed.entry) {
|
|
154
|
+
throw new Error("exact-cache entry identifier mismatch");
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
entryId: verified,
|
|
158
|
+
product,
|
|
159
|
+
archive,
|
|
160
|
+
provenance,
|
|
161
|
+
marker,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
39
165
|
async function openDatabase(name) {
|
|
40
166
|
if (typeof globalThis.indexedDB === "undefined") {
|
|
41
167
|
throw new Error("BrowserExactProductCache requires IndexedDB");
|
|
42
168
|
}
|
|
43
|
-
const open = globalThis.indexedDB.open(name,
|
|
169
|
+
const open = globalThis.indexedDB.open(name, 4);
|
|
44
170
|
open.onupgradeneeded = () => {
|
|
45
171
|
if (!open.result.objectStoreNames.contains(MARKERS)) {
|
|
46
172
|
open.result.createObjectStore(MARKERS);
|
|
@@ -48,6 +174,12 @@ async function openDatabase(name) {
|
|
|
48
174
|
if (!open.result.objectStoreNames.contains(ENTRIES)) {
|
|
49
175
|
open.result.createObjectStore(ENTRIES);
|
|
50
176
|
}
|
|
177
|
+
if (!open.result.objectStoreNames.contains(IN_FLIGHT)) {
|
|
178
|
+
open.result.createObjectStore(IN_FLIGHT);
|
|
179
|
+
}
|
|
180
|
+
if (!open.result.objectStoreNames.contains(HEARTBEATS)) {
|
|
181
|
+
open.result.createObjectStore(HEARTBEATS);
|
|
182
|
+
}
|
|
51
183
|
};
|
|
52
184
|
return request(open);
|
|
53
185
|
}
|
|
@@ -118,9 +250,130 @@ class LockedExactCache {
|
|
|
118
250
|
}
|
|
119
251
|
}
|
|
120
252
|
|
|
253
|
+
export class ExactCacheSingleFlightOwner {
|
|
254
|
+
constructor(cache, identity, source, base, ownerToken, heartbeatIntervalMs) {
|
|
255
|
+
this.cache = cache;
|
|
256
|
+
this.identity = identity;
|
|
257
|
+
this.source = source;
|
|
258
|
+
this.base = base;
|
|
259
|
+
this.ownerToken = ownerToken;
|
|
260
|
+
this.heartbeatIntervalMs = heartbeatIntervalMs;
|
|
261
|
+
this.state = "active";
|
|
262
|
+
this.heartbeatError = null;
|
|
263
|
+
this.heartbeatTail = Promise.resolve();
|
|
264
|
+
this.heartbeatTimer = undefined;
|
|
265
|
+
this._scheduleHeartbeat();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
_ownershipLost(cause) {
|
|
269
|
+
return cause instanceof ExactCacheSingleFlightOwnershipLostError
|
|
270
|
+
? cause
|
|
271
|
+
: new ExactCacheSingleFlightOwnershipLostError(undefined, { cause });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
_ensureActive() {
|
|
275
|
+
if (this.state !== "active") {
|
|
276
|
+
throw new ExactCacheSingleFlightOwnershipLostError();
|
|
277
|
+
}
|
|
278
|
+
if (this.heartbeatError !== null) throw this.heartbeatError;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
_stopHeartbeat() {
|
|
282
|
+
if (this.heartbeatTimer !== undefined) {
|
|
283
|
+
globalThis.clearTimeout(this.heartbeatTimer);
|
|
284
|
+
this.heartbeatTimer = undefined;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
_scheduleHeartbeat() {
|
|
289
|
+
if (this.state !== "active" || this.heartbeatError !== null) return;
|
|
290
|
+
this.heartbeatTimer = globalThis.setTimeout(() => {
|
|
291
|
+
this.heartbeatTimer = undefined;
|
|
292
|
+
void this._queueHeartbeat().then(
|
|
293
|
+
() => this._scheduleHeartbeat(),
|
|
294
|
+
() => {},
|
|
295
|
+
);
|
|
296
|
+
}, timerDelay(this.heartbeatIntervalMs));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
_queueHeartbeat() {
|
|
300
|
+
try {
|
|
301
|
+
this._ensureActive();
|
|
302
|
+
} catch (error) {
|
|
303
|
+
return Promise.reject(error);
|
|
304
|
+
}
|
|
305
|
+
const attempt = this.heartbeatTail.then(() =>
|
|
306
|
+
this.cache._heartbeatSingleFlight(this.base, this.ownerToken),
|
|
307
|
+
);
|
|
308
|
+
this.heartbeatTail = attempt.catch((error) => {
|
|
309
|
+
this.heartbeatError = this._ownershipLost(error);
|
|
310
|
+
this._stopHeartbeat();
|
|
311
|
+
});
|
|
312
|
+
return attempt;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
heartbeat() {
|
|
316
|
+
return this._queueHeartbeat();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async publish(product, archive, provenance) {
|
|
320
|
+
if (this.state !== "active") {
|
|
321
|
+
throw new ExactCacheSingleFlightOwnershipLostError();
|
|
322
|
+
}
|
|
323
|
+
this.state = "publishing";
|
|
324
|
+
this._stopHeartbeat();
|
|
325
|
+
try {
|
|
326
|
+
await this.heartbeatTail;
|
|
327
|
+
if (this.heartbeatError !== null) throw this.heartbeatError;
|
|
328
|
+
product = bytes(product);
|
|
329
|
+
archive = bytes(archive);
|
|
330
|
+
provenance = bytes(provenance);
|
|
331
|
+
const entry = entryId();
|
|
332
|
+
const marker = buildExactCacheCommit(
|
|
333
|
+
this.identity,
|
|
334
|
+
this.source,
|
|
335
|
+
entry,
|
|
336
|
+
product,
|
|
337
|
+
archive,
|
|
338
|
+
provenance,
|
|
339
|
+
);
|
|
340
|
+
const published = await this.cache._publishSingleFlight(
|
|
341
|
+
this.base,
|
|
342
|
+
this.ownerToken,
|
|
343
|
+
entry,
|
|
344
|
+
product,
|
|
345
|
+
archive,
|
|
346
|
+
provenance,
|
|
347
|
+
marker,
|
|
348
|
+
);
|
|
349
|
+
this.state = "published";
|
|
350
|
+
return published;
|
|
351
|
+
} catch (error) {
|
|
352
|
+
await this.cache._abandonSingleFlight(this.base, this.ownerToken).catch(() => {});
|
|
353
|
+
this.state = "abandoned";
|
|
354
|
+
throw error;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async abandon() {
|
|
359
|
+
if (this.state !== "active") {
|
|
360
|
+
throw new ExactCacheSingleFlightOwnershipLostError();
|
|
361
|
+
}
|
|
362
|
+
this.state = "abandoning";
|
|
363
|
+
this._stopHeartbeat();
|
|
364
|
+
await this.heartbeatTail;
|
|
365
|
+
try {
|
|
366
|
+
await this.cache._abandonSingleFlight(this.base, this.ownerToken);
|
|
367
|
+
} finally {
|
|
368
|
+
this.state = "abandoned";
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
121
373
|
/**
|
|
122
|
-
* Browser exact-product cache using
|
|
123
|
-
* coordination and
|
|
374
|
+
* Browser exact-product cache using IndexedDB transactions for single-flight
|
|
375
|
+
* coordination and durable entry publication. `withLock` retains its legacy
|
|
376
|
+
* Web Locks behavior.
|
|
124
377
|
*/
|
|
125
378
|
export class BrowserExactProductCache {
|
|
126
379
|
static async open({ name = "sidereon-exact-products-v3" } = {}) {
|
|
@@ -177,62 +430,141 @@ export class BrowserExactProductCache {
|
|
|
177
430
|
return this._read(identity, source, baseKey(identity, source));
|
|
178
431
|
}
|
|
179
432
|
|
|
180
|
-
_read(identity, source, base) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
try {
|
|
208
|
-
verified = verifyExactCacheCommit(
|
|
433
|
+
async _read(identity, source, base) {
|
|
434
|
+
const transaction = this.database.transaction([MARKERS, ENTRIES], "readonly");
|
|
435
|
+
return runTransaction(transaction, () => readCommitted(transaction, identity, source, base));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async openSingleFlight(identity, source, options) {
|
|
439
|
+
const startedMs = monotonicNow();
|
|
440
|
+
const { heartbeatIntervalMs, wait } = singleFlightWait(options, startedMs);
|
|
441
|
+
try {
|
|
442
|
+
const base = baseKey(identity, source);
|
|
443
|
+
const ownerToken = entryId();
|
|
444
|
+
let takeoverRevision;
|
|
445
|
+
while (true) {
|
|
446
|
+
const result = await this._openSingleFlightTransaction(
|
|
447
|
+
identity,
|
|
448
|
+
source,
|
|
449
|
+
base,
|
|
450
|
+
ownerToken,
|
|
451
|
+
takeoverRevision,
|
|
452
|
+
);
|
|
453
|
+
takeoverRevision = undefined;
|
|
454
|
+
if (result.kind === "hit") return result;
|
|
455
|
+
if (result.kind === "owner") {
|
|
456
|
+
return {
|
|
457
|
+
kind: "owner",
|
|
458
|
+
owner: new ExactCacheSingleFlightOwner(
|
|
459
|
+
this,
|
|
209
460
|
identity,
|
|
210
461
|
source,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
);
|
|
216
|
-
} catch (error) {
|
|
217
|
-
reject(error);
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
if (verified !== committed.entry) {
|
|
221
|
-
reject(new Error("exact-cache entry identifier mismatch"));
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
result = {
|
|
225
|
-
entryId: verified,
|
|
226
|
-
product,
|
|
227
|
-
archive,
|
|
228
|
-
provenance,
|
|
229
|
-
marker,
|
|
462
|
+
base,
|
|
463
|
+
ownerToken,
|
|
464
|
+
heartbeatIntervalMs,
|
|
465
|
+
),
|
|
230
466
|
};
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const decision = wait.observe(monotonicNow(), REVISION_ENCODER.encode(result.revision));
|
|
470
|
+
if (decision.action === "wait") {
|
|
471
|
+
await waitFor(decision.delayMs);
|
|
472
|
+
} else if (decision.action === "takeover") {
|
|
473
|
+
takeoverRevision = result.revision;
|
|
474
|
+
} else {
|
|
475
|
+
throw new ExactCacheSingleFlightTimeoutError();
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
} finally {
|
|
479
|
+
wait.free();
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
_openSingleFlightTransaction(identity, source, base, ownerToken, takeoverRevision) {
|
|
484
|
+
const transaction = this.database.transaction(
|
|
485
|
+
[MARKERS, ENTRIES, IN_FLIGHT, HEARTBEATS],
|
|
486
|
+
"readwrite",
|
|
487
|
+
{ durability: "strict" },
|
|
488
|
+
);
|
|
489
|
+
return runTransaction(transaction, async () => {
|
|
490
|
+
const committed = await readCommitted(transaction, identity, source, base);
|
|
491
|
+
if (committed !== null) return { kind: "hit", entry: committed };
|
|
492
|
+
|
|
493
|
+
const inFlightStore = transaction.objectStore(IN_FLIGHT);
|
|
494
|
+
const heartbeatStore = transaction.objectStore(HEARTBEATS);
|
|
495
|
+
const inFlight = await request(inFlightStore.get(base));
|
|
496
|
+
const heartbeat = await request(heartbeatStore.get(base));
|
|
497
|
+
const revision = ownerRevision(inFlight, heartbeat);
|
|
498
|
+
if (
|
|
499
|
+
inFlight === undefined ||
|
|
500
|
+
(takeoverRevision !== undefined && takeoverRevision === revision)
|
|
501
|
+
) {
|
|
502
|
+
inFlightStore.put({ ownerToken }, base);
|
|
503
|
+
heartbeatStore.put({ ownerToken, revision: 0 }, base);
|
|
504
|
+
return { kind: "owner" };
|
|
505
|
+
}
|
|
506
|
+
return { kind: "waiter", revision };
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
_heartbeatSingleFlight(base, ownerToken) {
|
|
511
|
+
const transaction = this.database.transaction([IN_FLIGHT, HEARTBEATS], "readwrite", {
|
|
512
|
+
durability: "strict",
|
|
513
|
+
});
|
|
514
|
+
return runTransaction(transaction, async () => {
|
|
515
|
+
const inFlight = await request(transaction.objectStore(IN_FLIGHT).get(base));
|
|
516
|
+
const heartbeatStore = transaction.objectStore(HEARTBEATS);
|
|
517
|
+
const heartbeat = await request(heartbeatStore.get(base));
|
|
518
|
+
if (
|
|
519
|
+
inFlight?.ownerToken !== ownerToken ||
|
|
520
|
+
heartbeat?.ownerToken !== ownerToken ||
|
|
521
|
+
!Number.isSafeInteger(heartbeat.revision) ||
|
|
522
|
+
heartbeat.revision < 0 ||
|
|
523
|
+
heartbeat.revision === Number.MAX_SAFE_INTEGER
|
|
524
|
+
) {
|
|
525
|
+
throw new ExactCacheSingleFlightOwnershipLostError();
|
|
526
|
+
}
|
|
527
|
+
heartbeatStore.put({ ownerToken, revision: heartbeat.revision + 1 }, base);
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
_publishSingleFlight(base, ownerToken, entry, product, archive, provenance, marker) {
|
|
532
|
+
const transaction = this.database.transaction(
|
|
533
|
+
[MARKERS, ENTRIES, IN_FLIGHT, HEARTBEATS],
|
|
534
|
+
"readwrite",
|
|
535
|
+
{ durability: "strict" },
|
|
536
|
+
);
|
|
537
|
+
return runTransaction(transaction, async () => {
|
|
538
|
+
const inFlight = await request(transaction.objectStore(IN_FLIGHT).get(base));
|
|
539
|
+
if (inFlight?.ownerToken !== ownerToken) {
|
|
540
|
+
throw new ExactCacheSingleFlightOwnershipLostError();
|
|
541
|
+
}
|
|
542
|
+
transaction.objectStore(ENTRIES).add(
|
|
543
|
+
{
|
|
544
|
+
product: product.slice(),
|
|
545
|
+
archive: archive.slice(),
|
|
546
|
+
provenance: provenance.slice(),
|
|
547
|
+
},
|
|
548
|
+
entryKey(base, entry),
|
|
549
|
+
);
|
|
550
|
+
transaction.objectStore(MARKERS).put({ entry, marker: marker.slice() }, base);
|
|
551
|
+
transaction.objectStore(IN_FLIGHT).delete(base);
|
|
552
|
+
transaction.objectStore(HEARTBEATS).delete(base);
|
|
553
|
+
return { entryId: entry, product, archive, provenance, marker };
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
_abandonSingleFlight(base, ownerToken) {
|
|
558
|
+
const transaction = this.database.transaction([IN_FLIGHT, HEARTBEATS], "readwrite", {
|
|
559
|
+
durability: "strict",
|
|
560
|
+
});
|
|
561
|
+
return runTransaction(transaction, async () => {
|
|
562
|
+
const inFlightStore = transaction.objectStore(IN_FLIGHT);
|
|
563
|
+
const inFlight = await request(inFlightStore.get(base));
|
|
564
|
+
if (inFlight?.ownerToken === ownerToken) {
|
|
565
|
+
inFlightStore.delete(base);
|
|
566
|
+
transaction.objectStore(HEARTBEATS).delete(base);
|
|
567
|
+
}
|
|
236
568
|
});
|
|
237
569
|
}
|
|
238
570
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neilberkman/sidereon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "WebAssembly / JavaScript interface over the sidereon GNSS + astrodynamics engine: SP3 loading and multi-product merge, SPP / RTK (float + fixed) / PPP (float + fixed) positioning, IONEX slant delay, SGP4 and numerical orbit propagation, RINEX observation/navigation/clock parsing, CRINEX (Hatanaka) decoding, observable-domain math (carrier combinations, cycle slips, Hatch smoothing, weighting, velocity, GPS C/A signal), and the astro domain (scale-tagged time, frame transforms, Sun/Moon ephemerides, eclipse / body angles, DOP, passes / visibility, conjunction + collision probability, CDM / OMM / ANTEX parsing, RF link budget)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|