@things-factory/board-service 10.1.205 → 10.1.208
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/service/board/board-mutation.js +26 -16
- package/dist-server/service/board/board-mutation.js.map +1 -1
- package/dist-server/service/board/thumbnail-save.test.d.ts +6 -0
- package/dist-server/service/board/thumbnail-save.test.js +67 -0
- package/dist-server/service/board/thumbnail-save.test.js.map +1 -0
- package/dist-server/service/board/thumbnail-scheduler.d.ts +22 -1
- package/dist-server/service/board/thumbnail-scheduler.js +41 -15
- package/dist-server/service/board/thumbnail-scheduler.js.map +1 -1
- package/dist-server/service/board/thumbnail-scheduler.test.d.ts +1 -0
- package/dist-server/service/board/thumbnail-scheduler.test.js +106 -6
- package/dist-server/service/board/thumbnail-scheduler.test.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* 5) generateThumbnail 실패 시 hash unset → 다음 schedule 에서 재시도
|
|
12
12
|
* 6) saveThumbnail 실패도 동일 — 재시도 가능
|
|
13
13
|
* 7) timestamp 가 saveThumbnail 인자로 정확히 전달됨
|
|
14
|
+
* 8) 클라이언트 thumbnail — 대기 작업 버림 · 진행 중 결과는 since 로 거절 · hash 지움
|
|
14
15
|
*/
|
|
15
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
17
|
// scheduler 모듈 import 시 끌려오는 무거운 deps (entity, puppeteer, shell DB)
|
|
@@ -84,10 +85,11 @@ describe('scheduleThumbnail — debounce / dedup / hash', () => {
|
|
|
84
85
|
throw new Error('generate failed');
|
|
85
86
|
return nextGenerateResult;
|
|
86
87
|
},
|
|
87
|
-
saveThumbnail: async (boardId, thumbnail, timestamp) => {
|
|
88
|
+
saveThumbnail: async (boardId, thumbnail, timestamp, since) => {
|
|
88
89
|
if (saveShouldFail)
|
|
89
90
|
throw new Error('save failed');
|
|
90
|
-
saveCalls.push({ boardId, thumbnail, timestamp });
|
|
91
|
+
saveCalls.push({ boardId, thumbnail, timestamp, since });
|
|
92
|
+
return true;
|
|
91
93
|
}
|
|
92
94
|
};
|
|
93
95
|
(0, thumbnail_scheduler_1._setSchedulerDeps)(mockDeps);
|
|
@@ -171,8 +173,9 @@ describe('scheduleThumbnail — debounce / dedup / hash', () => {
|
|
|
171
173
|
}
|
|
172
174
|
return 'base64data';
|
|
173
175
|
},
|
|
174
|
-
saveThumbnail: async (boardId, thumbnail, timestamp) => {
|
|
175
|
-
saveCalls.push({ boardId, thumbnail, timestamp });
|
|
176
|
+
saveThumbnail: async (boardId, thumbnail, timestamp, since) => {
|
|
177
|
+
saveCalls.push({ boardId, thumbnail, timestamp, since });
|
|
178
|
+
return true;
|
|
176
179
|
}
|
|
177
180
|
});
|
|
178
181
|
// 5 연속 schedule (각각 다른 model)
|
|
@@ -262,8 +265,9 @@ describe('scheduleThumbnail — debounce / dedup / hash', () => {
|
|
|
262
265
|
return firstPromise;
|
|
263
266
|
return 'base64data';
|
|
264
267
|
},
|
|
265
|
-
saveThumbnail: async (boardId, thumbnail, timestamp) => {
|
|
266
|
-
saveCalls.push({ boardId, thumbnail, timestamp });
|
|
268
|
+
saveThumbnail: async (boardId, thumbnail, timestamp, since) => {
|
|
269
|
+
saveCalls.push({ boardId, thumbnail, timestamp, since });
|
|
270
|
+
return true;
|
|
267
271
|
}
|
|
268
272
|
});
|
|
269
273
|
(0, thumbnail_scheduler_1.scheduleThumbnail)('board-1', '{"v":1}', {});
|
|
@@ -290,4 +294,100 @@ describe('scheduleThumbnail — debounce / dedup / hash', () => {
|
|
|
290
294
|
expect(after.cachedHashes).toBeGreaterThanOrEqual(1);
|
|
291
295
|
});
|
|
292
296
|
});
|
|
297
|
+
// ─── 클라이언트 thumbnail ──────────────────────────────────────────────────
|
|
298
|
+
describe('isClientThumbnail', () => {
|
|
299
|
+
test('png data URL 은 클라이언트 그림', () => {
|
|
300
|
+
expect((0, thumbnail_scheduler_1.isClientThumbnail)('data:image/png;base64,AAAA')).toBe(true);
|
|
301
|
+
});
|
|
302
|
+
test('없거나 빈 gif 는 그림이 없다는 뜻', () => {
|
|
303
|
+
expect((0, thumbnail_scheduler_1.isClientThumbnail)(undefined)).toBe(false);
|
|
304
|
+
expect((0, thumbnail_scheduler_1.isClientThumbnail)(null)).toBe(false);
|
|
305
|
+
expect((0, thumbnail_scheduler_1.isClientThumbnail)('')).toBe(false);
|
|
306
|
+
expect((0, thumbnail_scheduler_1.isClientThumbnail)('data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==')).toBe(false);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
describe('acceptClientThumbnail', () => {
|
|
310
|
+
let generateCalls;
|
|
311
|
+
let saveCalls;
|
|
312
|
+
/* DB 처럼 board 마다 thumbnailUpdatedAt 을 갖고, since 보다 늦게 저장된 것이 있으면 거절한다 */
|
|
313
|
+
let thumbnailUpdatedAt;
|
|
314
|
+
let releaseGenerate;
|
|
315
|
+
beforeEach(() => {
|
|
316
|
+
generateCalls = [];
|
|
317
|
+
saveCalls = [];
|
|
318
|
+
thumbnailUpdatedAt = new Map();
|
|
319
|
+
releaseGenerate = undefined;
|
|
320
|
+
(0, thumbnail_scheduler_1._setSchedulerDeps)({
|
|
321
|
+
generateThumbnail: async (model) => {
|
|
322
|
+
generateCalls.push(model);
|
|
323
|
+
if (model.includes('slow'))
|
|
324
|
+
return new Promise(resolve => (releaseGenerate = resolve));
|
|
325
|
+
return 'server';
|
|
326
|
+
},
|
|
327
|
+
saveThumbnail: async (boardId, thumbnail, timestamp, since) => {
|
|
328
|
+
const saved = thumbnailUpdatedAt.get(boardId);
|
|
329
|
+
if (saved && saved >= since)
|
|
330
|
+
return false;
|
|
331
|
+
thumbnailUpdatedAt.set(boardId, timestamp);
|
|
332
|
+
saveCalls.push({ boardId, thumbnail, since });
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
(0, thumbnail_scheduler_1._resetSchedulerState)();
|
|
337
|
+
});
|
|
338
|
+
afterEach(() => {
|
|
339
|
+
(0, thumbnail_scheduler_1._resetSchedulerDeps)();
|
|
340
|
+
(0, thumbnail_scheduler_1._resetSchedulerState)();
|
|
341
|
+
});
|
|
342
|
+
/* updateBoard 가 하는 일: acceptClientThumbnail 을 부르고 thumbnail 과 thumbnailUpdatedAt 을 저장한다 */
|
|
343
|
+
function saveClientThumbnail(boardId) {
|
|
344
|
+
(0, thumbnail_scheduler_1.acceptClientThumbnail)(boardId);
|
|
345
|
+
thumbnailUpdatedAt.set(boardId, new Date());
|
|
346
|
+
}
|
|
347
|
+
test('대기 중인 생성은 버린다', async () => {
|
|
348
|
+
(0, thumbnail_scheduler_1.scheduleThumbnail)('board-1', '{"v":"slow"}', {});
|
|
349
|
+
await new Promise(r => setImmediate(r));
|
|
350
|
+
(0, thumbnail_scheduler_1.scheduleThumbnail)('board-1', '{"v":2}', {}); // 진행 중이라 대기
|
|
351
|
+
saveClientThumbnail('board-1');
|
|
352
|
+
releaseGenerate('server-slow');
|
|
353
|
+
await (0, thumbnail_scheduler_1._waitForIdle)();
|
|
354
|
+
expect(generateCalls).toEqual(['{"v":"slow"}']);
|
|
355
|
+
expect((0, thumbnail_scheduler_1.getSchedulerStats)().pending).toBe(0);
|
|
356
|
+
});
|
|
357
|
+
test('진행 중인 생성은 끝나도 저장하지 않는다', async () => {
|
|
358
|
+
(0, thumbnail_scheduler_1.scheduleThumbnail)('board-1', '{"v":"slow"}', {});
|
|
359
|
+
await new Promise(r => setImmediate(r));
|
|
360
|
+
await new Promise(r => setTimeout(r, 2)); // 생성 시작과 클라이언트 저장이 같은 ms 가 되지 않게
|
|
361
|
+
saveClientThumbnail('board-1');
|
|
362
|
+
await new Promise(r => setTimeout(r, 2)); // 생성이 끝나는 때는 클라이언트 저장보다 늦다
|
|
363
|
+
releaseGenerate('server-slow');
|
|
364
|
+
await (0, thumbnail_scheduler_1._waitForIdle)();
|
|
365
|
+
expect(saveCalls).toHaveLength(0);
|
|
366
|
+
expect((0, thumbnail_scheduler_1.getSchedulerStats)().cachedHashes).toBe(0);
|
|
367
|
+
});
|
|
368
|
+
test('since 는 생성을 시작한 때다', async () => {
|
|
369
|
+
const before = new Date();
|
|
370
|
+
(0, thumbnail_scheduler_1.scheduleThumbnail)('board-1', '{"v":1}', {});
|
|
371
|
+
await (0, thumbnail_scheduler_1._waitForIdle)();
|
|
372
|
+
expect(saveCalls).toHaveLength(1);
|
|
373
|
+
expect(saveCalls[0].since.getTime()).toBeGreaterThanOrEqual(before.getTime());
|
|
374
|
+
});
|
|
375
|
+
test('hash 를 지운다 — 같은 model 을 thumbnail 없이 다시 저장하면 다시 그린다', async () => {
|
|
376
|
+
(0, thumbnail_scheduler_1.scheduleThumbnail)('board-1', '{"v":1}', {});
|
|
377
|
+
await (0, thumbnail_scheduler_1._waitForIdle)();
|
|
378
|
+
expect(generateCalls).toHaveLength(1);
|
|
379
|
+
saveClientThumbnail('board-1');
|
|
380
|
+
await new Promise(r => setTimeout(r, 2));
|
|
381
|
+
(0, thumbnail_scheduler_1.scheduleThumbnail)('board-1', '{"v":1}', {});
|
|
382
|
+
await (0, thumbnail_scheduler_1._waitForIdle)();
|
|
383
|
+
expect(generateCalls).toHaveLength(2);
|
|
384
|
+
expect(saveCalls).toHaveLength(2);
|
|
385
|
+
});
|
|
386
|
+
test('다른 board 의 생성은 그대로', async () => {
|
|
387
|
+
(0, thumbnail_scheduler_1.scheduleThumbnail)('board-2', '{"v":1}', {});
|
|
388
|
+
saveClientThumbnail('board-1');
|
|
389
|
+
await (0, thumbnail_scheduler_1._waitForIdle)();
|
|
390
|
+
expect(saveCalls.map(c => c.boardId)).toEqual(['board-2']);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
293
393
|
//# sourceMappingURL=thumbnail-scheduler.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"thumbnail-scheduler.test.js","sourceRoot":"","sources":["../../../server/service/board/thumbnail-scheduler.test.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Thumbnail Scheduler — debounce / dedup / hash 동작 단위 테스트.\n *\n * 실제 puppeteer / DB 의존성은 dep injection (_setSchedulerDeps) 로 mock.\n * 검증 대상은 *스케줄링 정책 그 자체*:\n * 1) fnv1a hash 결정성 / 충돌 적음\n * 2) 동일 model 반복 schedule → generateThumbnail 1 회만\n * 3) per-board debounce — 빈번 schedule 시 중간 model 은 skip, 최신만 처리\n * 4) 다른 board 는 서로 독립\n * 5) generateThumbnail 실패 시 hash unset → 다음 schedule 에서 재시도\n * 6) saveThumbnail 실패도 동일 — 재시도 가능\n * 7) timestamp 가 saveThumbnail 인자로 정확히 전달됨\n */\n\n// scheduler 모듈 import 시 끌려오는 무거운 deps (entity, puppeteer, shell DB)\n// 는 _setSchedulerDeps 로 어차피 교체하니, import 시점 부담만 줄이고자 mock.\njest.mock('../../controllers/thumbnail', () => ({\n thumbnail: jest.fn().mockResolvedValue(undefined)\n}))\njest.mock('./board', () => ({ Board: class MockBoard {} }))\njest.mock('@things-factory/shell', () => ({\n getDataSource: () => ({ transaction: async (cb: any) => cb({}) }),\n getRepository: () => ({ save: jest.fn().mockResolvedValue({}) })\n}))\n\nimport {\n fnv1a,\n scheduleThumbnail,\n _setSchedulerDeps,\n _resetSchedulerDeps,\n _resetSchedulerState,\n _waitForIdle,\n getSchedulerStats,\n type SchedulerDeps\n} from './thumbnail-scheduler'\n\n// ─── fnv1a hash ─────────────────────────────────────────────────────────────\n\ndescribe('fnv1a hash', () => {\n test('같은 입력은 항상 같은 hash', () => {\n expect(fnv1a('hello')).toBe(fnv1a('hello'))\n expect(fnv1a('{\"x\":1}')).toBe(fnv1a('{\"x\":1}'))\n expect(fnv1a('')).toBe(fnv1a(''))\n })\n\n test('다른 입력은 다른 hash (현실적인 model 변경)', () => {\n expect(fnv1a('{\"width\":100}')).not.toBe(fnv1a('{\"width\":200}'))\n expect(fnv1a('{\"a\":1}')).not.toBe(fnv1a('{\"a\":2}'))\n expect(fnv1a('A')).not.toBe(fnv1a('B'))\n })\n\n test('빈 문자열은 결정적 hash', () => {\n const h = fnv1a('')\n expect(typeof h).toBe('string')\n expect(h.length).toBeGreaterThan(0)\n })\n\n test('아주 작은 변경도 다른 hash', () => {\n expect(fnv1a('{\"x\":1.0}')).not.toBe(fnv1a('{\"x\":1.1}'))\n expect(fnv1a('abc')).not.toBe(fnv1a('abd'))\n })\n\n test('긴 입력에서도 빠르게 결정적', () => {\n const big = JSON.stringify({ components: Array(10000).fill({ type: 'rect', x: 1, y: 2 }) })\n const h1 = fnv1a(big)\n const h2 = fnv1a(big)\n expect(h1).toBe(h2)\n })\n\n test('hex 형식 string 반환', () => {\n const h = fnv1a('test')\n expect(h).toMatch(/^[0-9a-f]+$/)\n })\n\n test('100 개 다른 입력 중 hash 충돌 없음', () => {\n const hashes = new Set<string>()\n for (let i = 0; i < 100; i++) {\n hashes.add(fnv1a(`{\"id\":${i},\"data\":\"value-${i}\"}`))\n }\n expect(hashes.size).toBe(100)\n })\n})\n\n// ─── scheduleThumbnail 동작 ─────────────────────────────────────────────────\n\ndescribe('scheduleThumbnail — debounce / dedup / hash', () => {\n let generateCalls: Array<{ model: string; context: any }>\n let saveCalls: Array<{ boardId: string; thumbnail: string; timestamp: Date }>\n let nextGenerateResult: string | undefined\n let generateShouldFail: boolean\n let saveShouldFail: boolean\n\n beforeEach(() => {\n generateCalls = []\n saveCalls = []\n nextGenerateResult = 'base64data'\n generateShouldFail = false\n saveShouldFail = false\n\n const mockDeps: SchedulerDeps = {\n generateThumbnail: async (model, context) => {\n generateCalls.push({ model, context })\n if (generateShouldFail) throw new Error('generate failed')\n return nextGenerateResult\n },\n saveThumbnail: async (boardId, thumbnail, timestamp) => {\n if (saveShouldFail) throw new Error('save failed')\n saveCalls.push({ boardId, thumbnail, timestamp })\n }\n }\n\n _setSchedulerDeps(mockDeps)\n _resetSchedulerState()\n })\n\n afterEach(() => {\n _resetSchedulerDeps()\n _resetSchedulerState()\n })\n\n test('첫 schedule → generateThumbnail 1 회, saveThumbnail 1 회', async () => {\n scheduleThumbnail('board-1', '{\"w\":100}', {})\n await _waitForIdle()\n\n expect(generateCalls).toHaveLength(1)\n expect(saveCalls).toHaveLength(1)\n expect(saveCalls[0].boardId).toBe('board-1')\n expect(saveCalls[0].thumbnail).toBe('data:image/png;base64,base64data')\n })\n\n test('saveThumbnail 인자에 dataURL 형식 적용', async () => {\n nextGenerateResult = 'AAAA'\n scheduleThumbnail('board-1', '{\"w\":100}', {})\n await _waitForIdle()\n\n expect(saveCalls[0].thumbnail).toBe('data:image/png;base64,AAAA')\n })\n\n test('generateThumbnail 가 null 반환 → empty thumbnail dataURL', async () => {\n nextGenerateResult = 'null'\n scheduleThumbnail('board-1', '{\"w\":100}', {})\n await _waitForIdle()\n\n expect(saveCalls[0].thumbnail).toMatch(/^data:image\\/gif;base64,/)\n })\n\n test('generateThumbnail 가 undefined 반환 → empty thumbnail', async () => {\n nextGenerateResult = undefined\n scheduleThumbnail('board-1', '{\"w\":100}', {})\n await _waitForIdle()\n\n expect(saveCalls[0].thumbnail).toMatch(/^data:image\\/gif;base64,/)\n })\n\n test('saveThumbnail timestamp 가 Date 인스턴스', async () => {\n const before = new Date()\n scheduleThumbnail('board-1', '{\"w\":100}', {})\n await _waitForIdle()\n const after = new Date()\n\n expect(saveCalls[0].timestamp).toBeInstanceOf(Date)\n expect(saveCalls[0].timestamp.getTime()).toBeGreaterThanOrEqual(before.getTime())\n expect(saveCalls[0].timestamp.getTime()).toBeLessThanOrEqual(after.getTime())\n })\n\n test('동일 model 두 번 schedule → 두 번째는 hash dedup 으로 skip', async () => {\n const sameModel = '{\"w\":100,\"h\":200}'\n\n scheduleThumbnail('board-1', sameModel, {})\n await _waitForIdle()\n expect(generateCalls).toHaveLength(1)\n\n scheduleThumbnail('board-1', sameModel, {})\n await _waitForIdle()\n expect(generateCalls).toHaveLength(1) // hash dedup\n })\n\n test('model 변경 후 schedule → generateThumbnail 다시 호출', async () => {\n scheduleThumbnail('board-1', '{\"w\":100}', {})\n await _waitForIdle()\n expect(generateCalls).toHaveLength(1)\n\n scheduleThumbnail('board-1', '{\"w\":200}', {}) // 다른 model\n await _waitForIdle()\n expect(generateCalls).toHaveLength(2)\n })\n\n test('빈 boardId 또는 빈 model 은 no-op', async () => {\n scheduleThumbnail('', '{\"w\":100}', {})\n scheduleThumbnail('board-1', '', {})\n scheduleThumbnail('', '', {})\n await _waitForIdle()\n\n expect(generateCalls).toHaveLength(0)\n expect(saveCalls).toHaveLength(0)\n })\n\n test('빈번한 schedule (5 회) → debounce 로 generateThumbnail 최대 2 회 (진행 중 1 + 최신 1)', async () => {\n // 첫 schedule 은 즉시 진행. 그 사이 나머지 4 개는 pending 큐에 누적되어\n // 최신 model 1 개만 살아남고 그것이 두 번째 호출이 됨. 총 generate = 2.\n let resolveFirst: (value: string) => void\n let firstStarted = false\n\n _setSchedulerDeps({\n generateThumbnail: async (model, context) => {\n generateCalls.push({ model, context })\n if (!firstStarted) {\n firstStarted = true\n return new Promise<string>(resolve => { resolveFirst = resolve })\n }\n return 'base64data'\n },\n saveThumbnail: async (boardId, thumbnail, timestamp) => {\n saveCalls.push({ boardId, thumbnail, timestamp })\n }\n })\n\n // 5 연속 schedule (각각 다른 model)\n scheduleThumbnail('board-1', '{\"v\":1}', {})\n await new Promise(r => setImmediate(r)) // 첫 호출 시작 보장\n scheduleThumbnail('board-1', '{\"v\":2}', {})\n scheduleThumbnail('board-1', '{\"v\":3}', {})\n scheduleThumbnail('board-1', '{\"v\":4}', {})\n scheduleThumbnail('board-1', '{\"v\":5}', {})\n\n // 첫 호출 release\n resolveFirst!('base64data')\n await _waitForIdle()\n\n // 첫 호출 + 큐의 최신 (v=5) = 2 회\n expect(generateCalls).toHaveLength(2)\n expect(generateCalls[0].model).toBe('{\"v\":1}')\n expect(generateCalls[1].model).toBe('{\"v\":5}')\n })\n\n test('다른 board 는 서로 독립 처리', async () => {\n scheduleThumbnail('board-1', '{\"v\":1}', {})\n scheduleThumbnail('board-2', '{\"v\":1}', {})\n scheduleThumbnail('board-3', '{\"v\":1}', {})\n await _waitForIdle()\n\n expect(generateCalls).toHaveLength(3)\n expect(saveCalls.map(s => s.boardId).sort()).toEqual(['board-1', 'board-2', 'board-3'])\n })\n\n test('generateThumbnail 실패 시 hash unset → 다음 schedule 에서 재시도', async () => {\n generateShouldFail = true\n scheduleThumbnail('board-1', '{\"w\":100}', {})\n await _waitForIdle()\n expect(generateCalls).toHaveLength(1)\n expect(saveCalls).toHaveLength(0)\n\n // 같은 model 로 다시 — 실패했으니 hash 없어서 재시도\n generateShouldFail = false\n scheduleThumbnail('board-1', '{\"w\":100}', {})\n await _waitForIdle()\n expect(generateCalls).toHaveLength(2)\n expect(saveCalls).toHaveLength(1)\n })\n\n test('saveThumbnail 실패 시 hash unset → 다음 schedule 에서 재시도', async () => {\n saveShouldFail = true\n scheduleThumbnail('board-1', '{\"w\":100}', {})\n await _waitForIdle()\n expect(generateCalls).toHaveLength(1)\n expect(saveCalls).toHaveLength(0)\n\n saveShouldFail = false\n scheduleThumbnail('board-1', '{\"w\":100}', {})\n await _waitForIdle()\n expect(generateCalls).toHaveLength(2)\n expect(saveCalls).toHaveLength(1)\n })\n\n test('성공 → 같은 model 재시도 안 함 (hash set 됨)', async () => {\n scheduleThumbnail('board-1', '{\"w\":100}', {})\n await _waitForIdle()\n\n for (let i = 0; i < 5; i++) {\n scheduleThumbnail('board-1', '{\"w\":100}', {})\n }\n await _waitForIdle()\n\n expect(generateCalls).toHaveLength(1) // 첫 성공 후 모두 dedup\n })\n\n test('성공 → 다른 model → 같은 첫 model 다시: 다시 호출됨 (hash 는 마지막 성공 model 만 추적)', async () => {\n scheduleThumbnail('board-1', '{\"v\":1}', {})\n await _waitForIdle()\n expect(generateCalls).toHaveLength(1)\n\n scheduleThumbnail('board-1', '{\"v\":2}', {})\n await _waitForIdle()\n expect(generateCalls).toHaveLength(2)\n\n // v=1 으로 돌아가도 마지막 hash 는 v=2 이므로 다시 호출.\n scheduleThumbnail('board-1', '{\"v\":1}', {})\n await _waitForIdle()\n expect(generateCalls).toHaveLength(3)\n })\n\n test('context 객체가 generateThumbnail 에 전달됨', async () => {\n const ctx = { state: { domain: { subdomain: 'test' }, user: { id: 'u1' } } }\n scheduleThumbnail('board-1', '{\"w\":100}', ctx)\n await _waitForIdle()\n\n expect(generateCalls[0].context).toBe(ctx)\n })\n\n test('진행 중에 새 schedule → 큐에 보존 → 끝나면 자동으로 처리', async () => {\n let resolveFirst: (value: string) => void\n const firstPromise = new Promise<string>(resolve => { resolveFirst = resolve })\n let callCount = 0\n\n _setSchedulerDeps({\n generateThumbnail: async (model, context) => {\n callCount++\n generateCalls.push({ model, context })\n if (callCount === 1) return firstPromise\n return 'base64data'\n },\n saveThumbnail: async (boardId, thumbnail, timestamp) => {\n saveCalls.push({ boardId, thumbnail, timestamp })\n }\n })\n\n scheduleThumbnail('board-1', '{\"v\":1}', {})\n await new Promise(r => setImmediate(r))\n expect(generateCalls).toHaveLength(1)\n\n // 진행 중에 새 schedule\n scheduleThumbnail('board-1', '{\"v\":2}', {})\n expect(generateCalls).toHaveLength(1) // 아직 1 (대기 중)\n\n // 첫 호출 release\n resolveFirst!('first')\n await _waitForIdle()\n\n // 첫 호출 끝나고 큐의 v=2 처리됨\n expect(generateCalls).toHaveLength(2)\n expect(generateCalls[1].model).toBe('{\"v\":2}')\n })\n\n test('getSchedulerStats 는 합리적 값 반환', async () => {\n const stats = getSchedulerStats()\n expect(typeof stats.pending).toBe('number')\n expect(typeof stats.inFlight).toBe('number')\n expect(typeof stats.cachedHashes).toBe('number')\n\n scheduleThumbnail('board-1', '{\"v\":1}', {})\n await _waitForIdle()\n const after = getSchedulerStats()\n expect(after.cachedHashes).toBeGreaterThanOrEqual(1)\n })\n})\n"]}
|
|
1
|
+
{"version":3,"file":"thumbnail-scheduler.test.js","sourceRoot":"","sources":["../../../server/service/board/thumbnail-scheduler.test.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Thumbnail Scheduler — debounce / dedup / hash 동작 단위 테스트.\n *\n * 실제 puppeteer / DB 의존성은 dep injection (_setSchedulerDeps) 로 mock.\n * 검증 대상은 *스케줄링 정책 그 자체*:\n * 1) fnv1a hash 결정성 / 충돌 적음\n * 2) 동일 model 반복 schedule → generateThumbnail 1 회만\n * 3) per-board debounce — 빈번 schedule 시 중간 model 은 skip, 최신만 처리\n * 4) 다른 board 는 서로 독립\n * 5) generateThumbnail 실패 시 hash unset → 다음 schedule 에서 재시도\n * 6) saveThumbnail 실패도 동일 — 재시도 가능\n * 7) timestamp 가 saveThumbnail 인자로 정확히 전달됨\n * 8) 클라이언트 thumbnail — 대기 작업 버림 · 진행 중 결과는 since 로 거절 · hash 지움\n */\n\n// scheduler 모듈 import 시 끌려오는 무거운 deps (entity, puppeteer, shell DB)\n// 는 _setSchedulerDeps 로 어차피 교체하니, import 시점 부담만 줄이고자 mock.\njest.mock('../../controllers/thumbnail', () => ({\n thumbnail: jest.fn().mockResolvedValue(undefined)\n}))\njest.mock('./board', () => ({ Board: class MockBoard {} }))\njest.mock('@things-factory/shell', () => ({\n getDataSource: () => ({ transaction: async (cb: any) => cb({}) }),\n getRepository: () => ({ save: jest.fn().mockResolvedValue({}) })\n}))\n\nimport {\n acceptClientThumbnail,\n fnv1a,\n isClientThumbnail,\n scheduleThumbnail,\n _setSchedulerDeps,\n _resetSchedulerDeps,\n _resetSchedulerState,\n _waitForIdle,\n getSchedulerStats,\n type SchedulerDeps\n} from './thumbnail-scheduler'\n\n// ─── fnv1a hash ─────────────────────────────────────────────────────────────\n\ndescribe('fnv1a hash', () => {\n test('같은 입력은 항상 같은 hash', () => {\n expect(fnv1a('hello')).toBe(fnv1a('hello'))\n expect(fnv1a('{\"x\":1}')).toBe(fnv1a('{\"x\":1}'))\n expect(fnv1a('')).toBe(fnv1a(''))\n })\n\n test('다른 입력은 다른 hash (현실적인 model 변경)', () => {\n expect(fnv1a('{\"width\":100}')).not.toBe(fnv1a('{\"width\":200}'))\n expect(fnv1a('{\"a\":1}')).not.toBe(fnv1a('{\"a\":2}'))\n expect(fnv1a('A')).not.toBe(fnv1a('B'))\n })\n\n test('빈 문자열은 결정적 hash', () => {\n const h = fnv1a('')\n expect(typeof h).toBe('string')\n expect(h.length).toBeGreaterThan(0)\n })\n\n test('아주 작은 변경도 다른 hash', () => {\n expect(fnv1a('{\"x\":1.0}')).not.toBe(fnv1a('{\"x\":1.1}'))\n expect(fnv1a('abc')).not.toBe(fnv1a('abd'))\n })\n\n test('긴 입력에서도 빠르게 결정적', () => {\n const big = JSON.stringify({ components: Array(10000).fill({ type: 'rect', x: 1, y: 2 }) })\n const h1 = fnv1a(big)\n const h2 = fnv1a(big)\n expect(h1).toBe(h2)\n })\n\n test('hex 형식 string 반환', () => {\n const h = fnv1a('test')\n expect(h).toMatch(/^[0-9a-f]+$/)\n })\n\n test('100 개 다른 입력 중 hash 충돌 없음', () => {\n const hashes = new Set<string>()\n for (let i = 0; i < 100; i++) {\n hashes.add(fnv1a(`{\"id\":${i},\"data\":\"value-${i}\"}`))\n }\n expect(hashes.size).toBe(100)\n })\n})\n\n// ─── scheduleThumbnail 동작 ─────────────────────────────────────────────────\n\ndescribe('scheduleThumbnail — debounce / dedup / hash', () => {\n let generateCalls: Array<{ model: string; context: any }>\n let saveCalls: Array<{ boardId: string; thumbnail: string; timestamp: Date; since: Date }>\n let nextGenerateResult: string | undefined\n let generateShouldFail: boolean\n let saveShouldFail: boolean\n\n beforeEach(() => {\n generateCalls = []\n saveCalls = []\n nextGenerateResult = 'base64data'\n generateShouldFail = false\n saveShouldFail = false\n\n const mockDeps: SchedulerDeps = {\n generateThumbnail: async (model, context) => {\n generateCalls.push({ model, context })\n if (generateShouldFail) throw new Error('generate failed')\n return nextGenerateResult\n },\n saveThumbnail: async (boardId, thumbnail, timestamp, since) => {\n if (saveShouldFail) throw new Error('save failed')\n saveCalls.push({ boardId, thumbnail, timestamp, since })\n return true\n }\n }\n\n _setSchedulerDeps(mockDeps)\n _resetSchedulerState()\n })\n\n afterEach(() => {\n _resetSchedulerDeps()\n _resetSchedulerState()\n })\n\n test('첫 schedule → generateThumbnail 1 회, saveThumbnail 1 회', async () => {\n scheduleThumbnail('board-1', '{\"w\":100}', {})\n await _waitForIdle()\n\n expect(generateCalls).toHaveLength(1)\n expect(saveCalls).toHaveLength(1)\n expect(saveCalls[0].boardId).toBe('board-1')\n expect(saveCalls[0].thumbnail).toBe('data:image/png;base64,base64data')\n })\n\n test('saveThumbnail 인자에 dataURL 형식 적용', async () => {\n nextGenerateResult = 'AAAA'\n scheduleThumbnail('board-1', '{\"w\":100}', {})\n await _waitForIdle()\n\n expect(saveCalls[0].thumbnail).toBe('data:image/png;base64,AAAA')\n })\n\n test('generateThumbnail 가 null 반환 → empty thumbnail dataURL', async () => {\n nextGenerateResult = 'null'\n scheduleThumbnail('board-1', '{\"w\":100}', {})\n await _waitForIdle()\n\n expect(saveCalls[0].thumbnail).toMatch(/^data:image\\/gif;base64,/)\n })\n\n test('generateThumbnail 가 undefined 반환 → empty thumbnail', async () => {\n nextGenerateResult = undefined\n scheduleThumbnail('board-1', '{\"w\":100}', {})\n await _waitForIdle()\n\n expect(saveCalls[0].thumbnail).toMatch(/^data:image\\/gif;base64,/)\n })\n\n test('saveThumbnail timestamp 가 Date 인스턴스', async () => {\n const before = new Date()\n scheduleThumbnail('board-1', '{\"w\":100}', {})\n await _waitForIdle()\n const after = new Date()\n\n expect(saveCalls[0].timestamp).toBeInstanceOf(Date)\n expect(saveCalls[0].timestamp.getTime()).toBeGreaterThanOrEqual(before.getTime())\n expect(saveCalls[0].timestamp.getTime()).toBeLessThanOrEqual(after.getTime())\n })\n\n test('동일 model 두 번 schedule → 두 번째는 hash dedup 으로 skip', async () => {\n const sameModel = '{\"w\":100,\"h\":200}'\n\n scheduleThumbnail('board-1', sameModel, {})\n await _waitForIdle()\n expect(generateCalls).toHaveLength(1)\n\n scheduleThumbnail('board-1', sameModel, {})\n await _waitForIdle()\n expect(generateCalls).toHaveLength(1) // hash dedup\n })\n\n test('model 변경 후 schedule → generateThumbnail 다시 호출', async () => {\n scheduleThumbnail('board-1', '{\"w\":100}', {})\n await _waitForIdle()\n expect(generateCalls).toHaveLength(1)\n\n scheduleThumbnail('board-1', '{\"w\":200}', {}) // 다른 model\n await _waitForIdle()\n expect(generateCalls).toHaveLength(2)\n })\n\n test('빈 boardId 또는 빈 model 은 no-op', async () => {\n scheduleThumbnail('', '{\"w\":100}', {})\n scheduleThumbnail('board-1', '', {})\n scheduleThumbnail('', '', {})\n await _waitForIdle()\n\n expect(generateCalls).toHaveLength(0)\n expect(saveCalls).toHaveLength(0)\n })\n\n test('빈번한 schedule (5 회) → debounce 로 generateThumbnail 최대 2 회 (진행 중 1 + 최신 1)', async () => {\n // 첫 schedule 은 즉시 진행. 그 사이 나머지 4 개는 pending 큐에 누적되어\n // 최신 model 1 개만 살아남고 그것이 두 번째 호출이 됨. 총 generate = 2.\n let resolveFirst: (value: string) => void\n let firstStarted = false\n\n _setSchedulerDeps({\n generateThumbnail: async (model, context) => {\n generateCalls.push({ model, context })\n if (!firstStarted) {\n firstStarted = true\n return new Promise<string>(resolve => { resolveFirst = resolve })\n }\n return 'base64data'\n },\n saveThumbnail: async (boardId, thumbnail, timestamp, since) => {\n saveCalls.push({ boardId, thumbnail, timestamp, since })\n return true\n }\n })\n\n // 5 연속 schedule (각각 다른 model)\n scheduleThumbnail('board-1', '{\"v\":1}', {})\n await new Promise(r => setImmediate(r)) // 첫 호출 시작 보장\n scheduleThumbnail('board-1', '{\"v\":2}', {})\n scheduleThumbnail('board-1', '{\"v\":3}', {})\n scheduleThumbnail('board-1', '{\"v\":4}', {})\n scheduleThumbnail('board-1', '{\"v\":5}', {})\n\n // 첫 호출 release\n resolveFirst!('base64data')\n await _waitForIdle()\n\n // 첫 호출 + 큐의 최신 (v=5) = 2 회\n expect(generateCalls).toHaveLength(2)\n expect(generateCalls[0].model).toBe('{\"v\":1}')\n expect(generateCalls[1].model).toBe('{\"v\":5}')\n })\n\n test('다른 board 는 서로 독립 처리', async () => {\n scheduleThumbnail('board-1', '{\"v\":1}', {})\n scheduleThumbnail('board-2', '{\"v\":1}', {})\n scheduleThumbnail('board-3', '{\"v\":1}', {})\n await _waitForIdle()\n\n expect(generateCalls).toHaveLength(3)\n expect(saveCalls.map(s => s.boardId).sort()).toEqual(['board-1', 'board-2', 'board-3'])\n })\n\n test('generateThumbnail 실패 시 hash unset → 다음 schedule 에서 재시도', async () => {\n generateShouldFail = true\n scheduleThumbnail('board-1', '{\"w\":100}', {})\n await _waitForIdle()\n expect(generateCalls).toHaveLength(1)\n expect(saveCalls).toHaveLength(0)\n\n // 같은 model 로 다시 — 실패했으니 hash 없어서 재시도\n generateShouldFail = false\n scheduleThumbnail('board-1', '{\"w\":100}', {})\n await _waitForIdle()\n expect(generateCalls).toHaveLength(2)\n expect(saveCalls).toHaveLength(1)\n })\n\n test('saveThumbnail 실패 시 hash unset → 다음 schedule 에서 재시도', async () => {\n saveShouldFail = true\n scheduleThumbnail('board-1', '{\"w\":100}', {})\n await _waitForIdle()\n expect(generateCalls).toHaveLength(1)\n expect(saveCalls).toHaveLength(0)\n\n saveShouldFail = false\n scheduleThumbnail('board-1', '{\"w\":100}', {})\n await _waitForIdle()\n expect(generateCalls).toHaveLength(2)\n expect(saveCalls).toHaveLength(1)\n })\n\n test('성공 → 같은 model 재시도 안 함 (hash set 됨)', async () => {\n scheduleThumbnail('board-1', '{\"w\":100}', {})\n await _waitForIdle()\n\n for (let i = 0; i < 5; i++) {\n scheduleThumbnail('board-1', '{\"w\":100}', {})\n }\n await _waitForIdle()\n\n expect(generateCalls).toHaveLength(1) // 첫 성공 후 모두 dedup\n })\n\n test('성공 → 다른 model → 같은 첫 model 다시: 다시 호출됨 (hash 는 마지막 성공 model 만 추적)', async () => {\n scheduleThumbnail('board-1', '{\"v\":1}', {})\n await _waitForIdle()\n expect(generateCalls).toHaveLength(1)\n\n scheduleThumbnail('board-1', '{\"v\":2}', {})\n await _waitForIdle()\n expect(generateCalls).toHaveLength(2)\n\n // v=1 으로 돌아가도 마지막 hash 는 v=2 이므로 다시 호출.\n scheduleThumbnail('board-1', '{\"v\":1}', {})\n await _waitForIdle()\n expect(generateCalls).toHaveLength(3)\n })\n\n test('context 객체가 generateThumbnail 에 전달됨', async () => {\n const ctx = { state: { domain: { subdomain: 'test' }, user: { id: 'u1' } } }\n scheduleThumbnail('board-1', '{\"w\":100}', ctx)\n await _waitForIdle()\n\n expect(generateCalls[0].context).toBe(ctx)\n })\n\n test('진행 중에 새 schedule → 큐에 보존 → 끝나면 자동으로 처리', async () => {\n let resolveFirst: (value: string) => void\n const firstPromise = new Promise<string>(resolve => { resolveFirst = resolve })\n let callCount = 0\n\n _setSchedulerDeps({\n generateThumbnail: async (model, context) => {\n callCount++\n generateCalls.push({ model, context })\n if (callCount === 1) return firstPromise\n return 'base64data'\n },\n saveThumbnail: async (boardId, thumbnail, timestamp, since) => {\n saveCalls.push({ boardId, thumbnail, timestamp, since })\n return true\n }\n })\n\n scheduleThumbnail('board-1', '{\"v\":1}', {})\n await new Promise(r => setImmediate(r))\n expect(generateCalls).toHaveLength(1)\n\n // 진행 중에 새 schedule\n scheduleThumbnail('board-1', '{\"v\":2}', {})\n expect(generateCalls).toHaveLength(1) // 아직 1 (대기 중)\n\n // 첫 호출 release\n resolveFirst!('first')\n await _waitForIdle()\n\n // 첫 호출 끝나고 큐의 v=2 처리됨\n expect(generateCalls).toHaveLength(2)\n expect(generateCalls[1].model).toBe('{\"v\":2}')\n })\n\n test('getSchedulerStats 는 합리적 값 반환', async () => {\n const stats = getSchedulerStats()\n expect(typeof stats.pending).toBe('number')\n expect(typeof stats.inFlight).toBe('number')\n expect(typeof stats.cachedHashes).toBe('number')\n\n scheduleThumbnail('board-1', '{\"v\":1}', {})\n await _waitForIdle()\n const after = getSchedulerStats()\n expect(after.cachedHashes).toBeGreaterThanOrEqual(1)\n })\n})\n\n// ─── 클라이언트 thumbnail ──────────────────────────────────────────────────\n\ndescribe('isClientThumbnail', () => {\n test('png data URL 은 클라이언트 그림', () => {\n expect(isClientThumbnail('data:image/png;base64,AAAA')).toBe(true)\n })\n\n test('없거나 빈 gif 는 그림이 없다는 뜻', () => {\n expect(isClientThumbnail(undefined)).toBe(false)\n expect(isClientThumbnail(null)).toBe(false)\n expect(isClientThumbnail('')).toBe(false)\n expect(isClientThumbnail('data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==')).toBe(false)\n })\n})\n\ndescribe('acceptClientThumbnail', () => {\n let generateCalls: string[]\n let saveCalls: Array<{ boardId: string; thumbnail: string; since: Date }>\n\n /* DB 처럼 board 마다 thumbnailUpdatedAt 을 갖고, since 보다 늦게 저장된 것이 있으면 거절한다 */\n let thumbnailUpdatedAt: Map<string, Date>\n let releaseGenerate: ((value: string) => void) | undefined\n\n beforeEach(() => {\n generateCalls = []\n saveCalls = []\n thumbnailUpdatedAt = new Map()\n releaseGenerate = undefined\n\n _setSchedulerDeps({\n generateThumbnail: async model => {\n generateCalls.push(model)\n if (model.includes('slow')) return new Promise<string>(resolve => (releaseGenerate = resolve))\n return 'server'\n },\n saveThumbnail: async (boardId, thumbnail, timestamp, since) => {\n const saved = thumbnailUpdatedAt.get(boardId)\n if (saved && saved >= since) return false\n thumbnailUpdatedAt.set(boardId, timestamp)\n saveCalls.push({ boardId, thumbnail, since })\n return true\n }\n })\n _resetSchedulerState()\n })\n\n afterEach(() => {\n _resetSchedulerDeps()\n _resetSchedulerState()\n })\n\n /* updateBoard 가 하는 일: acceptClientThumbnail 을 부르고 thumbnail 과 thumbnailUpdatedAt 을 저장한다 */\n function saveClientThumbnail(boardId: string) {\n acceptClientThumbnail(boardId)\n thumbnailUpdatedAt.set(boardId, new Date())\n }\n\n test('대기 중인 생성은 버린다', async () => {\n scheduleThumbnail('board-1', '{\"v\":\"slow\"}', {})\n await new Promise(r => setImmediate(r))\n scheduleThumbnail('board-1', '{\"v\":2}', {}) // 진행 중이라 대기\n\n saveClientThumbnail('board-1')\n releaseGenerate!('server-slow')\n await _waitForIdle()\n\n expect(generateCalls).toEqual(['{\"v\":\"slow\"}'])\n expect(getSchedulerStats().pending).toBe(0)\n })\n\n test('진행 중인 생성은 끝나도 저장하지 않는다', async () => {\n scheduleThumbnail('board-1', '{\"v\":\"slow\"}', {})\n await new Promise(r => setImmediate(r))\n await new Promise(r => setTimeout(r, 2)) // 생성 시작과 클라이언트 저장이 같은 ms 가 되지 않게\n\n saveClientThumbnail('board-1')\n await new Promise(r => setTimeout(r, 2)) // 생성이 끝나는 때는 클라이언트 저장보다 늦다\n releaseGenerate!('server-slow')\n await _waitForIdle()\n\n expect(saveCalls).toHaveLength(0)\n expect(getSchedulerStats().cachedHashes).toBe(0)\n })\n\n test('since 는 생성을 시작한 때다', async () => {\n const before = new Date()\n scheduleThumbnail('board-1', '{\"v\":1}', {})\n await _waitForIdle()\n\n expect(saveCalls).toHaveLength(1)\n expect(saveCalls[0].since.getTime()).toBeGreaterThanOrEqual(before.getTime())\n })\n\n test('hash 를 지운다 — 같은 model 을 thumbnail 없이 다시 저장하면 다시 그린다', async () => {\n scheduleThumbnail('board-1', '{\"v\":1}', {})\n await _waitForIdle()\n expect(generateCalls).toHaveLength(1)\n\n saveClientThumbnail('board-1')\n await new Promise(r => setTimeout(r, 2))\n\n scheduleThumbnail('board-1', '{\"v\":1}', {})\n await _waitForIdle()\n\n expect(generateCalls).toHaveLength(2)\n expect(saveCalls).toHaveLength(2)\n })\n\n test('다른 board 의 생성은 그대로', async () => {\n scheduleThumbnail('board-2', '{\"v\":1}', {})\n saveClientThumbnail('board-1')\n await _waitForIdle()\n\n expect(saveCalls.map(c => c.boardId)).toEqual(['board-2'])\n })\n})\n"]}
|