@operato/board 10.0.0-beta.4 → 10.0.0-beta.40

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 (47) hide show
  1. package/CHANGELOG.md +316 -0
  2. package/dist/src/component/container.js +1 -3
  3. package/dist/src/component/container.js.map +1 -1
  4. package/dist/src/component/etc.js +2 -10
  5. package/dist/src/component/etc.js.map +1 -1
  6. package/dist/src/component/line.js +4 -28
  7. package/dist/src/component/line.js.map +1 -1
  8. package/dist/src/component/shape.js +5 -29
  9. package/dist/src/component/shape.js.map +1 -1
  10. package/dist/src/component/text-and-media.js +2 -25
  11. package/dist/src/component/text-and-media.js.map +1 -1
  12. package/dist/src/data-storage/board-model-cache.d.ts +30 -0
  13. package/dist/src/data-storage/board-model-cache.js +93 -0
  14. package/dist/src/data-storage/board-model-cache.js.map +1 -0
  15. package/dist/src/graphql/playback-buffer.d.ts +79 -0
  16. package/dist/src/graphql/playback-buffer.js +139 -0
  17. package/dist/src/graphql/playback-buffer.js.map +1 -0
  18. package/dist/src/graphql/playback-buffer.test.d.ts +1 -0
  19. package/dist/src/graphql/playback-buffer.test.js +261 -0
  20. package/dist/src/graphql/playback-buffer.test.js.map +1 -0
  21. package/dist/src/graphql/playback-subscription.d.ts +89 -0
  22. package/dist/src/graphql/playback-subscription.js +258 -0
  23. package/dist/src/graphql/playback-subscription.js.map +1 -0
  24. package/dist/src/index.d.ts +2 -0
  25. package/dist/src/index.js +1 -0
  26. package/dist/src/index.js.map +1 -1
  27. package/dist/src/modeller/edit-toolbar-style.js +38 -1
  28. package/dist/src/modeller/edit-toolbar-style.js.map +1 -1
  29. package/dist/src/modeller/edit-toolbar.d.ts +8 -16
  30. package/dist/src/modeller/edit-toolbar.js +204 -199
  31. package/dist/src/modeller/edit-toolbar.js.map +1 -1
  32. package/dist/src/modeller/scene-viewer/ox-scene-viewer.d.ts +2 -1
  33. package/dist/src/modeller/scene-viewer/ox-scene-viewer.js +7 -11
  34. package/dist/src/modeller/scene-viewer/ox-scene-viewer.js.map +1 -1
  35. package/dist/src/ox-board-modeller.d.ts +8 -1
  36. package/dist/src/ox-board-modeller.js +125 -6
  37. package/dist/src/ox-board-modeller.js.map +1 -1
  38. package/dist/src/ox-board-viewer.d.ts +50 -1
  39. package/dist/src/ox-board-viewer.js +271 -28
  40. package/dist/src/ox-board-viewer.js.map +1 -1
  41. package/dist/src/ox-playback-controls.d.ts +56 -0
  42. package/dist/src/ox-playback-controls.js +515 -0
  43. package/dist/src/ox-playback-controls.js.map +1 -0
  44. package/dist/src/selector/ox-board-selector.js +11 -1
  45. package/dist/src/selector/ox-board-selector.js.map +1 -1
  46. package/dist/tsconfig.tsbuildinfo +1 -1
  47. package/package.json +13 -12
@@ -0,0 +1,261 @@
1
+ import { PlaybackBuffer, CHUNK_DURATION } from './playback-buffer';
2
+ // --- 테스트 헬퍼 ---
3
+ const BASE_TIME = new Date('2026-04-12T10:00:00Z').getTime();
4
+ const ONE_HOUR = 3600000;
5
+ const ONE_MIN = 60000;
6
+ function makeSnapshots(fromMs, toMs, intervalMs = 1000) {
7
+ const snaps = [];
8
+ for (let t = fromMs; t < toMs; t += intervalMs) {
9
+ snaps.push({
10
+ timestamp: t,
11
+ data: { vehicle: { id: 'AGV01', position: t - fromMs } }
12
+ });
13
+ }
14
+ return snaps;
15
+ }
16
+ function createMockFetcher(allSnapshots) {
17
+ const calls = [];
18
+ const fetcher = async (from, to) => {
19
+ calls.push({ from: from.getTime(), to: to.getTime() });
20
+ if (allSnapshots) {
21
+ return allSnapshots.filter(s => s.timestamp >= from.getTime() && s.timestamp < to.getTime());
22
+ }
23
+ // 기본: 1초 간격 스냅샷 생성
24
+ return makeSnapshots(from.getTime(), to.getTime());
25
+ };
26
+ return { fetcher, calls };
27
+ }
28
+ // --- PlaybackBuffer 단위 테스트 ---
29
+ describe('PlaybackBuffer', () => {
30
+ const totalRange = {
31
+ from: new Date(BASE_TIME),
32
+ to: new Date(BASE_TIME + ONE_HOUR)
33
+ };
34
+ describe('loadInitial', () => {
35
+ test('10분 청크를 로드한다', async () => {
36
+ const { fetcher, calls } = createMockFetcher();
37
+ const buffer = new PlaybackBuffer(fetcher, totalRange);
38
+ await buffer.loadInitial(BASE_TIME);
39
+ expect(calls).toHaveLength(1);
40
+ expect(calls[0].from).toBe(BASE_TIME);
41
+ expect(calls[0].to).toBe(BASE_TIME + CHUNK_DURATION);
42
+ });
43
+ test('기존 버퍼를 폐기하고 새로 로드한다', async () => {
44
+ const { fetcher, calls } = createMockFetcher();
45
+ const buffer = new PlaybackBuffer(fetcher, totalRange);
46
+ await buffer.loadInitial(BASE_TIME);
47
+ await buffer.loadInitial(BASE_TIME + 30 * ONE_MIN);
48
+ expect(calls).toHaveLength(2);
49
+ expect(buffer.getBufferedRanges()).toHaveLength(1);
50
+ expect(buffer.getBufferedRanges()[0].from).toBe(BASE_TIME + 30 * ONE_MIN);
51
+ });
52
+ });
53
+ describe('getSnapshotAt', () => {
54
+ test('playHead 이하인 가장 가까운 스냅샷을 반환한다', async () => {
55
+ const snaps = makeSnapshots(BASE_TIME, BASE_TIME + CHUNK_DURATION, 5000);
56
+ const { fetcher } = createMockFetcher(snaps);
57
+ const buffer = new PlaybackBuffer(fetcher, totalRange);
58
+ await buffer.loadInitial(BASE_TIME);
59
+ const snap = buffer.getSnapshotAt(BASE_TIME + 7000);
60
+ expect(snap).not.toBeNull();
61
+ expect(snap.timestamp).toBe(BASE_TIME + 5000); // 5초 스냅샷 (7초 이하 중 가장 가까운)
62
+ });
63
+ test('버퍼에 데이터가 없으면 null', async () => {
64
+ const { fetcher } = createMockFetcher([]);
65
+ const buffer = new PlaybackBuffer(fetcher, totalRange);
66
+ await buffer.loadInitial(BASE_TIME);
67
+ expect(buffer.getSnapshotAt(BASE_TIME + 5000)).toBeNull();
68
+ });
69
+ test('playHead가 첫 스냅샷보다 이전이면 null', async () => {
70
+ const snaps = makeSnapshots(BASE_TIME + 10000, BASE_TIME + CHUNK_DURATION, 5000);
71
+ const { fetcher } = createMockFetcher(snaps);
72
+ const buffer = new PlaybackBuffer(fetcher, totalRange);
73
+ await buffer.loadInitial(BASE_TIME);
74
+ expect(buffer.getSnapshotAt(BASE_TIME + 5000)).toBeNull();
75
+ });
76
+ });
77
+ describe('getNextSnapshotTime', () => {
78
+ test('현재 이후 가장 가까운 스냅샷 시간을 반환한다', async () => {
79
+ const snaps = makeSnapshots(BASE_TIME, BASE_TIME + CHUNK_DURATION, 5000);
80
+ const { fetcher } = createMockFetcher(snaps);
81
+ const buffer = new PlaybackBuffer(fetcher, totalRange);
82
+ await buffer.loadInitial(BASE_TIME);
83
+ expect(buffer.getNextSnapshotTime(BASE_TIME + 3000)).toBe(BASE_TIME + 5000);
84
+ });
85
+ test('마지막 스냅샷 이후면 null', async () => {
86
+ const snaps = makeSnapshots(BASE_TIME, BASE_TIME + 10000, 5000);
87
+ const { fetcher } = createMockFetcher(snaps);
88
+ const buffer = new PlaybackBuffer(fetcher, totalRange);
89
+ await buffer.loadInitial(BASE_TIME);
90
+ expect(buffer.getNextSnapshotTime(BASE_TIME + 10000)).toBeNull();
91
+ });
92
+ });
93
+ describe('checkPrefetch', () => {
94
+ test('남은 버퍼가 1분 이하이면 다음 청크를 prefetch한다', async () => {
95
+ const { fetcher, calls } = createMockFetcher();
96
+ const buffer = new PlaybackBuffer(fetcher, totalRange);
97
+ await buffer.loadInitial(BASE_TIME);
98
+ expect(calls).toHaveLength(1);
99
+ // playHead를 9분 위치로 (남은 1분)
100
+ await buffer.checkPrefetch(BASE_TIME + 9 * ONE_MIN);
101
+ expect(calls).toHaveLength(2);
102
+ expect(calls[1].from).toBe(BASE_TIME + CHUNK_DURATION); // 10분 후부터
103
+ });
104
+ test('남은 버퍼가 1분 초과이면 prefetch하지 않는다', async () => {
105
+ const { fetcher, calls } = createMockFetcher();
106
+ const buffer = new PlaybackBuffer(fetcher, totalRange);
107
+ await buffer.loadInitial(BASE_TIME);
108
+ // playHead를 5분 위치로 (남은 5분)
109
+ await buffer.checkPrefetch(BASE_TIME + 5 * ONE_MIN);
110
+ expect(calls).toHaveLength(1); // 추가 fetch 없음
111
+ });
112
+ test('전체 범위 끝에 도달하면 prefetch하지 않는다', async () => {
113
+ const shortRange = {
114
+ from: new Date(BASE_TIME),
115
+ to: new Date(BASE_TIME + CHUNK_DURATION) // 전체 10분
116
+ };
117
+ const { fetcher, calls } = createMockFetcher();
118
+ const buffer = new PlaybackBuffer(fetcher, shortRange);
119
+ await buffer.loadInitial(BASE_TIME);
120
+ // 9분 위치 — 하지만 전체 범위가 10분이므로 더 가져올 게 없음
121
+ await buffer.checkPrefetch(BASE_TIME + 9 * ONE_MIN);
122
+ expect(calls).toHaveLength(1);
123
+ });
124
+ test('중복 prefetch를 방지한다', async () => {
125
+ let resolveSecond;
126
+ let fetchCount = 0;
127
+ const slowFetcher = async (from, to) => {
128
+ fetchCount++;
129
+ if (fetchCount === 2) {
130
+ // 두 번째 fetch는 느리게
131
+ await new Promise(r => { resolveSecond = r; });
132
+ }
133
+ return makeSnapshots(from.getTime(), to.getTime());
134
+ };
135
+ const buffer = new PlaybackBuffer(slowFetcher, totalRange);
136
+ await buffer.loadInitial(BASE_TIME);
137
+ // 동시에 두 번 checkPrefetch
138
+ const p1 = buffer.checkPrefetch(BASE_TIME + 9 * ONE_MIN);
139
+ const p2 = buffer.checkPrefetch(BASE_TIME + 9 * ONE_MIN);
140
+ resolveSecond();
141
+ await Promise.all([p1, p2]);
142
+ // 초기 1 + prefetch 1 = 2 (중복 방지)
143
+ expect(fetchCount).toBe(2);
144
+ });
145
+ });
146
+ describe('seek', () => {
147
+ test('기존 버퍼를 폐기하고 새 위치에서 로드한다', async () => {
148
+ const { fetcher, calls } = createMockFetcher();
149
+ const buffer = new PlaybackBuffer(fetcher, totalRange);
150
+ await buffer.loadInitial(BASE_TIME);
151
+ expect(buffer.getBufferedRanges()).toHaveLength(1);
152
+ await buffer.seek(BASE_TIME + 30 * ONE_MIN);
153
+ expect(buffer.getBufferedRanges()).toHaveLength(1); // 이전 버퍼 폐기, 새 청크 1개
154
+ expect(buffer.getBufferedRanges()[0].from).toBe(BASE_TIME + 30 * ONE_MIN);
155
+ });
156
+ test('seek 후 이전 데이터에 접근 불가', async () => {
157
+ const snaps = makeSnapshots(BASE_TIME, BASE_TIME + ONE_HOUR, 5000);
158
+ const { fetcher } = createMockFetcher(snaps);
159
+ const buffer = new PlaybackBuffer(fetcher, totalRange);
160
+ await buffer.loadInitial(BASE_TIME);
161
+ expect(buffer.getSnapshotAt(BASE_TIME + 5000)).not.toBeNull();
162
+ await buffer.seek(BASE_TIME + 30 * ONE_MIN);
163
+ expect(buffer.getSnapshotAt(BASE_TIME + 5000)).toBeNull(); // 이전 데이터 없음
164
+ });
165
+ });
166
+ describe('getBufferedRanges', () => {
167
+ test('로드된 청크들의 시간 범위를 반환한다', async () => {
168
+ const { fetcher } = createMockFetcher();
169
+ const buffer = new PlaybackBuffer(fetcher, totalRange);
170
+ await buffer.loadInitial(BASE_TIME);
171
+ await buffer.checkPrefetch(BASE_TIME + 9 * ONE_MIN); // prefetch 트리거
172
+ const ranges = buffer.getBufferedRanges();
173
+ expect(ranges).toHaveLength(2);
174
+ expect(ranges[0].from).toBe(BASE_TIME);
175
+ expect(ranges[0].to).toBe(BASE_TIME + CHUNK_DURATION);
176
+ expect(ranges[1].from).toBe(BASE_TIME + CHUNK_DURATION);
177
+ expect(ranges[1].to).toBe(BASE_TIME + 2 * CHUNK_DURATION);
178
+ });
179
+ });
180
+ describe('hasDataAt', () => {
181
+ test('버퍼 내 시점은 true', async () => {
182
+ const { fetcher } = createMockFetcher();
183
+ const buffer = new PlaybackBuffer(fetcher, totalRange);
184
+ await buffer.loadInitial(BASE_TIME);
185
+ expect(buffer.hasDataAt(BASE_TIME + 5 * ONE_MIN)).toBe(true);
186
+ });
187
+ test('버퍼 외 시점은 false', async () => {
188
+ const { fetcher } = createMockFetcher();
189
+ const buffer = new PlaybackBuffer(fetcher, totalRange);
190
+ await buffer.loadInitial(BASE_TIME);
191
+ expect(buffer.hasDataAt(BASE_TIME + 15 * ONE_MIN)).toBe(false);
192
+ });
193
+ });
194
+ describe('clear', () => {
195
+ test('모든 버퍼를 폐기한다', async () => {
196
+ const { fetcher } = createMockFetcher();
197
+ const buffer = new PlaybackBuffer(fetcher, totalRange);
198
+ await buffer.loadInitial(BASE_TIME);
199
+ expect(buffer.getBufferedRanges()).toHaveLength(1);
200
+ buffer.clear();
201
+ expect(buffer.getBufferedRanges()).toHaveLength(0);
202
+ });
203
+ });
204
+ describe('연속 재생 시나리오', () => {
205
+ test('10분 → prefetch → 20분 연속 재생', async () => {
206
+ const { fetcher, calls } = createMockFetcher();
207
+ const buffer = new PlaybackBuffer(fetcher, totalRange);
208
+ // 1. 초기 로드 (0~10분)
209
+ await buffer.loadInitial(BASE_TIME);
210
+ expect(calls).toHaveLength(1);
211
+ // 2. 5분 위치 — prefetch 안 함
212
+ await buffer.checkPrefetch(BASE_TIME + 5 * ONE_MIN);
213
+ expect(calls).toHaveLength(1);
214
+ // 3. 9분 위치 — prefetch 트리거 (10~20분)
215
+ await buffer.checkPrefetch(BASE_TIME + 9 * ONE_MIN);
216
+ expect(calls).toHaveLength(2);
217
+ // 4. 15분 위치 — 두 번째 청크에서 데이터 조회 가능
218
+ expect(buffer.hasDataAt(BASE_TIME + 15 * ONE_MIN)).toBe(true);
219
+ // 5. 19분 위치 — 다시 prefetch (20~30분)
220
+ await buffer.checkPrefetch(BASE_TIME + 19 * ONE_MIN);
221
+ expect(calls).toHaveLength(3);
222
+ });
223
+ test('seek 후 prefetch 정상 동작', async () => {
224
+ const { fetcher, calls } = createMockFetcher();
225
+ const buffer = new PlaybackBuffer(fetcher, totalRange);
226
+ await buffer.loadInitial(BASE_TIME);
227
+ // seek to 40분
228
+ await buffer.seek(BASE_TIME + 40 * ONE_MIN);
229
+ expect(calls).toHaveLength(2); // initial + seek
230
+ // 49분 위치 — prefetch (50~60분)
231
+ await buffer.checkPrefetch(BASE_TIME + 49 * ONE_MIN);
232
+ expect(calls).toHaveLength(3);
233
+ expect(calls[2].from).toBe(BASE_TIME + 50 * ONE_MIN);
234
+ });
235
+ });
236
+ describe('경계 조건', () => {
237
+ test('전체 범위가 10분 미만', async () => {
238
+ const shortRange = {
239
+ from: new Date(BASE_TIME),
240
+ to: new Date(BASE_TIME + 5 * ONE_MIN)
241
+ };
242
+ const { fetcher, calls } = createMockFetcher();
243
+ const buffer = new PlaybackBuffer(fetcher, shortRange);
244
+ await buffer.loadInitial(BASE_TIME);
245
+ expect(calls).toHaveLength(1);
246
+ expect(calls[0].to).toBe(BASE_TIME + 5 * ONE_MIN); // 5분까지만
247
+ });
248
+ test('끝 경계에서 청크 크기 조정', async () => {
249
+ const range55min = {
250
+ from: new Date(BASE_TIME),
251
+ to: new Date(BASE_TIME + 55 * ONE_MIN)
252
+ };
253
+ const { fetcher, calls } = createMockFetcher();
254
+ const buffer = new PlaybackBuffer(fetcher, range55min);
255
+ // 50분 위치에서 시작
256
+ await buffer.loadInitial(BASE_TIME + 50 * ONE_MIN);
257
+ expect(calls[0].to).toBe(BASE_TIME + 55 * ONE_MIN); // 55분까지만 (10분이 아닌 5분)
258
+ });
259
+ });
260
+ });
261
+ //# sourceMappingURL=playback-buffer.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playback-buffer.test.js","sourceRoot":"","sources":["../../../src/graphql/playback-buffer.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { PlaybackBuffer, PlaybackSnapshot, CHUNK_DURATION, PREFETCH_THRESHOLD } from './playback-buffer'\n\n// --- 테스트 헬퍼 ---\n\nconst BASE_TIME = new Date('2026-04-12T10:00:00Z').getTime()\nconst ONE_HOUR = 3600000\nconst ONE_MIN = 60000\n\nfunction makeSnapshots(fromMs: number, toMs: number, intervalMs: number = 1000): PlaybackSnapshot[] {\n const snaps: PlaybackSnapshot[] = []\n for (let t = fromMs; t < toMs; t += intervalMs) {\n snaps.push({\n timestamp: t,\n data: { vehicle: { id: 'AGV01', position: t - fromMs } }\n })\n }\n return snaps\n}\n\nfunction createMockFetcher(allSnapshots?: PlaybackSnapshot[]) {\n const calls: { from: number; to: number }[] = []\n\n const fetcher = async (from: Date, to: Date) => {\n calls.push({ from: from.getTime(), to: to.getTime() })\n\n if (allSnapshots) {\n return allSnapshots.filter(s => s.timestamp >= from.getTime() && s.timestamp < to.getTime())\n }\n\n // 기본: 1초 간격 스냅샷 생성\n return makeSnapshots(from.getTime(), to.getTime())\n }\n\n return { fetcher, calls }\n}\n\n// --- PlaybackBuffer 단위 테스트 ---\n\ndescribe('PlaybackBuffer', () => {\n const totalRange = {\n from: new Date(BASE_TIME),\n to: new Date(BASE_TIME + ONE_HOUR)\n }\n\n describe('loadInitial', () => {\n test('10분 청크를 로드한다', async () => {\n const { fetcher, calls } = createMockFetcher()\n const buffer = new PlaybackBuffer(fetcher, totalRange)\n\n await buffer.loadInitial(BASE_TIME)\n\n expect(calls).toHaveLength(1)\n expect(calls[0].from).toBe(BASE_TIME)\n expect(calls[0].to).toBe(BASE_TIME + CHUNK_DURATION)\n })\n\n test('기존 버퍼를 폐기하고 새로 로드한다', async () => {\n const { fetcher, calls } = createMockFetcher()\n const buffer = new PlaybackBuffer(fetcher, totalRange)\n\n await buffer.loadInitial(BASE_TIME)\n await buffer.loadInitial(BASE_TIME + 30 * ONE_MIN)\n\n expect(calls).toHaveLength(2)\n expect(buffer.getBufferedRanges()).toHaveLength(1)\n expect(buffer.getBufferedRanges()[0].from).toBe(BASE_TIME + 30 * ONE_MIN)\n })\n })\n\n describe('getSnapshotAt', () => {\n test('playHead 이하인 가장 가까운 스냅샷을 반환한다', async () => {\n const snaps = makeSnapshots(BASE_TIME, BASE_TIME + CHUNK_DURATION, 5000)\n const { fetcher } = createMockFetcher(snaps)\n const buffer = new PlaybackBuffer(fetcher, totalRange)\n\n await buffer.loadInitial(BASE_TIME)\n\n const snap = buffer.getSnapshotAt(BASE_TIME + 7000)\n expect(snap).not.toBeNull()\n expect(snap!.timestamp).toBe(BASE_TIME + 5000) // 5초 스냅샷 (7초 이하 중 가장 가까운)\n })\n\n test('버퍼에 데이터가 없으면 null', async () => {\n const { fetcher } = createMockFetcher([])\n const buffer = new PlaybackBuffer(fetcher, totalRange)\n\n await buffer.loadInitial(BASE_TIME)\n\n expect(buffer.getSnapshotAt(BASE_TIME + 5000)).toBeNull()\n })\n\n test('playHead가 첫 스냅샷보다 이전이면 null', async () => {\n const snaps = makeSnapshots(BASE_TIME + 10000, BASE_TIME + CHUNK_DURATION, 5000)\n const { fetcher } = createMockFetcher(snaps)\n const buffer = new PlaybackBuffer(fetcher, totalRange)\n\n await buffer.loadInitial(BASE_TIME)\n\n expect(buffer.getSnapshotAt(BASE_TIME + 5000)).toBeNull()\n })\n })\n\n describe('getNextSnapshotTime', () => {\n test('현재 이후 가장 가까운 스냅샷 시간을 반환한다', async () => {\n const snaps = makeSnapshots(BASE_TIME, BASE_TIME + CHUNK_DURATION, 5000)\n const { fetcher } = createMockFetcher(snaps)\n const buffer = new PlaybackBuffer(fetcher, totalRange)\n\n await buffer.loadInitial(BASE_TIME)\n\n expect(buffer.getNextSnapshotTime(BASE_TIME + 3000)).toBe(BASE_TIME + 5000)\n })\n\n test('마지막 스냅샷 이후면 null', async () => {\n const snaps = makeSnapshots(BASE_TIME, BASE_TIME + 10000, 5000)\n const { fetcher } = createMockFetcher(snaps)\n const buffer = new PlaybackBuffer(fetcher, totalRange)\n\n await buffer.loadInitial(BASE_TIME)\n\n expect(buffer.getNextSnapshotTime(BASE_TIME + 10000)).toBeNull()\n })\n })\n\n describe('checkPrefetch', () => {\n test('남은 버퍼가 1분 이하이면 다음 청크를 prefetch한다', async () => {\n const { fetcher, calls } = createMockFetcher()\n const buffer = new PlaybackBuffer(fetcher, totalRange)\n\n await buffer.loadInitial(BASE_TIME)\n expect(calls).toHaveLength(1)\n\n // playHead를 9분 위치로 (남은 1분)\n await buffer.checkPrefetch(BASE_TIME + 9 * ONE_MIN)\n\n expect(calls).toHaveLength(2)\n expect(calls[1].from).toBe(BASE_TIME + CHUNK_DURATION) // 10분 후부터\n })\n\n test('남은 버퍼가 1분 초과이면 prefetch하지 않는다', async () => {\n const { fetcher, calls } = createMockFetcher()\n const buffer = new PlaybackBuffer(fetcher, totalRange)\n\n await buffer.loadInitial(BASE_TIME)\n\n // playHead를 5분 위치로 (남은 5분)\n await buffer.checkPrefetch(BASE_TIME + 5 * ONE_MIN)\n\n expect(calls).toHaveLength(1) // 추가 fetch 없음\n })\n\n test('전체 범위 끝에 도달하면 prefetch하지 않는다', async () => {\n const shortRange = {\n from: new Date(BASE_TIME),\n to: new Date(BASE_TIME + CHUNK_DURATION) // 전체 10분\n }\n const { fetcher, calls } = createMockFetcher()\n const buffer = new PlaybackBuffer(fetcher, shortRange)\n\n await buffer.loadInitial(BASE_TIME)\n\n // 9분 위치 — 하지만 전체 범위가 10분이므로 더 가져올 게 없음\n await buffer.checkPrefetch(BASE_TIME + 9 * ONE_MIN)\n\n expect(calls).toHaveLength(1)\n })\n\n test('중복 prefetch를 방지한다', async () => {\n let resolveSecond: () => void\n let fetchCount = 0\n\n const slowFetcher = async (from: Date, to: Date) => {\n fetchCount++\n if (fetchCount === 2) {\n // 두 번째 fetch는 느리게\n await new Promise<void>(r => { resolveSecond = r })\n }\n return makeSnapshots(from.getTime(), to.getTime())\n }\n\n const buffer = new PlaybackBuffer(slowFetcher, totalRange)\n await buffer.loadInitial(BASE_TIME)\n\n // 동시에 두 번 checkPrefetch\n const p1 = buffer.checkPrefetch(BASE_TIME + 9 * ONE_MIN)\n const p2 = buffer.checkPrefetch(BASE_TIME + 9 * ONE_MIN)\n\n resolveSecond!()\n await Promise.all([p1, p2])\n\n // 초기 1 + prefetch 1 = 2 (중복 방지)\n expect(fetchCount).toBe(2)\n })\n })\n\n describe('seek', () => {\n test('기존 버퍼를 폐기하고 새 위치에서 로드한다', async () => {\n const { fetcher, calls } = createMockFetcher()\n const buffer = new PlaybackBuffer(fetcher, totalRange)\n\n await buffer.loadInitial(BASE_TIME)\n expect(buffer.getBufferedRanges()).toHaveLength(1)\n\n await buffer.seek(BASE_TIME + 30 * ONE_MIN)\n\n expect(buffer.getBufferedRanges()).toHaveLength(1) // 이전 버퍼 폐기, 새 청크 1개\n expect(buffer.getBufferedRanges()[0].from).toBe(BASE_TIME + 30 * ONE_MIN)\n })\n\n test('seek 후 이전 데이터에 접근 불가', async () => {\n const snaps = makeSnapshots(BASE_TIME, BASE_TIME + ONE_HOUR, 5000)\n const { fetcher } = createMockFetcher(snaps)\n const buffer = new PlaybackBuffer(fetcher, totalRange)\n\n await buffer.loadInitial(BASE_TIME)\n expect(buffer.getSnapshotAt(BASE_TIME + 5000)).not.toBeNull()\n\n await buffer.seek(BASE_TIME + 30 * ONE_MIN)\n expect(buffer.getSnapshotAt(BASE_TIME + 5000)).toBeNull() // 이전 데이터 없음\n })\n })\n\n describe('getBufferedRanges', () => {\n test('로드된 청크들의 시간 범위를 반환한다', async () => {\n const { fetcher } = createMockFetcher()\n const buffer = new PlaybackBuffer(fetcher, totalRange)\n\n await buffer.loadInitial(BASE_TIME)\n await buffer.checkPrefetch(BASE_TIME + 9 * ONE_MIN) // prefetch 트리거\n\n const ranges = buffer.getBufferedRanges()\n expect(ranges).toHaveLength(2)\n expect(ranges[0].from).toBe(BASE_TIME)\n expect(ranges[0].to).toBe(BASE_TIME + CHUNK_DURATION)\n expect(ranges[1].from).toBe(BASE_TIME + CHUNK_DURATION)\n expect(ranges[1].to).toBe(BASE_TIME + 2 * CHUNK_DURATION)\n })\n })\n\n describe('hasDataAt', () => {\n test('버퍼 내 시점은 true', async () => {\n const { fetcher } = createMockFetcher()\n const buffer = new PlaybackBuffer(fetcher, totalRange)\n\n await buffer.loadInitial(BASE_TIME)\n\n expect(buffer.hasDataAt(BASE_TIME + 5 * ONE_MIN)).toBe(true)\n })\n\n test('버퍼 외 시점은 false', async () => {\n const { fetcher } = createMockFetcher()\n const buffer = new PlaybackBuffer(fetcher, totalRange)\n\n await buffer.loadInitial(BASE_TIME)\n\n expect(buffer.hasDataAt(BASE_TIME + 15 * ONE_MIN)).toBe(false)\n })\n })\n\n describe('clear', () => {\n test('모든 버퍼를 폐기한다', async () => {\n const { fetcher } = createMockFetcher()\n const buffer = new PlaybackBuffer(fetcher, totalRange)\n\n await buffer.loadInitial(BASE_TIME)\n expect(buffer.getBufferedRanges()).toHaveLength(1)\n\n buffer.clear()\n expect(buffer.getBufferedRanges()).toHaveLength(0)\n })\n })\n\n describe('연속 재생 시나리오', () => {\n test('10분 → prefetch → 20분 연속 재생', async () => {\n const { fetcher, calls } = createMockFetcher()\n const buffer = new PlaybackBuffer(fetcher, totalRange)\n\n // 1. 초기 로드 (0~10분)\n await buffer.loadInitial(BASE_TIME)\n expect(calls).toHaveLength(1)\n\n // 2. 5분 위치 — prefetch 안 함\n await buffer.checkPrefetch(BASE_TIME + 5 * ONE_MIN)\n expect(calls).toHaveLength(1)\n\n // 3. 9분 위치 — prefetch 트리거 (10~20분)\n await buffer.checkPrefetch(BASE_TIME + 9 * ONE_MIN)\n expect(calls).toHaveLength(2)\n\n // 4. 15분 위치 — 두 번째 청크에서 데이터 조회 가능\n expect(buffer.hasDataAt(BASE_TIME + 15 * ONE_MIN)).toBe(true)\n\n // 5. 19분 위치 — 다시 prefetch (20~30분)\n await buffer.checkPrefetch(BASE_TIME + 19 * ONE_MIN)\n expect(calls).toHaveLength(3)\n })\n\n test('seek 후 prefetch 정상 동작', async () => {\n const { fetcher, calls } = createMockFetcher()\n const buffer = new PlaybackBuffer(fetcher, totalRange)\n\n await buffer.loadInitial(BASE_TIME)\n\n // seek to 40분\n await buffer.seek(BASE_TIME + 40 * ONE_MIN)\n expect(calls).toHaveLength(2) // initial + seek\n\n // 49분 위치 — prefetch (50~60분)\n await buffer.checkPrefetch(BASE_TIME + 49 * ONE_MIN)\n expect(calls).toHaveLength(3)\n expect(calls[2].from).toBe(BASE_TIME + 50 * ONE_MIN)\n })\n })\n\n describe('경계 조건', () => {\n test('전체 범위가 10분 미만', async () => {\n const shortRange = {\n from: new Date(BASE_TIME),\n to: new Date(BASE_TIME + 5 * ONE_MIN)\n }\n const { fetcher, calls } = createMockFetcher()\n const buffer = new PlaybackBuffer(fetcher, shortRange)\n\n await buffer.loadInitial(BASE_TIME)\n\n expect(calls).toHaveLength(1)\n expect(calls[0].to).toBe(BASE_TIME + 5 * ONE_MIN) // 5분까지만\n })\n\n test('끝 경계에서 청크 크기 조정', async () => {\n const range55min = {\n from: new Date(BASE_TIME),\n to: new Date(BASE_TIME + 55 * ONE_MIN)\n }\n const { fetcher, calls } = createMockFetcher()\n const buffer = new PlaybackBuffer(fetcher, range55min)\n\n // 50분 위치에서 시작\n await buffer.loadInitial(BASE_TIME + 50 * ONE_MIN)\n\n expect(calls[0].to).toBe(BASE_TIME + 55 * ONE_MIN) // 55분까지만 (10분이 아닌 5분)\n })\n })\n})\n"]}
@@ -0,0 +1,89 @@
1
+ import { Component, DataSubscriptionProvider } from '@hatiolab/things-scene';
2
+ export type PlaybackState = 'idle' | 'playing' | 'paused' | 'stopped';
3
+ export interface PlaybackStatus {
4
+ state: PlaybackState;
5
+ currentTime: string;
6
+ speed: number;
7
+ bufferedRanges?: {
8
+ from: number;
9
+ to: number;
10
+ }[];
11
+ }
12
+ export interface PlaybackConfig {
13
+ /** 플레이백 가능한 시간 범위 */
14
+ timeRange?: {
15
+ from: Date;
16
+ to: Date;
17
+ };
18
+ }
19
+ /**
20
+ * PlaybackProvider — YouTube 스트리밍 방식의 청크 기반 플레이백
21
+ *
22
+ * - 10분 단위로 데이터를 fetch하여 버퍼에 적재
23
+ * - 남은 데이터 1분 이하 시 다음 10분 prefetch
24
+ * - seek 시 기존 버퍼 전체 폐기 후 fresh fetch
25
+ * - requestAnimationFrame 기반 재생 루프
26
+ */
27
+ export declare class PlaybackProvider implements DataSubscriptionProvider {
28
+ private _components;
29
+ private _buffer;
30
+ private _state;
31
+ private _speed;
32
+ private _playHead;
33
+ private _lastFrameTime;
34
+ private _rafId;
35
+ private _lastDistributedSnapshot;
36
+ private _onStatusChange?;
37
+ private _totalRange;
38
+ constructor(onStatusChange?: (status: PlaybackStatus) => void);
39
+ get state(): PlaybackState;
40
+ get speed(): number;
41
+ get currentTime(): string;
42
+ /**
43
+ * DataSubscriptionProvider.subscribe 구현
44
+ */
45
+ subscribe(tag: string, component: Component): Promise<{
46
+ unsubscribe: () => void;
47
+ }>;
48
+ /**
49
+ * 플레이백 시작
50
+ */
51
+ start(fromTime: Date, speed?: number, totalRange?: {
52
+ from: Date;
53
+ to: Date;
54
+ }): Promise<void>;
55
+ /**
56
+ * 일시정지
57
+ */
58
+ pause(): void;
59
+ /**
60
+ * 재개
61
+ */
62
+ resume(): void;
63
+ /**
64
+ * seek — 기존 버퍼 폐기 + 새 위치에서 fresh fetch
65
+ */
66
+ seek(toTime: Date): Promise<void>;
67
+ /**
68
+ * 배속 변경
69
+ */
70
+ setSpeed(speed: number): void;
71
+ /**
72
+ * 중지
73
+ */
74
+ stop(): void;
75
+ /**
76
+ * DataSubscriptionProvider.dispose 구현
77
+ */
78
+ dispose(): void;
79
+ private _startPlayLoop;
80
+ private _stopPlayLoop;
81
+ private _playLoop;
82
+ private _distributeSnapshot;
83
+ private _createFetcher;
84
+ /**
85
+ * 백엔드 응답을 PlaybackSnapshot[] 형식으로 변환
86
+ */
87
+ private _parsePlaybackResponse;
88
+ private _notifyStatus;
89
+ }
@@ -0,0 +1,258 @@
1
+ import gql from 'graphql-tag';
2
+ import { PlaybackBuffer } from './playback-buffer.js';
3
+ /**
4
+ * PlaybackProvider — YouTube 스트리밍 방식의 청크 기반 플레이백
5
+ *
6
+ * - 10분 단위로 데이터를 fetch하여 버퍼에 적재
7
+ * - 남은 데이터 1분 이하 시 다음 10분 prefetch
8
+ * - seek 시 기존 버퍼 전체 폐기 후 fresh fetch
9
+ * - requestAnimationFrame 기반 재생 루프
10
+ */
11
+ export class PlaybackProvider {
12
+ constructor(onStatusChange) {
13
+ this._components = new Map();
14
+ this._buffer = null;
15
+ this._state = 'idle';
16
+ this._speed = 1;
17
+ this._playHead = 0;
18
+ this._lastFrameTime = 0;
19
+ this._rafId = 0;
20
+ this._lastDistributedSnapshot = null;
21
+ this._totalRange = null;
22
+ this._playLoop = (now) => {
23
+ if (this._state !== 'playing' || !this._buffer)
24
+ return;
25
+ const delta = (now - this._lastFrameTime) * this._speed;
26
+ this._lastFrameTime = now;
27
+ this._playHead += delta;
28
+ // 전체 범위 끝 도달 체크
29
+ if (this._totalRange && this._playHead >= this._totalRange.to.getTime()) {
30
+ this._playHead = this._totalRange.to.getTime();
31
+ this._state = 'stopped';
32
+ this._notifyStatus();
33
+ return;
34
+ }
35
+ // 현재 스냅샷 배포
36
+ const snap = this._buffer.getSnapshotAt(this._playHead);
37
+ if (snap && snap !== this._lastDistributedSnapshot) {
38
+ this._distributeSnapshot(snap.data);
39
+ this._lastDistributedSnapshot = snap;
40
+ }
41
+ // prefetch 체크 (비동기, 루프 블로킹 안 함)
42
+ this._buffer.checkPrefetch(this._playHead);
43
+ this._notifyStatus();
44
+ // 다음 프레임
45
+ this._rafId = requestAnimationFrame(this._playLoop);
46
+ };
47
+ this._onStatusChange = onStatusChange;
48
+ }
49
+ get state() { return this._state; }
50
+ get speed() { return this._speed; }
51
+ get currentTime() { return this._playHead ? new Date(this._playHead).toISOString() : ''; }
52
+ /**
53
+ * DataSubscriptionProvider.subscribe 구현
54
+ */
55
+ async subscribe(tag, component) {
56
+ if (!this._components.has(tag)) {
57
+ this._components.set(tag, new Set());
58
+ }
59
+ this._components.get(tag).add(component);
60
+ return {
61
+ unsubscribe: () => {
62
+ const components = this._components.get(tag);
63
+ if (components) {
64
+ components.delete(component);
65
+ if (components.size === 0) {
66
+ this._components.delete(tag);
67
+ }
68
+ }
69
+ }
70
+ };
71
+ }
72
+ /**
73
+ * 플레이백 시작
74
+ */
75
+ async start(fromTime, speed = 1, totalRange) {
76
+ this.stop();
77
+ this._speed = speed;
78
+ this._totalRange = totalRange || { from: fromTime, to: new Date(fromTime.getTime() + 3600000) };
79
+ // 버퍼 생성 + 초기 로드
80
+ this._buffer = new PlaybackBuffer(this._createFetcher(), this._totalRange);
81
+ await this._buffer.loadInitial(fromTime.getTime());
82
+ this._playHead = fromTime.getTime();
83
+ this._state = 'playing';
84
+ this._lastFrameTime = performance.now();
85
+ this._startPlayLoop();
86
+ this._notifyStatus();
87
+ }
88
+ /**
89
+ * 일시정지
90
+ */
91
+ pause() {
92
+ if (this._state !== 'playing')
93
+ return;
94
+ this._state = 'paused';
95
+ this._stopPlayLoop();
96
+ this._notifyStatus();
97
+ }
98
+ /**
99
+ * 재개
100
+ */
101
+ resume() {
102
+ if (this._state !== 'paused')
103
+ return;
104
+ this._state = 'playing';
105
+ this._lastFrameTime = performance.now();
106
+ this._startPlayLoop();
107
+ this._notifyStatus();
108
+ }
109
+ /**
110
+ * seek — 기존 버퍼 폐기 + 새 위치에서 fresh fetch
111
+ */
112
+ async seek(toTime) {
113
+ if (!this._buffer)
114
+ return;
115
+ const wasPlaying = this._state === 'playing';
116
+ this._stopPlayLoop();
117
+ // 버퍼 전체 폐기 + 새 위치 로드
118
+ await this._buffer.seek(toTime.getTime());
119
+ this._playHead = toTime.getTime();
120
+ this._lastDistributedSnapshot = null;
121
+ // 새 위치의 첫 스냅샷 즉시 배포
122
+ const snap = this._buffer.getSnapshotAt(this._playHead);
123
+ if (snap) {
124
+ this._distributeSnapshot(snap.data);
125
+ this._lastDistributedSnapshot = snap;
126
+ }
127
+ if (wasPlaying) {
128
+ this._state = 'playing';
129
+ this._lastFrameTime = performance.now();
130
+ this._startPlayLoop();
131
+ }
132
+ this._notifyStatus();
133
+ }
134
+ /**
135
+ * 배속 변경
136
+ */
137
+ setSpeed(speed) {
138
+ this._speed = speed;
139
+ this._notifyStatus();
140
+ }
141
+ /**
142
+ * 중지
143
+ */
144
+ stop() {
145
+ var _a;
146
+ this._stopPlayLoop();
147
+ (_a = this._buffer) === null || _a === void 0 ? void 0 : _a.clear();
148
+ this._buffer = null;
149
+ this._state = 'stopped';
150
+ this._playHead = 0;
151
+ this._lastDistributedSnapshot = null;
152
+ this._notifyStatus();
153
+ }
154
+ /**
155
+ * DataSubscriptionProvider.dispose 구현
156
+ */
157
+ dispose() {
158
+ this.stop();
159
+ this._state = 'idle';
160
+ this._components.clear();
161
+ }
162
+ // --- 재생 루프 ---
163
+ _startPlayLoop() {
164
+ this._rafId = requestAnimationFrame(this._playLoop);
165
+ }
166
+ _stopPlayLoop() {
167
+ if (this._rafId) {
168
+ cancelAnimationFrame(this._rafId);
169
+ this._rafId = 0;
170
+ }
171
+ }
172
+ // --- 데이터 배포 ---
173
+ _distributeSnapshot(snapshotData) {
174
+ if (!snapshotData || typeof snapshotData !== 'object')
175
+ return;
176
+ for (const [tag, value] of Object.entries(snapshotData)) {
177
+ const components = this._components.get(tag);
178
+ if (components) {
179
+ for (const component of components) {
180
+ component.data = value;
181
+ }
182
+ }
183
+ }
184
+ }
185
+ // --- 데이터 fetcher ---
186
+ _createFetcher() {
187
+ return async (fromTime, toTime) => {
188
+ var _a;
189
+ try {
190
+ const { client } = await import('@operato/graphql');
191
+ const response = await client.query({
192
+ query: gql `
193
+ query PlaybackChunk($startTime: String!, $endTime: String!) {
194
+ playback(startTime: $startTime, endTime: $endTime) {
195
+ status
196
+ playback {
197
+ scenarios {
198
+ scenarioName
199
+ snapshots {
200
+ type
201
+ time
202
+ data
203
+ }
204
+ }
205
+ }
206
+ }
207
+ }
208
+ `,
209
+ variables: {
210
+ startTime: fromTime.toISOString().replace('T', ' ').replace(/\.\d+Z$/, ''),
211
+ endTime: toTime.toISOString().replace('T', ' ').replace(/\.\d+Z$/, '')
212
+ },
213
+ fetchPolicy: 'no-cache'
214
+ });
215
+ return this._parsePlaybackResponse((_a = response.data) === null || _a === void 0 ? void 0 : _a.playback);
216
+ }
217
+ catch (e) {
218
+ console.error('[PlaybackProvider] chunk fetch error:', e);
219
+ return [];
220
+ }
221
+ };
222
+ }
223
+ /**
224
+ * 백엔드 응답을 PlaybackSnapshot[] 형식으로 변환
225
+ */
226
+ _parsePlaybackResponse(response) {
227
+ var _a;
228
+ if (!(response === null || response === void 0 ? void 0 : response.status) || !((_a = response === null || response === void 0 ? void 0 : response.playback) === null || _a === void 0 ? void 0 : _a.scenarios))
229
+ return [];
230
+ const snapshotMap = new Map();
231
+ for (const scenario of response.playback.scenarios) {
232
+ for (const record of scenario.snapshots) {
233
+ const time = new Date(record.time.replace(' ', 'T')).getTime();
234
+ const data = typeof record.data === 'string' ? JSON.parse(record.data) : record.data;
235
+ if (!snapshotMap.has(time)) {
236
+ snapshotMap.set(time, {});
237
+ }
238
+ const merged = snapshotMap.get(time);
239
+ // scenarioName을 tag로 사용
240
+ merged[scenario.scenarioName] = data;
241
+ }
242
+ }
243
+ return Array.from(snapshotMap.entries())
244
+ .map(([timestamp, data]) => ({ timestamp, data }))
245
+ .sort((a, b) => a.timestamp - b.timestamp);
246
+ }
247
+ // --- 상태 통보 ---
248
+ _notifyStatus() {
249
+ var _a, _b;
250
+ (_a = this._onStatusChange) === null || _a === void 0 ? void 0 : _a.call(this, {
251
+ state: this._state,
252
+ currentTime: this._playHead ? new Date(this._playHead).toISOString() : '',
253
+ speed: this._speed,
254
+ bufferedRanges: (_b = this._buffer) === null || _b === void 0 ? void 0 : _b.getBufferedRanges()
255
+ });
256
+ }
257
+ }
258
+ //# sourceMappingURL=playback-subscription.js.map