import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm' /** * `twin_references.live_cursor` — **어디까지 읽었나**를 재기동 너머로 잇는다. * * ── 무엇이 오류 없이 사라지고 있었나 (2026-08-23 실측) ───────────────────────── * 라이브 어댑터가 붙을 때마다 `Date.now() − 되돌아볼 날수` 로 창을 새로 만들었다. 그래서 **재기동마다 * 미러의 과거가 잘렸다** — 작업 2,855 → 2,820(그 사이 8시간이 흐른 만큼). 미러가 아는 것이 「원본의 * 사실」이 아니라 「창의 함수」였고, 화면은 그 수를 보이면서 그 사실을 말하지 않았다. * * 같은 원본에서 잰 규모가 그 값을 말한다: 작업지시 77,594건 중 창이 읽는 것이 3,039건(3.9%)이다. * * ── 왜 표인가 (캐시가 아니라) ─────────────────────────────────────────────── * 커널이 갈림을 이미 적어 두었다(§`hydrateContinuity`): 「원천이 **애초에 다시 말해 주지 않는 축**」은 * 재기동 연속성으로 이어받는다. 「우리가 어디까지 읽었나」가 정확히 그 성질이다. * * 그리고 스냅샷 체크포인트와 성질이 다르다: 그쪽은 잃어도 원천이 정정해 준다(그래서 TTL 캐시로 족하다). * **이 값은 잃으면 원천에 묻지 않게 된다** — 창이 다시 미끄러지고, 그 손실은 오류 없이 들어온 것이 없다. * 만료되는 자리에 둘 수 없다. * * ── 모양 ──────────────────────────────────────────────────────────────────── * `{ streams: { [흐름]: { since?, seen: string[] } }, firstAttachedAt? }` * * **흐름 이름은 어댑터가 정한다** — 원본마다 흐름 수와 뜻이 다르다(첫 실 원본은 재고·오더·로트·투입 넷). * 이 층은 열쇠로만 다룬다. 계약이 이름을 닫으면 새 원본마다 계약을 고치게 된다. * * `firstAttachedAt` 은 「언제부터 아는가」다 — 커널은 상한만 안다(`nowTime`). 둘이 함께 「이 트윈이 * 아는 구간」이고, 화면이 수를 보일 때 그 구간을 말해야 한다. * * **nullable, 기본값 없음.** `null` = 한 번도 붙지 않았다는 사실이고, 그때만 되돌아볼 날수로 첫 창을 * 만든다. 빈 객체를 기본으로 깔면 「첫 붙음」과 「이어 붙음」이 같아지고, 그 구분이 이 컬럼의 전부다. * * `simple-json` = TEXT 이므로 드라이버 다섯을 그대로 지난다(DB별 JSON 타입·연산자 금지). * 멱등: 이미 있으면 아무것도 하지 않는다. TypeORM API 만 쓴다(raw SQL 금지). */ const REFS = 'twin_references' const CURSOR = 'live_cursor' export class CarryLiveFeedCursor1786200000000 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { const refs = await queryRunner.getTable(REFS) if (refs && !refs.columns.some(c => c.name === CURSOR)) { await queryRunner.addColumn(refs, new TableColumn({ name: CURSOR, type: 'text', isNullable: true })) } } public async down(queryRunner: QueryRunner): Promise { const refs = await queryRunner.getTable(REFS) if (refs && refs.columns.some(c => c.name === CURSOR)) { await queryRunner.dropColumn(refs, CURSOR) } } }