@things-factory/headless-twin 10.1.227 → 10.1.228

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.
@@ -71,7 +71,7 @@ async function backfillSubjects(domainId, instanceId, opts = {}) {
71
71
  (0, log_js_1.twinWarn)(`[twin-subject] "${instanceId}" 선언된 품목이 없어 품목 줄은 만들지 않는다 — 개체·자리·설비 줄만 옮긴다.`);
72
72
  }
73
73
  /*
74
- * ── 진행 지점을 **판 안에서만** 든다 (2026-08-27 · 사용자 판단) ────────────────
74
+ * ── 진행 지점을 **회차 안에서만** 든다 (2026-08-27 · 사용자 판단) ────────────────
75
75
  *
76
76
  * 처음에는 테이블에 적었다. 사용자 판단으로 없앴다 — 사건이 들어올 때 파생 테이블도 같이 적히므로,
77
77
  * 옛 사건을 옮기는 일은 새 파생 테이블을 만들 때와 계산 규칙을 고쳐 다시 만들 때뿐이다. 그 두 경우가
@@ -1 +1 @@
1
- {"version":3,"file":"twin-subject-backfill.js","sourceRoot":"","sources":["../../../server/service/twin-subject/twin-subject-backfill.ts"],"names":[],"mappings":"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","sourcesContent":["import { getRepository } from '@things-factory/shell'\n\nimport { twinLog, twinWarn } from '../../engine/log.js'\nimport { TwinEvent } from '../twin-event/twin-event.js'\nimport { TwinInstance } from '../twin-instance/twin-instance.js'\nimport { TwinSubjectEvent } from './twin-subject-event.js'\nimport { writeSubjectRows } from './twin-subject-writer.js'\nimport { itemResolverFor } from './item-resolver.js'\n\n/*\n * **이미 저장된 사건을 이 표로 옮긴다** — 요청받은 트윈만.\n *\n * ── 왜 기본이 아닌가 ────────────────────────────────────────────────────────\n * 이 표의 규칙은 「앞으로만 채운다」다. 그래야 새 조회 축이 필요할 때마다 3,128만 줄을 다시 만드는\n * 구조가 되지 않는다(설계: read-models.md §4).\n *\n * 그런데 이미 쌓인 트윈에서 이력을 보려면 그 구간이 있어야 한다. 그래서 **부르면 채우는 도구**로 둔다 —\n * 마이그레이션이 아니고, 부팅에 자동으로 돌지도 않는다. 트윈을 지정해 부른다.\n *\n * ── 어디까지 옮겼는지 적어 둔다 (2026-08-27 · 세 번 틀린 뒤) ─────────────────\n * 저장하지 않고 표의 내용으로 진행 지점을 알아내려 했고 세 번 틀렸다.\n *\n * ① 표의 최대 리비전 뒤부터 앞으로 채워진 줄의 최댓값이 곧 저널의 끝이라 소급을 다 건너뛴다\n * ② 표의 최소 리비전 앞까지 소급이 1부터 적으면 최솟값이 1이 되어 구간이 0으로 접힌다\n * ③ 표의 최소 시각 아래로 사건이 시각순으로 오지 않는 트윈에서 곧바로 끝난다\n *\n * ③이 결정적이었다. 승화푸드 커넥터는 과거 기록을 되읽으므로 방금 들어온 사건의 시각이 2024년 10월이다.\n * 그래서 표의 가장 오래된 시각이 이미 저널의 시작과 같아진다.\n *\n * 원인은 하나다. **채워진 것은 구간이 아니라 집합이다.** 리비전으로 보든 시각으로 보든 「어디까지」를\n * 하나의 경계로 말할 수 없다. 그래서 알아내려 하지 않고 **이 프로세스가 든다**(§`inFlight`).\n *\n * 리비전 1부터 순서대로 올라간다. 목표는 **채우기를 시작한 순간의 저널 끝**이다 — 그보다 뒤는 앞으로\n * 채우는 쪽이 이미 적고 있다.\n *\n * ── 규율 ────────────────────────────────────────────────────────────────────\n * **나눠서 옮긴다.** 한 번에 통째로 읽으면 그동안 호스트가 아무 일도 못 한다(sqlite 는 쓰기가 하나다).\n * 배치 사이에 루프를 비워 준다.\n *\n * **같은 시각의 줄을 반쪽만 적지 않는다.** 한 배치의 가장 오래된 시각에 걸친 줄들은 다음 배치로 넘긴다 —\n * 반쪽만 적으면 그 시각이 「채워진 것」으로 보이면서 일부가 빠진다.\n *\n * **몇 줄을 옮겼고 어디까지 왔는지 말한다.** 알리지 않고 절반만 채우면 화면이 「그 앞에는 사건이 없다」고\n * 말하게 된다.\n */\n\n/** 한 번에 읽는 저널 행 수 — 저널 정리와 같은 규율. */\nconst READ_BATCH = 5_000\n\nexport interface BackfillResult {\n instanceId: string\n /** 읽은 저널 행 수. */\n read: number\n /** 이 표에 적은 줄 수. */\n written: number\n /** 여기까지 옮겼다(저널 리비전). */\n upToRevision: number\n /** 목표 — 채우기를 시작한 순간의 저널 끝. */\n targetRevision: number\n /** 다 옮겼나. */\n complete: boolean\n /** 예산을 다 써서 멈췄나. */\n paused: boolean\n}\n\n/**\n * 옮기는 중인 트윈의 목표와 지점 — **이 프로세스 안에서만** 든다.\n *\n * 테이블에 적지 않는다(사용자 판단, 2026-08-27). 서버가 내려가면 사라지고, 다음에 부르면 처음부터\n * 다시 읽는다. 그 대가를 아는 채로 두는 것이다.\n */\nconst inFlight = new Map<string, { upToRevision: number; targetRevision: number }>()\n\n/**\n * 한 트윈의 저장된 사건을 이 표로 옮긴다 — 예산 안에서.\n *\n * 되풀어 부르면 이어서 한다. 넘길 인자가 없다 — 진행 지점을 적어 두기 때문이다.\n */\nexport async function backfillSubjects(\n domainId: string,\n instanceId: string,\n opts: { budgetMs?: number } = {}\n): Promise<BackfillResult> {\n const budgetMs = opts.budgetMs ?? 30_000\n const startedMs = Date.now()\n\n const reg = await getRepository(TwinInstance)\n .findOne({ where: { domain: { id: domainId }, instanceId } })\n .catch(() => null)\n if (!reg?.model) {\n throw new Error(`twin \"${instanceId}\" is not registered — no model to resolve declared items with`)\n }\n const itemOf = itemResolverFor(domainId, instanceId, reg.model)\n if (!itemOf) {\n twinWarn(\n `[twin-subject] \"${instanceId}\" 선언된 품목이 없어 품목 줄은 만들지 않는다 — 개체·자리·설비 줄만 옮긴다.`\n )\n }\n\n /*\n * ── 진행 지점을 **판 안에서만** 든다 (2026-08-27 · 사용자 판단) ────────────────\n *\n * 처음에는 테이블에 적었다. 사용자 판단으로 없앴다 — 사건이 들어올 때 파생 테이블도 같이 적히므로,\n * 옛 사건을 옮기는 일은 새 파생 테이블을 만들 때와 계산 규칙을 고쳐 다시 만들 때뿐이다. 그 두 경우가\n * 흔하지 않다.\n *\n * 대가는 이것이다. 옮기는 중에 서버가 내려가면 그 몫을 다시 읽는다. 335만 건이면 20분쯤이다.\n *\n * 그래서 **목표와 지점을 이 프로세스가 든다.** 트윈마다 한 줄이고, 서버가 내려가면 사라진다.\n */\n const events = getRepository(TwinEvent)\n const key = `${domainId}:${instanceId}`\n let held = inFlight.get(key)\n if (!held) {\n /*\n * 목표는 **처음 부를 때의 저널 끝**이다. 매번 다시 재면 그 사이에 새 사건이 들어와 끝나지 않는다.\n * 그 뒤의 사건은 저널을 쓸 때 파생 테이블에도 같이 적히므로 이 작업의 몫이 아니다.\n */\n const tip = await events\n .createQueryBuilder('e')\n .select('MAX(e.revision)', 'max')\n .where('e.domain = :domainId', { domainId })\n .andWhere('e.instanceId = :instanceId', { instanceId })\n .getRawOne<{ max: unknown }>()\n held = { upToRevision: 0, targetRevision: Number(tip?.max ?? 0) }\n inFlight.set(key, held)\n }\n\n let read = 0\n let written = 0\n let paused = false\n let cursor = held.upToRevision\n const target = held.targetRevision\n\n for (;;) {\n if (cursor >= target) break\n if (Date.now() - startedMs >= budgetMs) {\n paused = true\n break\n }\n const rows = await events\n .createQueryBuilder('e')\n .where('e.domain = :domainId', { domainId })\n .andWhere('e.instanceId = :instanceId', { instanceId })\n .andWhere('e.revision > :cursor', { cursor })\n .andWhere('e.revision <= :target', { target })\n .orderBy('e.revision', 'ASC')\n .take(READ_BATCH)\n .getMany()\n if (!rows.length) {\n cursor = target\n break\n }\n\n read += rows.length\n written += await writeRows(domainId, instanceId, rows, itemOf)\n cursor = Number(rows[rows.length - 1].revision)\n /* 진행 지점을 그때그때 든다 — 중간에 멈춰도 다음 호출이 여기서 잇는다(같은 프로세스 안에서). */\n held.upToRevision = cursor\n /* 배치 사이에 루프를 비워 준다 — 이 한 줄이 없으면 나눈 뜻이 없다. */\n await new Promise(resolve => setImmediate(resolve))\n }\n\n held.upToRevision = cursor\n const complete = cursor >= target\n /* 다 옮겼으면 들고 있을 이유가 없다 — 다시 부르면 목표를 새로 잡는다(그때는 옮길 것이 없다). */\n if (complete) inFlight.delete(key)\n twinLog(\n `[twin-subject] backfill \"${instanceId}\" — 저널 ${read}행을 읽어 ${written}줄 적었다 · ` +\n `리비전 ${cursor}/${target}` +\n `${complete ? ' · 다 옮겼다' : paused ? ' · 예산을 다 써서 멈췄다 — 다시 부르면 이어서 한다' : ''}`\n )\n\n return { instanceId, read, written, upToRevision: cursor, targetRevision: target, complete, paused }\n}\n\n/** 저널 행들을 봉투로 되돌려 적는다 — `payload` 가 봉투 전체다. */\nasync function writeRows(domainId: string, instanceId: string, rows: TwinEvent[], itemOf?: any): Promise<number> {\n return await writeSubjectRows(\n domainId,\n instanceId,\n rows.map(r => ({\n /*\n * `payload` 가 봉투 전체다(`{ eventId, eventType, eventTime, tenantId, data: {…} }`). 한 겹 더\n * 감싸면 키가 묻혀 아무것도 찾지 못한다 — 실측으로 825,000행을 읽어 0줄을 적었다.\n *\n * 시각과 종류는 승격 컬럼을 믿는다. payload 안의 값과 컬럼이 어긋난 옛 행이 있을 수 있고,\n * 이 표의 정렬 축은 컬럼이 정본이다.\n */\n envelope: { ...((r as any).payload ?? {}), eventType: r.eventType, eventTime: r.eventTime },\n revision: Number(r.revision),\n eventId: r.id\n })),\n itemOf\n )\n}\n"]}
1
+ {"version":3,"file":"twin-subject-backfill.js","sourceRoot":"","sources":["../../../server/service/twin-subject/twin-subject-backfill.ts"],"names":[],"mappings":"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","sourcesContent":["import { getRepository } from '@things-factory/shell'\n\nimport { twinLog, twinWarn } from '../../engine/log.js'\nimport { TwinEvent } from '../twin-event/twin-event.js'\nimport { TwinInstance } from '../twin-instance/twin-instance.js'\nimport { TwinSubjectEvent } from './twin-subject-event.js'\nimport { writeSubjectRows } from './twin-subject-writer.js'\nimport { itemResolverFor } from './item-resolver.js'\n\n/*\n * **이미 저장된 사건을 이 표로 옮긴다** — 요청받은 트윈만.\n *\n * ── 왜 기본이 아닌가 ────────────────────────────────────────────────────────\n * 이 표의 규칙은 「앞으로만 채운다」다. 그래야 새 조회 축이 필요할 때마다 3,128만 줄을 다시 만드는\n * 구조가 되지 않는다(설계: read-models.md §4).\n *\n * 그런데 이미 쌓인 트윈에서 이력을 보려면 그 구간이 있어야 한다. 그래서 **부르면 채우는 도구**로 둔다 —\n * 마이그레이션이 아니고, 부팅에 자동으로 돌지도 않는다. 트윈을 지정해 부른다.\n *\n * ── 어디까지 옮겼는지 적어 둔다 (2026-08-27 · 세 번 틀린 뒤) ─────────────────\n * 저장하지 않고 표의 내용으로 진행 지점을 알아내려 했고 세 번 틀렸다.\n *\n * ① 표의 최대 리비전 뒤부터 앞으로 채워진 줄의 최댓값이 곧 저널의 끝이라 소급을 다 건너뛴다\n * ② 표의 최소 리비전 앞까지 소급이 1부터 적으면 최솟값이 1이 되어 구간이 0으로 접힌다\n * ③ 표의 최소 시각 아래로 사건이 시각순으로 오지 않는 트윈에서 곧바로 끝난다\n *\n * ③이 결정적이었다. 승화푸드 커넥터는 과거 기록을 되읽으므로 방금 들어온 사건의 시각이 2024년 10월이다.\n * 그래서 표의 가장 오래된 시각이 이미 저널의 시작과 같아진다.\n *\n * 원인은 하나다. **채워진 것은 구간이 아니라 집합이다.** 리비전으로 보든 시각으로 보든 「어디까지」를\n * 하나의 경계로 말할 수 없다. 그래서 알아내려 하지 않고 **이 프로세스가 든다**(§`inFlight`).\n *\n * 리비전 1부터 순서대로 올라간다. 목표는 **채우기를 시작한 순간의 저널 끝**이다 — 그보다 뒤는 앞으로\n * 채우는 쪽이 이미 적고 있다.\n *\n * ── 규율 ────────────────────────────────────────────────────────────────────\n * **나눠서 옮긴다.** 한 번에 통째로 읽으면 그동안 호스트가 아무 일도 못 한다(sqlite 는 쓰기가 하나다).\n * 배치 사이에 루프를 비워 준다.\n *\n * **같은 시각의 줄을 반쪽만 적지 않는다.** 한 배치의 가장 오래된 시각에 걸친 줄들은 다음 배치로 넘긴다 —\n * 반쪽만 적으면 그 시각이 「채워진 것」으로 보이면서 일부가 빠진다.\n *\n * **몇 줄을 옮겼고 어디까지 왔는지 말한다.** 알리지 않고 절반만 채우면 화면이 「그 앞에는 사건이 없다」고\n * 말하게 된다.\n */\n\n/** 한 번에 읽는 저널 행 수 — 저널 정리와 같은 규율. */\nconst READ_BATCH = 5_000\n\nexport interface BackfillResult {\n instanceId: string\n /** 읽은 저널 행 수. */\n read: number\n /** 이 표에 적은 줄 수. */\n written: number\n /** 여기까지 옮겼다(저널 리비전). */\n upToRevision: number\n /** 목표 — 채우기를 시작한 순간의 저널 끝. */\n targetRevision: number\n /** 다 옮겼나. */\n complete: boolean\n /** 예산을 다 써서 멈췄나. */\n paused: boolean\n}\n\n/**\n * 옮기는 중인 트윈의 목표와 지점 — **이 프로세스 안에서만** 든다.\n *\n * 테이블에 적지 않는다(사용자 판단, 2026-08-27). 서버가 내려가면 사라지고, 다음에 부르면 처음부터\n * 다시 읽는다. 그 대가를 아는 채로 두는 것이다.\n */\nconst inFlight = new Map<string, { upToRevision: number; targetRevision: number }>()\n\n/**\n * 한 트윈의 저장된 사건을 이 표로 옮긴다 — 예산 안에서.\n *\n * 되풀어 부르면 이어서 한다. 넘길 인자가 없다 — 진행 지점을 적어 두기 때문이다.\n */\nexport async function backfillSubjects(\n domainId: string,\n instanceId: string,\n opts: { budgetMs?: number } = {}\n): Promise<BackfillResult> {\n const budgetMs = opts.budgetMs ?? 30_000\n const startedMs = Date.now()\n\n const reg = await getRepository(TwinInstance)\n .findOne({ where: { domain: { id: domainId }, instanceId } })\n .catch(() => null)\n if (!reg?.model) {\n throw new Error(`twin \"${instanceId}\" is not registered — no model to resolve declared items with`)\n }\n const itemOf = itemResolverFor(domainId, instanceId, reg.model)\n if (!itemOf) {\n twinWarn(\n `[twin-subject] \"${instanceId}\" 선언된 품목이 없어 품목 줄은 만들지 않는다 — 개체·자리·설비 줄만 옮긴다.`\n )\n }\n\n /*\n * ── 진행 지점을 **회차 안에서만** 든다 (2026-08-27 · 사용자 판단) ────────────────\n *\n * 처음에는 테이블에 적었다. 사용자 판단으로 없앴다 — 사건이 들어올 때 파생 테이블도 같이 적히므로,\n * 옛 사건을 옮기는 일은 새 파생 테이블을 만들 때와 계산 규칙을 고쳐 다시 만들 때뿐이다. 그 두 경우가\n * 흔하지 않다.\n *\n * 대가는 이것이다. 옮기는 중에 서버가 내려가면 그 몫을 다시 읽는다. 335만 건이면 20분쯤이다.\n *\n * 그래서 **목표와 지점을 이 프로세스가 든다.** 트윈마다 한 줄이고, 서버가 내려가면 사라진다.\n */\n const events = getRepository(TwinEvent)\n const key = `${domainId}:${instanceId}`\n let held = inFlight.get(key)\n if (!held) {\n /*\n * 목표는 **처음 부를 때의 저널 끝**이다. 매번 다시 재면 그 사이에 새 사건이 들어와 끝나지 않는다.\n * 그 뒤의 사건은 저널을 쓸 때 파생 테이블에도 같이 적히므로 이 작업의 몫이 아니다.\n */\n const tip = await events\n .createQueryBuilder('e')\n .select('MAX(e.revision)', 'max')\n .where('e.domain = :domainId', { domainId })\n .andWhere('e.instanceId = :instanceId', { instanceId })\n .getRawOne<{ max: unknown }>()\n held = { upToRevision: 0, targetRevision: Number(tip?.max ?? 0) }\n inFlight.set(key, held)\n }\n\n let read = 0\n let written = 0\n let paused = false\n let cursor = held.upToRevision\n const target = held.targetRevision\n\n for (;;) {\n if (cursor >= target) break\n if (Date.now() - startedMs >= budgetMs) {\n paused = true\n break\n }\n const rows = await events\n .createQueryBuilder('e')\n .where('e.domain = :domainId', { domainId })\n .andWhere('e.instanceId = :instanceId', { instanceId })\n .andWhere('e.revision > :cursor', { cursor })\n .andWhere('e.revision <= :target', { target })\n .orderBy('e.revision', 'ASC')\n .take(READ_BATCH)\n .getMany()\n if (!rows.length) {\n cursor = target\n break\n }\n\n read += rows.length\n written += await writeRows(domainId, instanceId, rows, itemOf)\n cursor = Number(rows[rows.length - 1].revision)\n /* 진행 지점을 그때그때 든다 — 중간에 멈춰도 다음 호출이 여기서 잇는다(같은 프로세스 안에서). */\n held.upToRevision = cursor\n /* 배치 사이에 루프를 비워 준다 — 이 한 줄이 없으면 나눈 뜻이 없다. */\n await new Promise(resolve => setImmediate(resolve))\n }\n\n held.upToRevision = cursor\n const complete = cursor >= target\n /* 다 옮겼으면 들고 있을 이유가 없다 — 다시 부르면 목표를 새로 잡는다(그때는 옮길 것이 없다). */\n if (complete) inFlight.delete(key)\n twinLog(\n `[twin-subject] backfill \"${instanceId}\" — 저널 ${read}행을 읽어 ${written}줄 적었다 · ` +\n `리비전 ${cursor}/${target}` +\n `${complete ? ' · 다 옮겼다' : paused ? ' · 예산을 다 써서 멈췄다 — 다시 부르면 이어서 한다' : ''}`\n )\n\n return { instanceId, read, written, upToRevision: cursor, targetRevision: target, complete, paused }\n}\n\n/** 저널 행들을 봉투로 되돌려 적는다 — `payload` 가 봉투 전체다. */\nasync function writeRows(domainId: string, instanceId: string, rows: TwinEvent[], itemOf?: any): Promise<number> {\n return await writeSubjectRows(\n domainId,\n instanceId,\n rows.map(r => ({\n /*\n * `payload` 가 봉투 전체다(`{ eventId, eventType, eventTime, tenantId, data: {…} }`). 한 겹 더\n * 감싸면 키가 묻혀 아무것도 찾지 못한다 — 실측으로 825,000행을 읽어 0줄을 적었다.\n *\n * 시각과 종류는 승격 컬럼을 믿는다. payload 안의 값과 컬럼이 어긋난 옛 행이 있을 수 있고,\n * 이 표의 정렬 축은 컬럼이 정본이다.\n */\n envelope: { ...((r as any).payload ?? {}), eventType: r.eventType, eventTime: r.eventTime },\n revision: Number(r.revision),\n eventId: r.id\n })),\n itemOf\n )\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/headless-twin",
3
- "version": "10.1.227",
3
+ "version": "10.1.228",
4
4
  "main": "dist-server/index.js",
5
5
  "exports": {
6
6
  ".": "./dist-server/index.js",
@@ -41,11 +41,11 @@
41
41
  "dependencies": {
42
42
  "@operato/ops-contract": "^0.9.40",
43
43
  "@operato/twin-kernel": "^0.11.29",
44
- "@things-factory/auth-base": "^10.1.227",
45
- "@things-factory/cache-service": "^10.1.227",
44
+ "@things-factory/auth-base": "^10.1.228",
45
+ "@things-factory/cache-service": "^10.1.228",
46
46
  "@things-factory/env": "^10.1.149",
47
- "@things-factory/ingest": "^10.1.227",
48
- "@things-factory/shell": "^10.1.227"
47
+ "@things-factory/ingest": "^10.1.228",
48
+ "@things-factory/shell": "^10.1.228"
49
49
  },
50
- "gitHead": "f66591e3864aab08a6fb93fb1fcf1c9de6b546ac"
50
+ "gitHead": "fc4ede44617418df8701e83b83d177db4142a8f3"
51
51
  }