@serve.zone/interfaces 32.17.0 → 32.19.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.
@@ -1,6 +1,6 @@
1
1
  import type { IFleetCutoverAcceptedEvidenceReference } from './fleetcutover.js';
2
2
  import type { TSha256Digest } from './immutableimage.js';
3
- import { isReleaseVersion } from '../private/releaseversion.js';
3
+ import { isReleaseVersion } from './releaseversion.js';
4
4
  import {
5
5
  captureNetwork,
6
6
  networkCounter,
@@ -50,6 +50,18 @@ export interface ISparkNodeBundleIdentity {
50
50
  unitDigest: TSha256Digest;
51
51
  }
52
52
 
53
+ /**
54
+ * What became of the released configuration source once this node stopped reading it.
55
+ *
56
+ * A tuple beside the union, exactly like `sparkLegacyRuntimeModes`: a reader that has to name the
57
+ * two states — a form, a migration, a refusal message — reads them from the contract instead of
58
+ * restating two string literals the contract could later disagree with.
59
+ */
60
+ export const sparkLegacyConfigurationDispositions = ['retained-inert', 'removed'] as const;
61
+
62
+ export type TSparkLegacyConfigurationDisposition =
63
+ typeof sparkLegacyConfigurationDispositions[number];
64
+
53
65
  /**
54
66
  * The released key-value configuration source, by digest and disposition only.
55
67
  *
@@ -63,7 +75,7 @@ export interface ISparkLegacyConfigurationState {
63
75
  /** SHA-256 of the exact source bytes this node imported, once, under its fenced read. */
64
76
  sourceSha256: string;
65
77
  /** On disk and read by nothing, or unlinked. Erasure is not a cutover requirement. */
66
- disposition: 'retained-inert' | 'removed';
78
+ disposition: TSparkLegacyConfigurationDisposition;
67
79
  }
68
80
 
69
81
  /**
@@ -138,6 +150,46 @@ export const sparkLegacyRetirementRefusals = Object.freeze([
138
150
 
139
151
  export type TSparkLegacyRetirementRefusal = typeof sparkLegacyRetirementRefusals[number];
140
152
 
153
+ /**
154
+ * What the server did with the statement one observation carried, answered on that observation's
155
+ * own accepted receipt.
156
+ *
157
+ * The statement is judged against records, not against the message: a malformed one is already
158
+ * refused by `validateSparkLegacyRuntimeRetirement` before any of this, so everything left is a
159
+ * fact about this node's history. That is why a refusal here does not reject the observation —
160
+ * the node's liveness and census are true whatever its released runtime states — and why an
161
+ * accepted observation is the only place this outcome appears.
162
+ *
163
+ * `generation` is the revision both node-owned records stand at once the observation is accepted:
164
+ * `accepted` advanced it, `restated` is the replay that renewed nothing and left it where it was.
165
+ * A refusal carries no generation, because the counter a refused node has to reconcile with is
166
+ * its own `ownerGeneration`, which it already holds.
167
+ */
168
+ export type TSparkLegacyRetirementOutcome =
169
+ | {
170
+ state: 'accepted';
171
+ /** The revision this observation advanced both records to. */
172
+ generation: number;
173
+ }
174
+ | {
175
+ state: 'restated';
176
+ /** The revision that stands; this observation added nothing to it. */
177
+ generation: number;
178
+ }
179
+ | {
180
+ state: 'refused';
181
+ refusal: TSparkLegacyRetirementRefusal;
182
+ /**
183
+ * Always false, and stated anyway.
184
+ *
185
+ * Every refusal of this family answers one exact statement, so resending it cannot change
186
+ * the answer; a server-side race is refused one level up, as the observation's own
187
+ * `acceptance-race`. It travels on the wire for the reason the rejected receipt carries its
188
+ * own flag: a node that meets a refusal name a later minor added still learns it must stop.
189
+ */
190
+ retryable: false;
191
+ };
192
+
141
193
  const fail = (): never => { throw new Error('Spark legacy retirement contract rejected.'); };
142
194
  const safe = <T>(operationArg: () => T): T => {
143
195
  try { return operationArg(); } catch { return fail(); }
@@ -165,9 +217,16 @@ const legacyRuntimeMode = (valueArg: unknown): TSparkLegacyRuntimeMode => {
165
217
 
166
218
  const configurationDisposition = (
167
219
  valueArg: unknown,
168
- ): ISparkLegacyConfigurationState['disposition'] => {
169
- requireNetwork(valueArg === 'retained-inert' || valueArg === 'removed');
170
- return valueArg;
220
+ ): TSparkLegacyConfigurationDisposition => {
221
+ requireNetwork(sparkLegacyConfigurationDispositions
222
+ .includes(valueArg as TSparkLegacyConfigurationDisposition));
223
+ return valueArg as TSparkLegacyConfigurationDisposition;
224
+ };
225
+
226
+ const retirementRefusal = (valueArg: unknown): TSparkLegacyRetirementRefusal => {
227
+ requireNetwork(sparkLegacyRetirementRefusals
228
+ .includes(valueArg as TSparkLegacyRetirementRefusal));
229
+ return valueArg as TSparkLegacyRetirementRefusal;
171
230
  };
172
231
 
173
232
  /** Structural copy only; whether the tree it describes is installed is the node's to observe. */
@@ -223,6 +282,31 @@ export const validateSparkLegacyRuntimeRetirement = (valueArg: unknown): string[
223
282
  } catch { return ['Spark legacy runtime retirement must use its exact canonical schema']; }
224
283
  };
225
284
 
285
+ /** Exact-key per state, like every other shape here: an outcome states one answer and no other. */
286
+ export const snapshotSparkLegacyRetirementOutcome = (
287
+ valueArg: unknown,
288
+ ): TSparkLegacyRetirementOutcome => safe(() => {
289
+ const captured = captureNetwork(valueArg);
290
+ requireNetwork(captured !== null && typeof captured === 'object');
291
+ const state = (captured as Record<string, unknown>).state;
292
+ requireNetwork(state === 'accepted' || state === 'restated' || state === 'refused');
293
+ if (state === 'refused') {
294
+ const value = networkRecord(captured, ['state', 'refusal', 'retryable']);
295
+ requireNetwork(value.retryable === false);
296
+ return { state, refusal: retirementRefusal(value.refusal), retryable: false };
297
+ }
298
+ const value = networkRecord(captured, ['state', 'generation']);
299
+ return { state, generation: networkCounter(value.generation) };
300
+ });
301
+
302
+ /** Synchronous for the same reason: the receipt that carries it is what a reader binds. */
303
+ export const validateSparkLegacyRetirementOutcome = (valueArg: unknown): string[] => {
304
+ try {
305
+ snapshotSparkLegacyRetirementOutcome(valueArg);
306
+ return [];
307
+ } catch { return ['Spark legacy retirement outcome must use its exact canonical schema']; }
308
+ };
309
+
226
310
  /** The revision rule every accepted receipt of this contract obeys, read once for both of them. */
227
311
  const retirementReceiptBase = (
228
312
  valueArg: unknown,
@@ -1,4 +1,4 @@
1
- import { isReleaseVersion } from '../private/releaseversion.js';
1
+ import { isReleaseVersion } from './releaseversion.js';
2
2
  import { readExactDataRecord } from '../private/dataproperties.js';
3
3
  import { isClusterNodeId } from '../private/nodeidentifier.js';
4
4
  import { protocolRefusalHttpStatus, readProtocolOffer, type IProtocolOffer } from '../protocol.js';
package/ts/protocol.ts CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  maximumReleaseVersionLength,
9
9
  parseReleaseVersion,
10
10
  type IReleaseVersion,
11
- } from './private/releaseversion.js';
11
+ } from './data/releaseversion.js';
12
12
 
13
13
  /**
14
14
  * The contract this build speaks: the installed `@serve.zone/interfaces` release.
File without changes
File without changes