@things-factory/headless-twin 10.1.37 → 10.1.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.
- package/dist-server/engine/twin-engine.js +3 -2
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/engine/twin-state-channel.d.ts +14 -0
- package/dist-server/engine/twin-state-channel.js +31 -0
- package/dist-server/engine/twin-state-channel.js.map +1 -0
- package/dist-server/service/reference/reference-progress-channel.d.ts +14 -0
- package/dist-server/service/reference/reference-progress-channel.js +24 -0
- package/dist-server/service/reference/reference-progress-channel.js.map +1 -0
- package/dist-server/service/reference/reference-progress-subscription.d.ts +2 -3
- package/dist-server/service/reference/reference-progress-subscription.js +14 -6
- package/dist-server/service/reference/reference-progress-subscription.js.map +1 -1
- package/dist-server/service/reference/reference-resolver.js +3 -6
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/twin-forecast/gap-analytics.d.ts +10 -1
- package/dist-server/service/twin-forecast/gap-analytics.js +15 -6
- package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
- package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-item-query.js +6 -1
- package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
- package/dist-server/service/twin-state/twin-state-subscription.d.ts +2 -3
- package/dist-server/service/twin-state/twin-state-subscription.js +20 -13
- package/dist-server/service/twin-state/twin-state-subscription.js.map +1 -1
- package/package.json +8 -8
- package/server/engine/twin-engine.ts +6 -5
- package/server/engine/twin-state-channel.ts +39 -0
- package/server/service/reference/reference-progress-channel.ts +26 -0
- package/server/service/reference/reference-progress-subscription.ts +15 -10
- package/server/service/reference/reference-resolver.ts +6 -6
- package/server/service/twin-forecast/gap-analytics.ts +15 -6
- package/server/service/twin-forecast/twin-forecast-query.ts +1 -1
- package/server/service/twin-model/twin-model-item-query.ts +6 -1
- package/server/service/twin-state/twin-state-subscription.ts +22 -19
- package/test/boot-resume.test.ts +4 -1
- package/test/gap-analytics.test.ts +25 -1
- package/test/open-doors.test.ts +197 -0
- package/test/twin-model-item-db.test.ts +25 -0
- package/test/twin-state-channel.test.ts +111 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ Two tenants, one twin name — each subscriber receives its own tenant's messages and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* The same instanceId (and the same reference source) in two tenants is the expected case: source
|
|
5
|
+
* names such as `sap-ewm-1710` are vocabulary tenants share (`runtime-key.ts`). Until 2026-09-24 both
|
|
6
|
+
* channels filtered on that name alone, so tenant B's subscriber received A's deltas, and the snapshot
|
|
7
|
+
* sent when A subscribed went to B as well.
|
|
8
|
+
*
|
|
9
|
+
* This counts what arrives. A filter test alone would not see a sender that builds its own payload
|
|
10
|
+
* without a domain, so the last two tests read the source: every sender goes through the channel's
|
|
11
|
+
* payload builder, and nothing else names the topic.
|
|
12
|
+
*/
|
|
13
|
+
import { test } from 'node:test'
|
|
14
|
+
import assert from 'node:assert/strict'
|
|
15
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
|
16
|
+
import { dirname, join, relative } from 'node:path'
|
|
17
|
+
import { fileURLToPath } from 'node:url'
|
|
18
|
+
|
|
19
|
+
import { TWIN_STATE_TOPIC, twinStateFilter, twinStatePayload } from '../server/engine/twin-state-channel.ts'
|
|
20
|
+
import {
|
|
21
|
+
REFERENCE_PROGRESS_TOPIC,
|
|
22
|
+
referenceProgressFilter,
|
|
23
|
+
referenceProgressPayload
|
|
24
|
+
} from '../server/service/reference/reference-progress-channel.ts'
|
|
25
|
+
|
|
26
|
+
const A = 'domain-a'
|
|
27
|
+
const B = 'domain-b'
|
|
28
|
+
const SHARED = 'sap-ewm-1710'
|
|
29
|
+
|
|
30
|
+
/* A pubsub reduced to what matters here: every published payload, and a subscriber's filter over them. */
|
|
31
|
+
function bus() {
|
|
32
|
+
const sent: { topic: string; payload: any }[] = []
|
|
33
|
+
return {
|
|
34
|
+
publish: (topic: string, payload: any) => void sent.push({ topic, payload }),
|
|
35
|
+
received: (topic: string, keep: (p: any) => boolean) => sent.filter(m => m.topic === topic).map(m => m.payload).filter(keep)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
test('twin-state: B receives none of A\'s snapshot or deltas for a twin of the same name', () => {
|
|
40
|
+
const b = bus()
|
|
41
|
+
/* A subscribes (its snapshot goes out), then both tenants' twins tick. */
|
|
42
|
+
b.publish(TWIN_STATE_TOPIC, twinStatePayload(A, { instanceId: SHARED, kind: 'snapshot', revision: 7, payload: { owner: 'A' } }))
|
|
43
|
+
b.publish(TWIN_STATE_TOPIC, twinStatePayload(A, { instanceId: SHARED, kind: 'delta', revision: 8 }))
|
|
44
|
+
b.publish(TWIN_STATE_TOPIC, twinStatePayload(B, { instanceId: SHARED, kind: 'delta', revision: 3 }))
|
|
45
|
+
b.publish(TWIN_STATE_TOPIC, twinStatePayload(B, { instanceId: 'other-twin', kind: 'delta', revision: 1 }))
|
|
46
|
+
|
|
47
|
+
const toB = b.received(TWIN_STATE_TOPIC, twinStateFilter(B, SHARED))
|
|
48
|
+
assert.deepEqual(toB.map(p => [p.domainId, p.twinState.kind, p.twinState.revision]), [[B, 'delta', 3]])
|
|
49
|
+
|
|
50
|
+
const toA = b.received(TWIN_STATE_TOPIC, twinStateFilter(A, SHARED))
|
|
51
|
+
assert.deepEqual(toA.map(p => [p.twinState.kind, p.twinState.revision]), [['snapshot', 7], ['delta', 8]])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('twin-reference-progress: B receives none of A\'s import progress for a source of the same name', () => {
|
|
55
|
+
const b = bus()
|
|
56
|
+
b.publish(REFERENCE_PROGRESS_TOPIC, referenceProgressPayload(A, { source: SHARED, step: 'master', done: 1, error: 'A: connection refused' }))
|
|
57
|
+
b.publish(REFERENCE_PROGRESS_TOPIC, referenceProgressPayload(B, { source: SHARED, step: 'master', done: 2 }))
|
|
58
|
+
|
|
59
|
+
const toB = b.received(REFERENCE_PROGRESS_TOPIC, referenceProgressFilter(B, SHARED))
|
|
60
|
+
assert.deepEqual(toB.map(p => [p.domainId, p.twinReferenceProgress.done]), [[B, 2]])
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('a payload cannot be built without a domain', () => {
|
|
64
|
+
assert.throws(() => twinStatePayload('', { instanceId: SHARED, kind: 'delta' }), /domainId is required/)
|
|
65
|
+
assert.throws(() => referenceProgressPayload('', { source: SHARED }), /domainId is required/)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
/* ── every sender goes through the channel ────────────────────────────────── */
|
|
69
|
+
|
|
70
|
+
const SERVER = join(dirname(fileURLToPath(import.meta.url)), '..', 'server')
|
|
71
|
+
const CHANNELS = new Set(['engine/twin-state-channel.ts', 'service/reference/reference-progress-channel.ts'])
|
|
72
|
+
|
|
73
|
+
function serverSources(): { path: string; code: string }[] {
|
|
74
|
+
const out: { path: string; code: string }[] = []
|
|
75
|
+
const walk = (dir: string) => {
|
|
76
|
+
for (const name of readdirSync(dir)) {
|
|
77
|
+
const p = join(dir, name)
|
|
78
|
+
if (statSync(p).isDirectory()) walk(p)
|
|
79
|
+
else if (p.endsWith('.ts')) {
|
|
80
|
+
/* Comments describe the old shape; count code only. */
|
|
81
|
+
const code = readFileSync(p, 'utf8').replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1')
|
|
82
|
+
out.push({ path: relative(SERVER, p), code })
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
walk(SERVER)
|
|
87
|
+
return out
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
test('no sender builds a channel payload by hand, and nothing names the topics but the channels', () => {
|
|
91
|
+
const offenders: string[] = []
|
|
92
|
+
for (const { path, code } of serverSources()) {
|
|
93
|
+
if (CHANNELS.has(path)) continue
|
|
94
|
+
if (/\btwinState\s*:\s*\{/.test(code)) offenders.push(`${path}: builds { twinState: … } by hand`)
|
|
95
|
+
if (/\btwinReferenceProgress\s*:\s*\{/.test(code)) offenders.push(`${path}: builds { twinReferenceProgress: … } by hand`)
|
|
96
|
+
if (/['"`]twin-state['"`]/.test(code)) offenders.push(`${path}: names 'twin-state'`)
|
|
97
|
+
if (/['"`]twin-reference-progress['"`]/.test(code)) offenders.push(`${path}: names 'twin-reference-progress'`)
|
|
98
|
+
}
|
|
99
|
+
assert.deepEqual(offenders, [])
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('the senders are really there — the scan above is not passing by finding nothing', () => {
|
|
103
|
+
const callers = serverSources()
|
|
104
|
+
.filter(s => !CHANNELS.has(s.path))
|
|
105
|
+
.map(s => s.code)
|
|
106
|
+
.join('\n')
|
|
107
|
+
/* engine: runtime subscription + flush; the subscription's first snapshot. */
|
|
108
|
+
assert.equal((callers.match(/twinStatePayload\(/g) ?? []).length, 3, 'twinStatePayload call sites')
|
|
109
|
+
/* resync and import. */
|
|
110
|
+
assert.equal((callers.match(/referenceProgressPayload\(/g) ?? []).length, 2, 'referenceProgressPayload call sites')
|
|
111
|
+
})
|