@things-factory/headless-twin 10.1.35 → 10.1.38

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 (34) hide show
  1. package/dist-server/engine/twin-engine.js +3 -2
  2. package/dist-server/engine/twin-engine.js.map +1 -1
  3. package/dist-server/engine/twin-state-channel.d.ts +14 -0
  4. package/dist-server/engine/twin-state-channel.js +31 -0
  5. package/dist-server/engine/twin-state-channel.js.map +1 -0
  6. package/dist-server/service/reference/reference-progress-channel.d.ts +14 -0
  7. package/dist-server/service/reference/reference-progress-channel.js +24 -0
  8. package/dist-server/service/reference/reference-progress-channel.js.map +1 -0
  9. package/dist-server/service/reference/reference-progress-subscription.d.ts +2 -3
  10. package/dist-server/service/reference/reference-progress-subscription.js +14 -6
  11. package/dist-server/service/reference/reference-progress-subscription.js.map +1 -1
  12. package/dist-server/service/reference/reference-resolver.js +3 -6
  13. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  14. package/dist-server/service/twin-forecast/gap-analytics.d.ts +10 -1
  15. package/dist-server/service/twin-forecast/gap-analytics.js +15 -6
  16. package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
  17. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  18. package/dist-server/service/twin-state/twin-state-subscription.d.ts +2 -3
  19. package/dist-server/service/twin-state/twin-state-subscription.js +20 -13
  20. package/dist-server/service/twin-state/twin-state-subscription.js.map +1 -1
  21. package/package.json +6 -6
  22. package/server/engine/twin-engine.ts +6 -5
  23. package/server/engine/twin-state-channel.ts +39 -0
  24. package/server/service/reference/reference-progress-channel.ts +26 -0
  25. package/server/service/reference/reference-progress-subscription.ts +15 -10
  26. package/server/service/reference/reference-resolver.ts +6 -6
  27. package/server/service/twin-forecast/gap-analytics.ts +15 -6
  28. package/server/service/twin-forecast/twin-forecast-query.ts +1 -1
  29. package/server/service/twin-state/twin-state-subscription.ts +22 -19
  30. package/test/boot-resume.test.ts +4 -1
  31. package/test/gap-analytics.test.ts +25 -1
  32. package/test/open-doors.test.ts +197 -0
  33. package/test/twin-state-channel.test.ts +111 -0
  34. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,14 @@
1
+ export declare const TWIN_STATE_TOPIC = "twin-state";
2
+ export interface TwinStateMessageShape {
3
+ instanceId: string;
4
+ kind: string;
5
+ revision?: number;
6
+ payload?: unknown;
7
+ }
8
+ export interface TwinStatePayload {
9
+ domainId: string;
10
+ twinState: TwinStateMessageShape;
11
+ }
12
+ export declare function twinStatePayload(domainId: string, twinState: TwinStateMessageShape): TwinStatePayload;
13
+ /** Passes only this tenant's messages about this twin. */
14
+ export declare function twinStateFilter(domainId: string, instanceId: string): (p: TwinStatePayload | undefined) => boolean;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ /*
3
+ * The `twin-state` channel — **a message says whose twin it is, and the filter asks.** Pure.
4
+ *
5
+ * ── What went wrong (2026-09-24) ─────────────────────────────────────────────
6
+ * A twin is `(domain, instanceId)` (`runtime-key.ts`), and the same instanceId in two tenants is the
7
+ * expected case — reference source names such as `sap-ewm-1710` are vocabulary tenants share. The
8
+ * channel had not caught up: publishers sent `{ twinState: { instanceId, … } }` with no domain, and the
9
+ * subscription filtered on instanceId alone. Ownership was checked once, when subscribing. So a tenant
10
+ * watching its own `sap-ewm-1710` also received the other tenant's deltas, and the snapshot sent to a
11
+ * new subscriber went to every subscriber of that id in every tenant.
12
+ *
13
+ * The rule is shell's `data` channel's (the message carries its domain, the filter compares it with the
14
+ * key), compared by id because the twin's key already is. Every sender builds its payload here, and
15
+ * `twin-state-channel.test.ts` fails if a sender builds one anywhere else.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.TWIN_STATE_TOPIC = void 0;
19
+ exports.twinStatePayload = twinStatePayload;
20
+ exports.twinStateFilter = twinStateFilter;
21
+ exports.TWIN_STATE_TOPIC = 'twin-state';
22
+ function twinStatePayload(domainId, twinState) {
23
+ if (!domainId)
24
+ throw new Error('twinStatePayload: domainId is required — a twin state message always belongs to a tenant');
25
+ return { domainId, twinState };
26
+ }
27
+ /** Passes only this tenant's messages about this twin. */
28
+ function twinStateFilter(domainId, instanceId) {
29
+ return (p) => !!p && p.domainId === domainId && p.twinState?.instanceId === instanceId;
30
+ }
31
+ //# sourceMappingURL=twin-state-channel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"twin-state-channel.js","sourceRoot":"","sources":["../../server/engine/twin-state-channel.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;AAgBH,4CAGC;AAGD,0CAEC;AAtBY,QAAA,gBAAgB,GAAG,YAAY,CAAA;AAc5C,SAAgB,gBAAgB,CAAC,QAAgB,EAAE,SAAgC;IACjF,IAAI,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,0FAA0F,CAAC,CAAA;IAC1H,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAA;AAChC,CAAC;AAED,0DAA0D;AAC1D,SAAgB,eAAe,CAAC,QAAgB,EAAE,UAAkB;IAClE,OAAO,CAAC,CAA+B,EAAW,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,CAAC,SAAS,EAAE,UAAU,KAAK,UAAU,CAAA;AAC/H,CAAC","sourcesContent":["/*\n * The `twin-state` channel — **a message says whose twin it is, and the filter asks.** Pure.\n *\n * ── What went wrong (2026-09-24) ─────────────────────────────────────────────\n * A twin is `(domain, instanceId)` (`runtime-key.ts`), and the same instanceId in two tenants is the\n * expected case — reference source names such as `sap-ewm-1710` are vocabulary tenants share. The\n * channel had not caught up: publishers sent `{ twinState: { instanceId, … } }` with no domain, and the\n * subscription filtered on instanceId alone. Ownership was checked once, when subscribing. So a tenant\n * watching its own `sap-ewm-1710` also received the other tenant's deltas, and the snapshot sent to a\n * new subscriber went to every subscriber of that id in every tenant.\n *\n * The rule is shell's `data` channel's (the message carries its domain, the filter compares it with the\n * key), compared by id because the twin's key already is. Every sender builds its payload here, and\n * `twin-state-channel.test.ts` fails if a sender builds one anywhere else.\n */\n\nexport const TWIN_STATE_TOPIC = 'twin-state'\n\nexport interface TwinStateMessageShape {\n instanceId: string\n kind: string\n revision?: number\n payload?: unknown\n}\n\nexport interface TwinStatePayload {\n domainId: string\n twinState: TwinStateMessageShape\n}\n\nexport function twinStatePayload(domainId: string, twinState: TwinStateMessageShape): TwinStatePayload {\n if (!domainId) throw new Error('twinStatePayload: domainId is required — a twin state message always belongs to a tenant')\n return { domainId, twinState }\n}\n\n/** Passes only this tenant's messages about this twin. */\nexport function twinStateFilter(domainId: string, instanceId: string) {\n return (p: TwinStatePayload | undefined): boolean => !!p && p.domainId === domainId && p.twinState?.instanceId === instanceId\n}\n"]}
@@ -0,0 +1,14 @@
1
+ export declare const REFERENCE_PROGRESS_TOPIC = "twin-reference-progress";
2
+ export interface ReferenceProgressPayload<M extends {
3
+ source: string;
4
+ } = {
5
+ source: string;
6
+ }> {
7
+ domainId: string;
8
+ twinReferenceProgress: M;
9
+ }
10
+ export declare function referenceProgressPayload<M extends {
11
+ source: string;
12
+ }>(domainId: string, message: M): ReferenceProgressPayload<M>;
13
+ /** Passes only this tenant's progress for this source. */
14
+ export declare function referenceProgressFilter(domainId: string, source: string): (p: ReferenceProgressPayload | undefined) => boolean;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ /*
3
+ * The `twin-reference-progress` channel — **progress says whose import it is, and the filter asks.** Pure.
4
+ *
5
+ * The subscription filtered on `source` alone and checked only that the caller had some domain, so
6
+ * anyone who knew a source name — and names like `sap-ewm-1710` are shared between tenants — received
7
+ * another tenant's import progress, including its failure text. Same fix and same rule as
8
+ * `engine/twin-state-channel.ts`.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.REFERENCE_PROGRESS_TOPIC = void 0;
12
+ exports.referenceProgressPayload = referenceProgressPayload;
13
+ exports.referenceProgressFilter = referenceProgressFilter;
14
+ exports.REFERENCE_PROGRESS_TOPIC = 'twin-reference-progress';
15
+ function referenceProgressPayload(domainId, message) {
16
+ if (!domainId)
17
+ throw new Error('referenceProgressPayload: domainId is required — import progress always belongs to a tenant');
18
+ return { domainId, twinReferenceProgress: message };
19
+ }
20
+ /** Passes only this tenant's progress for this source. */
21
+ function referenceProgressFilter(domainId, source) {
22
+ return (p) => !!p && p.domainId === domainId && p.twinReferenceProgress?.source === source;
23
+ }
24
+ //# sourceMappingURL=reference-progress-channel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reference-progress-channel.js","sourceRoot":"","sources":["../../../server/service/reference/reference-progress-channel.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;AASH,4DAGC;AAGD,0DAGC;AAhBY,QAAA,wBAAwB,GAAG,yBAAyB,CAAA;AAOjE,SAAgB,wBAAwB,CAA+B,QAAgB,EAAE,OAAU;IACjG,IAAI,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,6FAA6F,CAAC,CAAA;IAC7H,OAAO,EAAE,QAAQ,EAAE,qBAAqB,EAAE,OAAO,EAAE,CAAA;AACrD,CAAC;AAED,0DAA0D;AAC1D,SAAgB,uBAAuB,CAAC,QAAgB,EAAE,MAAc;IACtE,OAAO,CAAC,CAAuC,EAAW,EAAE,CAC1D,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,CAAC,qBAAqB,EAAE,MAAM,KAAK,MAAM,CAAA;AAChF,CAAC","sourcesContent":["/*\n * The `twin-reference-progress` channel — **progress says whose import it is, and the filter asks.** Pure.\n *\n * The subscription filtered on `source` alone and checked only that the caller had some domain, so\n * anyone who knew a source name — and names like `sap-ewm-1710` are shared between tenants — received\n * another tenant's import progress, including its failure text. Same fix and same rule as\n * `engine/twin-state-channel.ts`.\n */\n\nexport const REFERENCE_PROGRESS_TOPIC = 'twin-reference-progress'\n\nexport interface ReferenceProgressPayload<M extends { source: string } = { source: string }> {\n domainId: string\n twinReferenceProgress: M\n}\n\nexport function referenceProgressPayload<M extends { source: string }>(domainId: string, message: M): ReferenceProgressPayload<M> {\n if (!domainId) throw new Error('referenceProgressPayload: domainId is required — import progress always belongs to a tenant')\n return { domainId, twinReferenceProgress: message }\n}\n\n/** Passes only this tenant's progress for this source. */\nexport function referenceProgressFilter(domainId: string, source: string) {\n return (p: ReferenceProgressPayload | undefined): boolean =>\n !!p && p.domainId === domainId && p.twinReferenceProgress?.source === source\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import { type ReferenceProgressPayload } from './reference-progress-channel.js';
1
2
  export declare class TwinReferenceProgressMessage {
2
3
  source: string;
3
4
  siteId: string;
@@ -11,7 +12,5 @@ export declare class TwinReferenceProgressMessage {
11
12
  error?: string;
12
13
  }
13
14
  export declare class TwinReferenceProgressSubscription {
14
- twinReferenceProgress(payload: {
15
- twinReferenceProgress: TwinReferenceProgressMessage;
16
- }, source: string): TwinReferenceProgressMessage;
15
+ twinReferenceProgress(payload: ReferenceProgressPayload<TwinReferenceProgressMessage>, source: string): TwinReferenceProgressMessage;
17
16
  }
@@ -15,6 +15,8 @@ const type_graphql_1 = require("type-graphql");
15
15
  const shell_1 = require("@things-factory/shell");
16
16
  const graphql_yoga_1 = require("graphql-yoga");
17
17
  const reference_progress_js_1 = require("./reference-progress.js");
18
+ const reference_progress_channel_js_1 = require("./reference-progress-channel.js");
19
+ const twin_reference_js_1 = require("./twin-reference.js");
18
20
  let TwinReferenceProgressMessage = class TwinReferenceProgressMessage {
19
21
  };
20
22
  exports.TwinReferenceProgressMessage = TwinReferenceProgressMessage;
@@ -72,13 +74,19 @@ let TwinReferenceProgressSubscription = class TwinReferenceProgressSubscription
72
74
  exports.TwinReferenceProgressSubscription = TwinReferenceProgressSubscription;
73
75
  tslib_1.__decorate([
74
76
  (0, type_graphql_1.Subscription)(returns => TwinReferenceProgressMessage, {
75
- subscribe: ({ args, context }) => {
77
+ /*
78
+ * Tenancy is asked twice (2026-09-24): the source must be one of the caller's references, and every
79
+ * message must be the caller's domain's. It used to filter on `source` alone after checking only that
80
+ * the caller had a domain — and source names are shared between tenants.
81
+ */
82
+ subscribe: async ({ args, context }) => {
76
83
  const { source } = args;
77
- const domainId = context?.state?.domain?.id;
78
- /* 테넌트 격리 — 도메인이 없으면 빈 구독이다(다른 테넌트의 진행을 흘리지 않는다). */
79
- if (!domainId)
80
- return (0, graphql_yoga_1.pipe)(shell_1.pubsub.subscribe('twin-reference-progress'), (0, graphql_yoga_1.filter)(() => false));
81
- return (0, graphql_yoga_1.pipe)(shell_1.pubsub.subscribe('twin-reference-progress'), (0, graphql_yoga_1.filter)((p) => p.twinReferenceProgress.source === source));
84
+ const { domain, user } = context?.state ?? {};
85
+ await (0, shell_1.assertDomainSubscribeAllowed)(domain, user, (d, u) => process.superUserGranted(d, u));
86
+ const owned = await (0, shell_1.getRepository)(twin_reference_js_1.TwinReference).findOne({ where: { domain: { id: domain.id }, source } });
87
+ if (!owned)
88
+ throw new Error(`reference not found in this tenant: ${source}`);
89
+ return (0, graphql_yoga_1.pipe)(shell_1.pubsub.subscribe(reference_progress_channel_js_1.REFERENCE_PROGRESS_TOPIC), (0, graphql_yoga_1.filter)((0, reference_progress_channel_js_1.referenceProgressFilter)(domain.id, source)));
82
90
  },
83
91
  description: 'Progress while importing a reference — standard step names, so one translation per step covers every connector.'
84
92
  }),
@@ -1 +1 @@
1
- {"version":3,"file":"reference-progress-subscription.js","sourceRoot":"","sources":["../../../server/service/reference/reference-progress-subscription.ts"],"names":[],"mappings":";;;;AAAA;;;;;;;;GAQG;AACH,+CAAwF;AACxF,iDAA8C;AAC9C,+CAA2C;AAE3C,mEAAyD;AAGlD,IAAM,4BAA4B,GAAlC,MAAM,4BAA4B;CAiCxC,CAAA;AAjCY,oEAA4B;AAEvC;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,2CAA2C,EAAE,CAAC;;4DACtD;AAGd;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,4BAA4B,EAAE,CAAC;;4DACvC;AAGd;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,gCAAgC,uCAAe,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC;;0DAC3E;AAGZ;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,+BAA+B,EAAE,CAAC;;0DACzD;AAMZ;IAJC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE;QAClB,QAAQ,EAAE,IAAI;QACd,WAAW,EAAE,2IAA2I;KACzJ,CAAC;;2DACY;AAGd;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,0CAA0C,EAAE,CAAC;;+DAC/D;AAGjB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,iCAAiC,EAAE,CAAC;;+DACtD;AAGjB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,0DAA0D,EAAE,CAAC;;4DACpF;AAGf;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,kFAAkF,EAAE,CAAC;;4DAC3G;AAGhB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,yCAAyC,EAAE,CAAC;;2DACpE;uCAhCH,4BAA4B;IADxC,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,sEAAsE,EAAE,CAAC;GACvF,4BAA4B,CAiCxC;AAGM,IAAM,iCAAiC,GAAvC,MAAM,iCAAiC;IAc5C,qBAAqB,CACX,OAAgE,EACzD,MAAc;QAE7B,OAAO,OAAO,CAAC,qBAAqB,CAAA;IACtC,CAAC;CACF,CAAA;AApBY,8EAAiC;AAc5C;IAbC,IAAA,2BAAY,EAAC,OAAO,CAAC,EAAE,CAAC,4BAA4B,EAAE;QACrD,SAAS,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE;YAC/B,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;YACvB,MAAM,QAAQ,GAAI,OAAe,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,CAAA;YACpD,oDAAoD;YACpD,IAAI,CAAC,QAAQ;gBAAE,OAAO,IAAA,mBAAI,EAAC,cAAM,CAAC,SAAS,CAAC,yBAAyB,CAAC,EAAE,IAAA,qBAAM,EAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,CAAA;YAC5F,OAAO,IAAA,mBAAI,EACT,cAAM,CAAC,SAAS,CAAC,yBAAyB,CAAC,EAC3C,IAAA,qBAAM,EAAC,CAAC,CAA0D,EAAE,EAAE,CAAC,CAAC,CAAC,qBAAqB,CAAC,MAAM,KAAK,MAAM,CAAC,CAClH,CAAA;QACH,CAAC;QACD,WAAW,EAAE,iHAAiH;KAC/H,CAAC;IAEC,mBAAA,IAAA,mBAAI,GAAE,CAAA;IACN,mBAAA,IAAA,kBAAG,EAAC,QAAQ,CAAC,CAAA;;;4CACb,4BAA4B;8EAE9B;4CAnBU,iCAAiC;IAD7C,IAAA,uBAAQ,GAAE;GACE,iCAAiC,CAoB7C","sourcesContent":["/*\n * 유입 진행 구독 — **트윈을 만드는 동안 무엇을 하고 있는지 화면이 안다.**\n *\n * 마스터 읽기는 원본에 여러 질의를 보내는 긴 작업이고, 그 사이 화면은 아무것도 알 수 없었다. 단계를\n * 아는 것은 어댑터뿐이므로 어댑터가 **표준 낱말**로 말하고(`REFERENCE_STEPS`) 호스트가 자기 두 단계를\n * 더해 이 채널로 흘린다.\n *\n * 낱말이 표준이므로 화면은 **단계마다 번역 하나**만 두면 모든 커넥터를 보여 준다.\n */\nimport { Arg, Field, Int, ObjectType, Resolver, Root, Subscription } from 'type-graphql'\nimport { pubsub } from '@things-factory/shell'\nimport { filter, pipe } from 'graphql-yoga'\n\nimport { REFERENCE_STEPS } from './reference-progress.js'\n\n@ObjectType({ description: 'One step of progress while a reference is being imported into twins.' })\nexport class TwinReferenceProgressMessage {\n @Field({ description: 'Which reference (source) this belongs to.' })\n source: string\n\n @Field({ description: 'Which site is being built.' })\n siteId: string\n\n @Field({ description: `Standard step name — one of: ${REFERENCE_STEPS.join(' | ')}.` })\n step: string\n\n @Field(type => Int, { description: 'Steps finished for this site.' })\n done: number\n\n @Field(type => Int, {\n nullable: true,\n description: 'Total steps for this site. Absent when the adapter did not declare its steps — the UI then shows no percentage rather than inventing one.'\n })\n total?: number\n\n @Field(type => Int, { description: 'Index of the site being built (0-based).' })\n siteIndex: number\n\n @Field(type => Int, { description: 'How many sites are being built.' })\n siteCount: number\n\n @Field({ nullable: true, description: 'A fact within the step, such as how many rows were read.' })\n detail?: string\n\n @Field({ nullable: true, description: 'True when this step failed. The UI keeps the progress panel and marks this step.' })\n failed?: boolean\n\n @Field({ nullable: true, description: 'Why it failed. Present only on failure.' })\n error?: string\n}\n\n@Resolver()\nexport class TwinReferenceProgressSubscription {\n @Subscription(returns => TwinReferenceProgressMessage, {\n subscribe: ({ args, context }) => {\n const { source } = args\n const domainId = (context as any)?.state?.domain?.id\n /* 테넌트 격리 — 도메인이 없으면 빈 구독이다(다른 테넌트의 진행을 흘리지 않는다). */\n if (!domainId) return pipe(pubsub.subscribe('twin-reference-progress'), filter(() => false))\n return pipe(\n pubsub.subscribe('twin-reference-progress'),\n filter((p: { twinReferenceProgress: TwinReferenceProgressMessage }) => p.twinReferenceProgress.source === source)\n )\n },\n description: 'Progress while importing a reference — standard step names, so one translation per step covers every connector.'\n })\n twinReferenceProgress(\n @Root() payload: { twinReferenceProgress: TwinReferenceProgressMessage },\n @Arg('source') source: string\n ): TwinReferenceProgressMessage {\n return payload.twinReferenceProgress\n }\n}\n"]}
1
+ {"version":3,"file":"reference-progress-subscription.js","sourceRoot":"","sources":["../../../server/service/reference/reference-progress-subscription.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 유입 진행 구독 — **트윈을 만드는 동안 무엇을 하고 있는지 화면이 안다.**\n *\n * 마스터 읽기는 원본에 여러 질의를 보내는 긴 작업이고, 그 사이 화면은 아무것도 알 수 없었다. 단계를\n * 아는 것은 어댑터뿐이므로 어댑터가 **표준 낱말**로 말하고(`REFERENCE_STEPS`) 호스트가 자기 두 단계를\n * 더해 이 채널로 흘린다.\n *\n * 낱말이 표준이므로 화면은 **단계마다 번역 하나**만 두면 모든 커넥터를 보여 준다.\n */\nimport { Arg, Field, Int, ObjectType, Resolver, Root, Subscription } from 'type-graphql'\nimport { assertDomainSubscribeAllowed, getRepository, pubsub } from '@things-factory/shell'\nimport { filter, pipe } from 'graphql-yoga'\n\nimport { REFERENCE_STEPS } from './reference-progress.js'\nimport { REFERENCE_PROGRESS_TOPIC, referenceProgressFilter, type ReferenceProgressPayload } from './reference-progress-channel.js'\nimport { TwinReference } from './twin-reference.js'\n\n@ObjectType({ description: 'One step of progress while a reference is being imported into twins.' })\nexport class TwinReferenceProgressMessage {\n @Field({ description: 'Which reference (source) this belongs to.' })\n source: string\n\n @Field({ description: 'Which site is being built.' })\n siteId: string\n\n @Field({ description: `Standard step name — one of: ${REFERENCE_STEPS.join(' | ')}.` })\n step: string\n\n @Field(type => Int, { description: 'Steps finished for this site.' })\n done: number\n\n @Field(type => Int, {\n nullable: true,\n description: 'Total steps for this site. Absent when the adapter did not declare its steps — the UI then shows no percentage rather than inventing one.'\n })\n total?: number\n\n @Field(type => Int, { description: 'Index of the site being built (0-based).' })\n siteIndex: number\n\n @Field(type => Int, { description: 'How many sites are being built.' })\n siteCount: number\n\n @Field({ nullable: true, description: 'A fact within the step, such as how many rows were read.' })\n detail?: string\n\n @Field({ nullable: true, description: 'True when this step failed. The UI keeps the progress panel and marks this step.' })\n failed?: boolean\n\n @Field({ nullable: true, description: 'Why it failed. Present only on failure.' })\n error?: string\n}\n\n@Resolver()\nexport class TwinReferenceProgressSubscription {\n @Subscription(returns => TwinReferenceProgressMessage, {\n /*\n * Tenancy is asked twice (2026-09-24): the source must be one of the caller's references, and every\n * message must be the caller's domain's. It used to filter on `source` alone after checking only that\n * the caller had a domain — and source names are shared between tenants.\n */\n subscribe: async ({ args, context }) => {\n const { source } = args\n const { domain, user } = (context as any)?.state ?? {}\n await assertDomainSubscribeAllowed(domain, user, (d, u) => process.superUserGranted(d, u))\n const owned = await getRepository(TwinReference).findOne({ where: { domain: { id: domain.id }, source } })\n if (!owned) throw new Error(`reference not found in this tenant: ${source}`)\n return pipe(pubsub.subscribe(REFERENCE_PROGRESS_TOPIC), filter(referenceProgressFilter(domain.id, source)))\n },\n description: 'Progress while importing a reference — standard step names, so one translation per step covers every connector.'\n })\n twinReferenceProgress(\n @Root() payload: ReferenceProgressPayload<TwinReferenceProgressMessage>,\n @Arg('source') source: string\n ): TwinReferenceProgressMessage {\n return payload.twinReferenceProgress\n }\n}\n"]}
@@ -4,6 +4,7 @@ exports.TwinReferenceResolver = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const shell_1 = require("@things-factory/shell");
6
6
  const reference_progress_js_1 = require("./reference-progress.js");
7
+ const reference_progress_channel_js_1 = require("./reference-progress-channel.js");
7
8
  const reference_assessment_js_1 = require("./reference-assessment.js");
8
9
  const type_graphql_1 = require("type-graphql");
9
10
  const shell_2 = require("@things-factory/shell");
@@ -233,9 +234,7 @@ let TwinReferenceResolver = class TwinReferenceResolver {
233
234
  return;
234
235
  lastDone = p.done;
235
236
  lastStep = p.step;
236
- shell_1.pubsub.publish('twin-reference-progress', {
237
- twinReferenceProgress: { ...p, siteId, siteIndex: 0, siteCount: 1, total: p.total ?? stepTotal, source: progressSource }
238
- });
237
+ shell_1.pubsub.publish(reference_progress_channel_js_1.REFERENCE_PROGRESS_TOPIC, (0, reference_progress_channel_js_1.referenceProgressPayload)(domainId, { ...p, siteId, siteIndex: 0, siteCount: 1, total: p.total ?? stepTotal, source: progressSource }));
239
238
  };
240
239
  try {
241
240
  const master = await this.masterFromOrigin(domainId, origin, instanceId, st => emit({ step: st.key, done: st.done, total: stepTotal, detail: st.detail }));
@@ -695,9 +694,7 @@ let TwinReferenceResolver = class TwinReferenceResolver {
695
694
  */
696
695
  const declared = adapter.masterSteps?.length;
697
696
  const stepTotal = declared === undefined ? undefined : declared + reference_progress_js_1.HOST_STEPS.length;
698
- const emit = (p) => shell_1.pubsub.publish('twin-reference-progress', {
699
- twinReferenceProgress: { ...p, siteCount: sites.length, total: p.total ?? stepTotal, source }
700
- });
697
+ const emit = (p) => shell_1.pubsub.publish(reference_progress_channel_js_1.REFERENCE_PROGRESS_TOPIC, (0, reference_progress_channel_js_1.referenceProgressPayload)(domainId, { ...p, siteCount: sites.length, total: p.total ?? stepTotal, source }));
701
698
  /*
702
699
  * **읽은 뒤의 총평을 사이트마다 남긴다** (2026-08-22).
703
700
  *