@uns-kit/core 3.0.37 → 3.0.39

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.
Files changed (37) hide show
  1. package/MIGRATIONS.md +39 -0
  2. package/README.md +11 -0
  3. package/dist/runtime/input-queue-boundary.d.ts +15 -0
  4. package/dist/runtime/input-queue-boundary.d.ts.map +1 -0
  5. package/dist/runtime/input-queue-boundary.js +33 -0
  6. package/dist/runtime/input-queue-boundary.js.map +1 -0
  7. package/dist/runtime/mqtt-handover-state.d.ts +4 -1
  8. package/dist/runtime/mqtt-handover-state.d.ts.map +1 -1
  9. package/dist/runtime/mqtt-handover-state.js +5 -3
  10. package/dist/runtime/mqtt-handover-state.js.map +1 -1
  11. package/dist/uns/handover-manager.d.ts +3 -0
  12. package/dist/uns/handover-manager.d.ts.map +1 -1
  13. package/dist/uns/handover-manager.js +61 -11
  14. package/dist/uns/handover-manager.js.map +1 -1
  15. package/dist/uns/uns-interfaces.d.ts +1 -0
  16. package/dist/uns/uns-interfaces.d.ts.map +1 -1
  17. package/dist/uns/uns-interfaces.js.map +1 -1
  18. package/dist/uns-mqtt/mqtt-proxy.d.ts +1 -0
  19. package/dist/uns-mqtt/mqtt-proxy.d.ts.map +1 -1
  20. package/dist/uns-mqtt/mqtt-proxy.js +1 -0
  21. package/dist/uns-mqtt/mqtt-proxy.js.map +1 -1
  22. package/dist/uns-mqtt/mqtt-worker.d.ts.map +1 -1
  23. package/dist/uns-mqtt/mqtt-worker.js +11 -0
  24. package/dist/uns-mqtt/mqtt-worker.js.map +1 -1
  25. package/dist/uns-mqtt/subscription-readiness.d.ts +2 -0
  26. package/dist/uns-mqtt/subscription-readiness.d.ts.map +1 -1
  27. package/dist/uns-mqtt/subscription-readiness.js +5 -1
  28. package/dist/uns-mqtt/subscription-readiness.js.map +1 -1
  29. package/dist/uns-mqtt/throttled-queue.d.ts +5 -8
  30. package/dist/uns-mqtt/throttled-queue.d.ts.map +1 -1
  31. package/dist/uns-mqtt/throttled-queue.js +25 -1
  32. package/dist/uns-mqtt/throttled-queue.js.map +1 -1
  33. package/dist/uns-mqtt/uns-mqtt-proxy.d.ts +10 -8
  34. package/dist/uns-mqtt/uns-mqtt-proxy.d.ts.map +1 -1
  35. package/dist/uns-mqtt/uns-mqtt-proxy.js +43 -3
  36. package/dist/uns-mqtt/uns-mqtt-proxy.js.map +1 -1
  37. package/package.json +1 -1
package/MIGRATIONS.md CHANGED
@@ -9,6 +9,45 @@ Agents must inspect the application's existing ownership and shutdown flow
9
9
  before editing it. The examples below describe the intended behavior, not a
10
10
  mechanical search-and-replace operation.
11
11
 
12
+ ## 3.0.39 - Explicit read-only HTTP work during migration
13
+
14
+ When crossing `<3.0.39` to `>=3.0.39`, existing handlers keep their conservative
15
+ HTTP drain behavior. No MQTT format, ownership contract or Controller change is
16
+ required. Applications that serve reads may opt in per handler using
17
+ `await api.runReadOnlyRequest(res, async () => { /* complete the read */ })`.
18
+ Declare the work before the first asynchronous step and await every admitted
19
+ subtask inside the callback, including cancellation settlement. Use it only for
20
+ work without external effects; neither GET nor POST establishes that property.
21
+
22
+ A reader disconnect no longer poisons future drains for an opted-in handler,
23
+ but drain still waits for actual handler completion, even after an early HTTP
24
+ response. The helper does not cancel SQL, authorize access, retry a request or
25
+ send a response. Keep application cancellation and error handling. An undeclared
26
+ disconnect remains an unknown outcome and blocks handover. Do not wrap writes,
27
+ publications or business mutations as reads to bypass that safeguard. See the
28
+ [HTTP example](https://github.com/uns-openhub/uns-kit/blob/master/docs/mqtt-stateful-handover.md#http-readers-and-disconnects-3039).
29
+
30
+ ## 3.0.38 - Bounded late-subscriber input boundary (format3)
31
+
32
+ When crossing `<3.0.38` to `>=3.0.38`, non-opt-in applications keep their existing
33
+ protocol. For opt-in correlated-v2 MQTT handover, both peers must use the new
34
+ format3 implementation. Format is bound
35
+ into the compatibility digest: a mixed old/new pair refuses while the source
36
+ remains active. Review an initial stop-before-start upgrade, then use matching
37
+ peers for live moves. Never relabel a business contract to bypass this check.
38
+
39
+ No application-specific migration hooks are introduced. Keep existing
40
+ `deferHandoverUntilReady`, semantic contracts and accepted-work/drain ownership.
41
+ The SDK checks fresh broker subscription acknowledgements and uniquely shared
42
+ ordered input before release, including sparse sources whose target missed old
43
+ packets. Without proof the source stays active and the target remains passive.
44
+ Only read-only preparation can repeat after delayed input; unknown import or
45
+ activation effects remain blocked. Equal values are not deduplicated.
46
+
47
+ This does not move persistent queues or guarantee replay of broker losses.
48
+ Use a reviewed reverse hot move for return ownership and retain existing fences.
49
+ See [protocol, example and limits](https://github.com/uns-openhub/uns-kit/blob/master/docs/mqtt-stateful-handover.md#sparse-input-and-late-subscribers-3038).
50
+
12
51
  ## 3.0.37 - Application lifecycle, HTTP ownership and bounded RAM state
13
52
 
14
53
  When crossing `<3.0.37` to `>=3.0.37`, existing non-opt-in consumers retain their
package/README.md CHANGED
@@ -273,6 +273,17 @@ and apply only the migrations whose version boundaries are crossed. The
273
273
  `3.0.0` migration documents the MQTT table-column object contract, while the
274
274
  `2.0.71` migration documents MQTT publish and shutdown ownership changes.
275
275
 
276
+ ## State-preserving MQTT handover
277
+
278
+ Correlated-v2 MQTT handover in3.0.38 verifies a uniquely shared input boundary
279
+ before releasing the source, including a late subscriber that missed older
280
+ packets. Both peers need format3; mixed old/new formats refuse before freeze.
281
+ Retain semantic contracts, accepted-work drain and restart fences. Missing or
282
+ ambiguous overlap and unknown activation results stay blocked.
283
+ See the [protocol, example and limits](https://github.com/uns-openhub/uns-kit/blob/master/docs/mqtt-stateful-handover.md)
284
+ and the version-bounded3.0.38 entry in `MIGRATIONS.md`. This does not transfer
285
+ persistent queues or promise replay of broker losses.
286
+
276
287
  ## Optional configuration-owned process groups
277
288
 
278
289
  Version3.0.29 adds optional shared contracts for immutable application-owned JSON
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ export declare const inputPreviewSchema: z.ZodArray<z.ZodString>;
3
+ export declare const inputCursorSchema: z.ZodObject<{
4
+ batchSize: z.ZodNumber;
5
+ referenceHash: z.ZodString;
6
+ processedTail: z.ZodOptional<z.ZodArray<z.ZodString>>;
7
+ }, z.core.$strict>;
8
+ export type InputQueueCursor = z.infer<typeof inputCursorSchema>;
9
+ export declare function inputFingerprint(item: {
10
+ topic: string;
11
+ message: string;
12
+ }): string;
13
+ /** Find an ordered common suffix. Neither a timestamp nor equal values alone authorize skipping. */
14
+ export declare function uniqueInputBoundary(processedTail: readonly string[], buffered: readonly string[]): number;
15
+ //# sourceMappingURL=input-queue-boundary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input-queue-boundary.d.ts","sourceRoot":"","sources":["../../src/runtime/input-queue-boundary.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,kBAAkB,yBAAuD,CAAC;AACvF,eAAO,MAAM,iBAAiB;;;;kBAI5B,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AACjE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAEjF;AAED,oGAAoG;AACpG,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,EAAE,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAezG"}
@@ -0,0 +1,33 @@
1
+ import { createHash } from "node:crypto";
2
+ import { z } from "zod";
3
+ export const inputPreviewSchema = z.array(z.string().regex(/^[a-f0-9]{64}$/)).max(100);
4
+ export const inputCursorSchema = z.strictObject({
5
+ batchSize: z.number().int().min(0).max(100),
6
+ referenceHash: z.string().regex(/^[a-f0-9]{64}$/),
7
+ processedTail: inputPreviewSchema.optional(),
8
+ });
9
+ export function inputFingerprint(item) {
10
+ return createHash("sha256").update(JSON.stringify({ topic: item.topic, message: item.message })).digest("hex");
11
+ }
12
+ /** Find an ordered common suffix. Neither a timestamp nor equal values alone authorize skipping. */
13
+ export function uniqueInputBoundary(processedTail, buffered) {
14
+ if (processedTail.length === 0)
15
+ return 0;
16
+ for (let size = Math.min(processedTail.length, buffered.length); size > 0; size--) {
17
+ const tail = processedTail.slice(-size);
18
+ const matches = (items) => {
19
+ const ends = [];
20
+ for (let start = 0; start <= items.length - size; start++)
21
+ if (tail.every((value, offset) => items[start + offset] === value))
22
+ ends.push(start + size);
23
+ return ends;
24
+ };
25
+ if (matches(processedTail).length !== 1)
26
+ continue;
27
+ const ends = matches(buffered);
28
+ if (ends.length === 1)
29
+ return ends[0];
30
+ }
31
+ throw Error("MQTT input overlap is missing or ambiguous; successor stays passive");
32
+ }
33
+ //# sourceMappingURL=input-queue-boundary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input-queue-boundary.js","sourceRoot":"","sources":["../../src/runtime/input-queue-boundary.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AACvF,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,YAAY,CAAC;IAC9C,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;IAC3C,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,gBAAgB,CAAC;IACjD,aAAa,EAAE,kBAAkB,CAAC,QAAQ,EAAE;CAC7C,CAAC,CAAC;AAEH,MAAM,UAAU,gBAAgB,CAAC,IAAwC;IACvE,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACjH,CAAC;AAED,oGAAoG;AACpG,MAAM,UAAU,mBAAmB,CAAC,aAAgC,EAAE,QAA2B;IAC/F,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACzC,KAAK,IAAI,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,GAAG,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC;QAClF,MAAM,IAAI,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC;QACxC,MAAM,OAAO,GAAG,CAAC,KAAwB,EAAE,EAAE;YAC3C,MAAM,IAAI,GAAa,EAAE,CAAC;YAC1B,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,IAAI,KAAK,CAAC,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE;gBACvD,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,KAAK,CAAC;oBAAE,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;YAC9F,OAAO,IAAI,CAAC;QACd,CAAC,CAAC;QACF,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAClD,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC/B,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC;IACxC,CAAC;IACD,MAAM,KAAK,CAAC,qEAAqE,CAAC,CAAC;AACrF,CAAC","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { z } from \"zod\";\n\nexport const inputPreviewSchema = z.array(z.string().regex(/^[a-f0-9]{64}$/)).max(100);\nexport const inputCursorSchema = z.strictObject({\n batchSize: z.number().int().min(0).max(100),\n referenceHash: z.string().regex(/^[a-f0-9]{64}$/),\n processedTail: inputPreviewSchema.optional(),\n});\nexport type InputQueueCursor = z.infer<typeof inputCursorSchema>;\nexport function inputFingerprint(item: { topic: string; message: string }): string {\n return createHash(\"sha256\").update(JSON.stringify({ topic: item.topic, message: item.message })).digest(\"hex\");\n}\n\n/** Find an ordered common suffix. Neither a timestamp nor equal values alone authorize skipping. */\nexport function uniqueInputBoundary(processedTail: readonly string[], buffered: readonly string[]): number {\n if (processedTail.length === 0) return 0;\n for (let size = Math.min(processedTail.length, buffered.length); size > 0; size--) {\n const tail = processedTail.slice(-size);\n const matches = (items: readonly string[]) => {\n const ends: number[] = [];\n for (let start = 0; start <= items.length - size; start++)\n if (tail.every((value, offset) => items[start + offset] === value)) ends.push(start + size);\n return ends;\n };\n if (matches(processedTail).length !== 1) continue;\n const ends = matches(buffered);\n if (ends.length === 1) return ends[0];\n }\n throw Error(\"MQTT input overlap is missing or ambiguous; successor stays passive\");\n}\n"]}
@@ -1,9 +1,10 @@
1
1
  import { z } from 'zod';
2
- export declare const MQTT_HANDOVER_STATE_FORMAT = 2;
2
+ export declare const MQTT_HANDOVER_STATE_FORMAT = 3;
3
3
  export declare const MQTT_HANDOVER_STATE_MAX_BYTES: number;
4
4
  export declare const mqttHandoverOfferSchema: z.ZodArray<z.ZodObject<{
5
5
  instanceName: z.ZodString;
6
6
  compatibility: z.ZodString;
7
+ inputPreview: z.ZodOptional<z.ZodArray<z.ZodString>>;
7
8
  }, z.core.$strict>>;
8
9
  export declare const mqttProxyHandoverStateSchema: z.ZodObject<{
9
10
  instanceName: z.ZodString;
@@ -18,6 +19,7 @@ export declare const mqttProxyHandoverStateSchema: z.ZodObject<{
18
19
  cursor: z.ZodObject<{
19
20
  batchSize: z.ZodNumber;
20
21
  referenceHash: z.ZodString;
22
+ processedTail: z.ZodOptional<z.ZodArray<z.ZodString>>;
21
23
  }, z.core.$strict>;
22
24
  }, z.core.$strict>;
23
25
  export declare const mqttHandoverStateSchema: z.ZodObject<{
@@ -39,6 +41,7 @@ export declare const mqttHandoverStateSchema: z.ZodObject<{
39
41
  cursor: z.ZodObject<{
40
42
  batchSize: z.ZodNumber;
41
43
  referenceHash: z.ZodString;
44
+ processedTail: z.ZodOptional<z.ZodArray<z.ZodString>>;
42
45
  }, z.core.$strict>;
43
46
  }, z.core.$strict>>;
44
47
  }, z.core.$strict>;
@@ -1 +1 @@
1
- {"version":3,"file":"mqtt-handover-state.d.ts","sourceRoot":"","sources":["../../src/runtime/mqtt-handover-state.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,eAAO,MAAM,0BAA0B,IAAI,CAAC;AAC5C,eAAO,MAAM,6BAA6B,QAAc,CAAC;AAIzD,eAAO,MAAM,uBAAuB;;;mBAG8E,CAAC;AACnH,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;kBASvC,CAAC;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;kBAOlC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAClF,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AACxE,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,iBAAiB,CAMxE;AACD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,iBAAiB,GAAG,MAAM,CAExE;AACD,eAAO,MAAM,4BAA4B;;kBAAoD,CAAC;AAC9F,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAS7E"}
1
+ {"version":3,"file":"mqtt-handover-state.d.ts","sourceRoot":"","sources":["../../src/runtime/mqtt-handover-state.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,eAAO,MAAM,0BAA0B,IAAI,CAAC;AAC5C,eAAO,MAAM,6BAA6B,QAAc,CAAC;AAIzD,eAAO,MAAM,uBAAuB;;;;mBAI8E,CAAC;AACnH,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;;kBASvC,CAAC;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;kBAOlC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAClF,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AACxE,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,iBAAiB,CAMxE;AACD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,iBAAiB,GAAG,MAAM,CAExE;AACD,eAAO,MAAM,4BAA4B;;kBAAoD,CAAC;AAC9F,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAS7E"}
@@ -3,9 +3,10 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { z } from 'zod';
5
5
  import { cloneApplicationHandoverState } from './application-handover-state.js';
6
+ import { inputCursorSchema, inputPreviewSchema } from './input-queue-boundary.js';
6
7
  // Same contract for every SDK MQTT consumer. No application/profile selectors.
7
- // Bound into peer compatibility so older required-unit readers reject before freeze.
8
- export const MQTT_HANDOVER_STATE_FORMAT = 2;
8
+ // Bound into peer compatibility so older cursor/state readers reject before freeze.
9
+ export const MQTT_HANDOVER_STATE_FORMAT = 3;
9
10
  export const MQTT_HANDOVER_STATE_MAX_BYTES = 1024 * 1024;
10
11
  const text = z.string().min(1).max(65535).refine(v => !v.includes('\0'));
11
12
  const scalar = z.union([z.number().finite(), z.string().max(65535)]);
@@ -13,13 +14,14 @@ const value = z.union([scalar, z.array(scalar).max(4096)]);
13
14
  export const mqttHandoverOfferSchema = z.array(z.strictObject({
14
15
  instanceName: z.string().min(1).max(256),
15
16
  compatibility: z.string().regex(/^[a-f0-9]{64}$/),
17
+ inputPreview: inputPreviewSchema.optional(),
16
18
  })).max(64).refine(offer => new Set(offer.map(p => p.instanceName)).size === offer.length, 'Duplicate MQTT proxy');
17
19
  export const mqttProxyHandoverStateSchema = z.strictObject({
18
20
  instanceName: z.string().min(1).max(256),
19
21
  compatibility: z.string().regex(/^[a-f0-9]{64}$/),
20
22
  values: z.array(z.strictObject({ topic: text, value, uom: z.string().max(256).optional(), timestamp: z.string().datetime() })).max(10000),
21
23
  sequences: z.array(z.tuple([text, z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER)])).max(10000),
22
- cursor: z.strictObject({ batchSize: z.number().int().min(0).max(100), referenceHash: z.string().regex(/^[a-f0-9]{64}$/) }),
24
+ cursor: inputCursorSchema,
23
25
  }).superRefine((state, ctx) => {
24
26
  for (const keys of [state.values.map(v => v.topic), state.sequences.map(v => v[0])])
25
27
  if (new Set(keys).size !== keys.length)
@@ -1 +1 @@
1
- {"version":3,"file":"mqtt-handover-state.js","sourceRoot":"","sources":["../../src/runtime/mqtt-handover-state.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { z } from 'zod';\nimport { cloneApplicationHandoverState } from './application-handover-state.js';\n\n// Same contract for every SDK MQTT consumer. No application/profile selectors.\n// Bound into peer compatibility so older required-unit readers reject before freeze.\nexport const MQTT_HANDOVER_STATE_FORMAT = 2;\nexport const MQTT_HANDOVER_STATE_MAX_BYTES = 1024 * 1024;\nconst text = z.string().min(1).max(65535).refine(v => !v.includes('\\0'));\nconst scalar = z.union([z.number().finite(), z.string().max(65535)]);\nconst value = z.union([scalar, z.array(scalar).max(4096)]);\nexport const mqttHandoverOfferSchema = z.array(z.strictObject({\n instanceName: z.string().min(1).max(256),\n compatibility: z.string().regex(/^[a-f0-9]{64}$/),\n})).max(64).refine(offer => new Set(offer.map(p => p.instanceName)).size === offer.length, 'Duplicate MQTT proxy');\nexport const mqttProxyHandoverStateSchema = z.strictObject({\n instanceName: z.string().min(1).max(256),\n compatibility: z.string().regex(/^[a-f0-9]{64}$/),\n values: z.array(z.strictObject({ topic: text, value, uom: z.string().max(256).optional(), timestamp: z.string().datetime() })).max(10000),\n sequences: z.array(z.tuple([text, z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER)])).max(10000),\n cursor: z.strictObject({ batchSize: z.number().int().min(0).max(100), referenceHash: z.string().regex(/^[a-f0-9]{64}$/) }),\n}).superRefine((state, ctx) => {\n for (const keys of [state.values.map(v => v.topic), state.sequences.map(v => v[0])])\n if (new Set(keys).size !== keys.length) ctx.addIssue({ code: 'custom', message: 'Duplicate MQTT state key' });\n});\nexport const mqttHandoverStateSchema = z.strictObject({\n protocol: z.literal(1),\n application: z.strictObject({ contract: z.string().min(1).max(128), data: z.unknown() }).optional(),\n proxies: z.array(mqttProxyHandoverStateSchema).max(64),\n}).superRefine((state, ctx) => {\n if (new Set(state.proxies.map(p => p.instanceName)).size !== state.proxies.length)\n ctx.addIssue({ code: 'custom', message: 'Duplicate MQTT proxy' });\n});\nexport type MqttProxyHandoverState = z.infer<typeof mqttProxyHandoverStateSchema>;\nexport type MqttHandoverState = z.infer<typeof mqttHandoverStateSchema>;\nexport function parseMqttHandoverState(input: unknown): MqttHandoverState {\n const json = JSON.stringify(input);\n if (!json || Buffer.byteLength(json) > MQTT_HANDOVER_STATE_MAX_BYTES) throw Error('MQTT handover state exceeds its bound');\n const state = mqttHandoverStateSchema.parse(input);\n if (state.application) state.application.data = cloneApplicationHandoverState(state.application.data);\n return state;\n}\nexport function mqttHandoverStateDigest(state: MqttHandoverState): string {\n return createHash('sha256').update(JSON.stringify(parseMqttHandoverState(state))).digest('hex');\n}\nexport const mqttHandoverCapabilitySchema = z.strictObject({ protocolVersion: z.literal(2) });\nexport function readMqttHandoverCapability(packageRoot: string): 2 | undefined {\n const file = path.join(packageRoot, 'package.json');\n let stat: fs.Stats;\n try { stat = fs.lstatSync(file); } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined; throw error; }\n if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1048576) throw Error('Unsafe MQTT handover descriptor');\n const descriptor = JSON.parse(fs.readFileSync(file, 'utf8'))?.unsDatahub?.mqttHandover;\n if (descriptor === undefined) return undefined;\n mqttHandoverCapabilitySchema.parse(descriptor);\n return 2;\n}\n"]}
1
+ {"version":3,"file":"mqtt-handover-state.js","sourceRoot":"","sources":["../../src/runtime/mqtt-handover-state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,6BAA6B,EAAE,MAAM,iCAAiC,CAAC;AAChF,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAElF,+EAA+E;AAC/E,oFAAoF;AACpF,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC;AAC5C,MAAM,CAAC,MAAM,6BAA6B,GAAG,IAAI,GAAG,IAAI,CAAC;AACzD,MAAM,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;AACzE,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACrE,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAC3D,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC;IAC5D,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;IACxC,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,gBAAgB,CAAC;IACjD,YAAY,EAAE,kBAAkB,CAAC,QAAQ,EAAE;CAC5C,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;AACnH,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC,CAAC,YAAY,CAAC;IACzD,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;IACxC,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,gBAAgB,CAAC;IACjD,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC;IACzI,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC;IAC3G,MAAM,EAAE,iBAAiB;CAC1B,CAAC,CAAC,WAAW,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;IAC5B,KAAK,MAAM,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACjF,IAAI,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,MAAM;YAAE,GAAG,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,0BAA0B,EAAE,CAAC,CAAC;AAClH,CAAC,CAAC,CAAC;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,YAAY,CAAC;IACpD,QAAQ,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IACtB,WAAW,EAAE,CAAC,CAAC,YAAY,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,QAAQ,EAAE;IACnG,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,4BAA4B,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;CACvD,CAAC,CAAC,WAAW,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;IAC5B,IAAI,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,OAAO,CAAC,MAAM;QAC/E,GAAG,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,sBAAsB,EAAE,CAAC,CAAC;AACtE,CAAC,CAAC,CAAC;AAGH,MAAM,UAAU,sBAAsB,CAAC,KAAc;IACnD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACnC,IAAI,CAAC,IAAI,IAAI,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,6BAA6B;QAAE,MAAM,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC3H,MAAM,KAAK,GAAG,uBAAuB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACnD,IAAI,KAAK,CAAC,WAAW;QAAE,KAAK,CAAC,WAAW,CAAC,IAAI,GAAG,6BAA6B,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IACtG,OAAO,KAAK,CAAC;AACf,CAAC;AACD,MAAM,UAAU,uBAAuB,CAAC,KAAwB;IAC9D,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,sBAAsB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAClG,CAAC;AACD,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC,CAAC,YAAY,CAAC,EAAE,eAAe,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AAC9F,MAAM,UAAU,0BAA0B,CAAC,WAAmB;IAC5D,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC;IACpD,IAAI,IAAc,CAAC;IACnB,IAAI,CAAC;QAAC,IAAI,GAAG,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IAAC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAAC,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC;QAAC,MAAM,KAAK,CAAC;IAAC,CAAC;IAC3I,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,IAAI,CAAC,cAAc,EAAE,IAAI,IAAI,CAAC,IAAI,GAAG,OAAO;QAAE,MAAM,KAAK,CAAC,iCAAiC,CAAC,CAAC;IACnH,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,EAAE,UAAU,EAAE,YAAY,CAAC;IACvF,IAAI,UAAU,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC/C,4BAA4B,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IAC/C,OAAO,CAAC,CAAC;AACX,CAAC","sourcesContent":["import { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { z } from 'zod';\nimport { cloneApplicationHandoverState } from './application-handover-state.js';\nimport { inputCursorSchema, inputPreviewSchema } from './input-queue-boundary.js';\n\n// Same contract for every SDK MQTT consumer. No application/profile selectors.\n// Bound into peer compatibility so older cursor/state readers reject before freeze.\nexport const MQTT_HANDOVER_STATE_FORMAT = 3;\nexport const MQTT_HANDOVER_STATE_MAX_BYTES = 1024 * 1024;\nconst text = z.string().min(1).max(65535).refine(v => !v.includes('\\0'));\nconst scalar = z.union([z.number().finite(), z.string().max(65535)]);\nconst value = z.union([scalar, z.array(scalar).max(4096)]);\nexport const mqttHandoverOfferSchema = z.array(z.strictObject({\n instanceName: z.string().min(1).max(256),\n compatibility: z.string().regex(/^[a-f0-9]{64}$/),\n inputPreview: inputPreviewSchema.optional(),\n})).max(64).refine(offer => new Set(offer.map(p => p.instanceName)).size === offer.length, 'Duplicate MQTT proxy');\nexport const mqttProxyHandoverStateSchema = z.strictObject({\n instanceName: z.string().min(1).max(256),\n compatibility: z.string().regex(/^[a-f0-9]{64}$/),\n values: z.array(z.strictObject({ topic: text, value, uom: z.string().max(256).optional(), timestamp: z.string().datetime() })).max(10000),\n sequences: z.array(z.tuple([text, z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER)])).max(10000),\n cursor: inputCursorSchema,\n}).superRefine((state, ctx) => {\n for (const keys of [state.values.map(v => v.topic), state.sequences.map(v => v[0])])\n if (new Set(keys).size !== keys.length) ctx.addIssue({ code: 'custom', message: 'Duplicate MQTT state key' });\n});\nexport const mqttHandoverStateSchema = z.strictObject({\n protocol: z.literal(1),\n application: z.strictObject({ contract: z.string().min(1).max(128), data: z.unknown() }).optional(),\n proxies: z.array(mqttProxyHandoverStateSchema).max(64),\n}).superRefine((state, ctx) => {\n if (new Set(state.proxies.map(p => p.instanceName)).size !== state.proxies.length)\n ctx.addIssue({ code: 'custom', message: 'Duplicate MQTT proxy' });\n});\nexport type MqttProxyHandoverState = z.infer<typeof mqttProxyHandoverStateSchema>;\nexport type MqttHandoverState = z.infer<typeof mqttHandoverStateSchema>;\nexport function parseMqttHandoverState(input: unknown): MqttHandoverState {\n const json = JSON.stringify(input);\n if (!json || Buffer.byteLength(json) > MQTT_HANDOVER_STATE_MAX_BYTES) throw Error('MQTT handover state exceeds its bound');\n const state = mqttHandoverStateSchema.parse(input);\n if (state.application) state.application.data = cloneApplicationHandoverState(state.application.data);\n return state;\n}\nexport function mqttHandoverStateDigest(state: MqttHandoverState): string {\n return createHash('sha256').update(JSON.stringify(parseMqttHandoverState(state))).digest('hex');\n}\nexport const mqttHandoverCapabilitySchema = z.strictObject({ protocolVersion: z.literal(2) });\nexport function readMqttHandoverCapability(packageRoot: string): 2 | undefined {\n const file = path.join(packageRoot, 'package.json');\n let stat: fs.Stats;\n try { stat = fs.lstatSync(file); } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined; throw error; }\n if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1048576) throw Error('Unsafe MQTT handover descriptor');\n const descriptor = JSON.parse(fs.readFileSync(file, 'utf8'))?.unsDatahub?.mqttHandover;\n if (descriptor === undefined) return undefined;\n mqttHandoverCapabilitySchema.parse(descriptor);\n return 2;\n}\n"]}
@@ -34,7 +34,10 @@ export declare class HandoverManager {
34
34
  private stateDigest;
35
35
  private acceptedStateDigest;
36
36
  private targetPreparation;
37
+ private targetStateApplied;
37
38
  private finRetry;
39
+ private requestRetry;
40
+ private sourceReviewInFlight;
38
41
  handoverRequestEnabled: boolean;
39
42
  handoverEnabled: boolean;
40
43
  forceStartEnabled: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"handover-manager.d.ts","sourceRoot":"","sources":["../../src/uns/handover-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiC,KAAK,wBAAwB,EAAE,MAAM,0CAA0C,CAAC;AAGxH,OAAO,EAAiD,KAAK,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AACnH,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,SAAS,MAAM,2BAA2B,CAAC;AAElD,OAAO,YAAY,MAAM,+BAA+B,CAAC;AACzD,OAAO,EAAE,2BAA2B,EAAE,MAAM,qCAAqC,CAAC;AAElF,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAQhD;;;;GAIG;AACH,qBAAa,eAAe;IAqCxB,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC;IArC7B,KAAK,EAAE,2BAA2B,CAAC,qBAAqB,CAAC,CAA4D;IAC5H,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,kBAAkB,CAAkB;IAC5C,OAAO,CAAC,kBAAkB,CAAkB;IAC5C,OAAO,CAAC,YAAY,CAAmB;IACvC,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAqB;IAChD,OAAO,CAAC,kBAAkB,CAAqB;IAC/C,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,iBAAiB,CAA4B;IACrD,OAAO,CAAC,UAAU,CAA4B;IAC9C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAoC;IAClE,OAAO,CAAC,gBAAgB,CAAqB;IAC7C,OAAO,CAAC,aAAa,CAAgC;IACrD,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,mBAAmB,CAAqB;IAChD,OAAO,CAAC,iBAAiB,CAA4B;IACrD,OAAO,CAAC,QAAQ,CAA6B;IACtC,sBAAsB,EAAE,OAAO,CAAS;IACxC,eAAe,EAAE,OAAO,CAAQ;IAChC,iBAAiB,EAAE,OAAO,CAAS;gBAGxC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,SAAS,EACpB,cAAc,EAAE,YAAY,EAAE,EAC9B,sBAAsB,EAAE,OAAO,EAC/B,eAAe,EAAE,OAAO,EACxB,iBAAiB,EAAE,OAAO,EAC1B,UAAU,CAAC,EAAE,MAAM,EACnB,aAAa,CAAC,EAAE,qBAAqB,EACpB,gBAAgB,UAAQ,EACxB,gBAAgB,CAAC,EAAE,wBAAwB;IAkC9D,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,eAAe;IAcvB,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,uBAAuB;IAW/B,OAAO,CAAC,kBAAkB;IAI1B,OAAO,CAAC,gBAAgB;IAgBxB,OAAO,CAAC,oBAAoB;IAU5B;;;OAGG;IACU,iBAAiB,CAAC,KAAK,EAAE,SAAS,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAoFxE;;OAEG;IACH,OAAO,CAAC,UAAU;YAIJ,qBAAqB;YAiFrB,iBAAiB;YAOjB,mBAAmB;YA2BnB,mBAAmB;YAQnB,qBAAqB;YAWrB,oBAAoB;YA2BpB,cAAc;CA6H7B"}
1
+ {"version":3,"file":"handover-manager.d.ts","sourceRoot":"","sources":["../../src/uns/handover-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiC,KAAK,wBAAwB,EAAE,MAAM,0CAA0C,CAAC;AAGxH,OAAO,EAAiD,KAAK,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AACnH,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,SAAS,MAAM,2BAA2B,CAAC;AAElD,OAAO,YAAY,MAAM,+BAA+B,CAAC;AACzD,OAAO,EAAE,2BAA2B,EAAE,MAAM,qCAAqC,CAAC;AAElF,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAQhD;;;;GAIG;AACH,qBAAa,eAAe;IAwCxB,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC;IAxC7B,KAAK,EAAE,2BAA2B,CAAC,qBAAqB,CAAC,CAA4D;IAC5H,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,kBAAkB,CAAkB;IAC5C,OAAO,CAAC,kBAAkB,CAAkB;IAC5C,OAAO,CAAC,YAAY,CAAmB;IACvC,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAqB;IAChD,OAAO,CAAC,kBAAkB,CAAqB;IAC/C,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,iBAAiB,CAA4B;IACrD,OAAO,CAAC,UAAU,CAA4B;IAC9C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAoC;IAClE,OAAO,CAAC,gBAAgB,CAAqB;IAC7C,OAAO,CAAC,aAAa,CAAgC;IACrD,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,mBAAmB,CAAqB;IAChD,OAAO,CAAC,iBAAiB,CAA4B;IACrD,OAAO,CAAC,kBAAkB,CAAS;IACnC,OAAO,CAAC,QAAQ,CAA6B;IAC7C,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,oBAAoB,CAAS;IAC9B,sBAAsB,EAAE,OAAO,CAAS;IACxC,eAAe,EAAE,OAAO,CAAQ;IAChC,iBAAiB,EAAE,OAAO,CAAS;gBAGxC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,SAAS,EACpB,cAAc,EAAE,YAAY,EAAE,EAC9B,sBAAsB,EAAE,OAAO,EAC/B,eAAe,EAAE,OAAO,EACxB,iBAAiB,EAAE,OAAO,EAC1B,UAAU,CAAC,EAAE,MAAM,EACnB,aAAa,CAAC,EAAE,qBAAqB,EACpB,gBAAgB,UAAQ,EACxB,gBAAgB,CAAC,EAAE,wBAAwB;IAkC9D,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,eAAe;IAcvB,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,uBAAuB;IAW/B,OAAO,CAAC,kBAAkB;IAI1B,OAAO,CAAC,gBAAgB;IAgBxB,OAAO,CAAC,oBAAoB;IAU5B;;;OAGG;IACU,iBAAiB,CAAC,KAAK,EAAE,SAAS,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IA+FxE;;OAEG;IACH,OAAO,CAAC,UAAU;YAIJ,qBAAqB;YAiFrB,iBAAiB;YAOjB,mBAAmB;YA8BnB,mBAAmB;YAQnB,qBAAqB;YAWrB,oBAAoB;YA2BpB,cAAc;CA+I7B"}
@@ -40,7 +40,10 @@ export class HandoverManager {
40
40
  stateDigest;
41
41
  acceptedStateDigest;
42
42
  targetPreparation;
43
+ targetStateApplied = false;
43
44
  finRetry;
45
+ requestRetry;
46
+ sourceReviewInFlight = false;
44
47
  handoverRequestEnabled = false;
45
48
  handoverEnabled = true;
46
49
  forceStartEnabled = false;
@@ -191,16 +194,35 @@ export class HandoverManager {
191
194
  },
192
195
  });
193
196
  logger.info(`${this.processName} - Requesting handover in 10 seconds.`);
194
- setTimeout(async () => {
195
- logger.info(`${this.processName} - Requesting handover ${eventHandoverTopic}.`);
197
+ setTimeout(() => {
196
198
  this.handoverInProgress = true;
197
- await this.mqttProxy.publish(eventHandoverTopic, this.handoverPayload("handover_request", undefined, this.statefulHandover ? { offer: this.unsMqttProxies.map(proxy => proxy.getHandoverCompatibility()), applicationContract: this.applicationState?.contract } : {}), {
198
- retain: false,
199
- properties: {
200
- responseTopic: this.topicBuilder.getHandoverTopic(),
201
- userProperties: this.getUserProperties(),
202
- },
203
- });
199
+ let retries = 0;
200
+ const request = async () => {
201
+ if (this.targetPreparation || this.acceptedStateDigest) {
202
+ clearInterval(this.requestRetry);
203
+ return;
204
+ }
205
+ try {
206
+ const offer = this.statefulHandover ? await Promise.all(this.unsMqttProxies.map(proxy => proxy.getHandoverOffer())) : undefined;
207
+ await this.mqttProxy.publish(eventHandoverTopic, this.handoverPayload("handover_request", undefined, this.statefulHandover ? { offer, applicationContract: this.applicationState?.contract } : {}), {
208
+ retain: false, properties: { responseTopic: this.topicBuilder.getHandoverTopic(), userProperties: this.getUserProperties() },
209
+ });
210
+ }
211
+ catch {
212
+ logger.warn("MQTT input boundary preparation is not ready; successor remains passive");
213
+ }
214
+ };
215
+ void request();
216
+ if (this.statefulHandover) {
217
+ this.requestRetry = setInterval(() => {
218
+ if (++retries > 15 || this.targetPreparation || this.acceptedStateDigest) {
219
+ clearInterval(this.requestRetry);
220
+ return;
221
+ }
222
+ void request();
223
+ }, 1000);
224
+ this.requestRetry.unref();
225
+ }
204
226
  }, 10000);
205
227
  }
206
228
  else {
@@ -265,7 +287,7 @@ export class HandoverManager {
265
287
  data: cloneApplicationHandoverState(this.applicationState.exportState()) } : undefined;
266
288
  if (application)
267
289
  this.applicationState.validateState(application.data);
268
- this.preparedState = parseMqttHandoverState({ protocol: 1, ...(application ? { application } : {}), proxies: this.unsMqttProxies.map((proxy, i) => proxy.exportHandoverState({ batchSize: mqttWorkerData[i].batchSize, referenceHash: mqttWorkerData[i].referenceHash })) });
290
+ this.preparedState = parseMqttHandoverState({ protocol: 1, ...(application ? { application } : {}), proxies: this.unsMqttProxies.map((proxy, i) => proxy.exportHandoverState({ batchSize: mqttWorkerData[i].batchSize, referenceHash: mqttWorkerData[i].referenceHash, ...(mqttWorkerData[i].processedTail ? { processedTail: mqttWorkerData[i].processedTail } : {}) })) });
269
291
  this.stateDigest = mqttHandoverStateDigest(this.preparedState);
270
292
  await this.sendPreparedState(event, handoverId);
271
293
  let retries = 0;
@@ -333,6 +355,9 @@ export class HandoverManager {
333
355
  // Validate every cursor before importing any state or activating any subscriber.
334
356
  for (const snapshot of state.proxies)
335
357
  await byName.get(snapshot.instanceName).prepareHandoverResume(snapshot.cursor);
358
+ // After this point an import/activation error has an unknown effect. Only
359
+ // read-only boundary preparation may be retried on a repeated FIN.
360
+ this.targetStateApplied = true;
336
361
  for (const snapshot of state.proxies)
337
362
  byName.get(snapshot.instanceName).importHandoverState(snapshot);
338
363
  if (state.application)
@@ -434,6 +459,23 @@ export class HandoverManager {
434
459
  const own = this.unsMqttProxies.map(proxy => proxy.getHandoverCompatibility());
435
460
  if (offer.length !== own.length || own.some(p => !offer.some(o => o.instanceName === p.instanceName && o.compatibility === p.compatibility)))
436
461
  throw Error("MQTT source/destination or semantic contract differs; source remains active");
462
+ if (this.sourceReviewInFlight)
463
+ return;
464
+ this.sourceReviewInFlight = true;
465
+ try {
466
+ // Hints only: actual frozen cursors are revalidated by every target
467
+ // worker before activation. A quiet/unready source keeps admission.
468
+ for (const proxy of this.unsMqttProxies) {
469
+ const preview = offer.find(candidate => candidate.instanceName === proxy.instanceName).inputPreview;
470
+ if (!await proxy.canPrepareHandoverInput(preview))
471
+ return;
472
+ }
473
+ }
474
+ finally {
475
+ this.sourceReviewInFlight = false;
476
+ }
477
+ if (this.sourcePeerId || !this.active)
478
+ return;
437
479
  }
438
480
  this.sourcePeerId = peerId;
439
481
  this.acceptedHandoverId = this.getHandoverId(response, event);
@@ -477,7 +519,15 @@ export class HandoverManager {
477
519
  return;
478
520
  if (this.acceptedStateDigest && response.stateDigest !== this.acceptedStateDigest)
479
521
  return;
480
- this.targetPreparation ??= this.acceptPreparedState(response);
522
+ clearInterval(this.requestRetry);
523
+ if (!this.targetPreparation) {
524
+ const preparation = this.acceptPreparedState(response);
525
+ this.targetPreparation = preparation;
526
+ void preparation.catch(() => {
527
+ if (!this.targetStateApplied && this.targetPreparation === preparation)
528
+ this.targetPreparation = undefined;
529
+ });
530
+ }
481
531
  await this.targetPreparation;
482
532
  // Duplicate fin only replays the receipt; it never reapplies a baseline.
483
533
  if (response.stateDigest !== this.acceptedStateDigest)