@hurraey/interval-virtualizer 0.0.0-stage → 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HURRAEY
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,144 @@
1
- # Temporary Holding Version
1
+ # Interval Virtualizer
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 숫자 축 위의 구간 데이터에서 화면과 겹치는 항목을 찾는 TypeScript 라이브러리입니다. 오디오·비디오 타임라인, 일정, 문자 범위처럼 시작·종료 좌표가 있는 데이터에 사용할 수 있습니다.
4
+
5
+ 구간 조회와 화면 범위 계산은 React나 DOM에 의존하지 않습니다. React 18·19용 구독 훅은 별도 진입점으로 제공합니다.
6
+
7
+ ## 설치
8
+
9
+ Node.js 20 이상을 지원하며 ESM과 CommonJS, TypeScript 타입을 제공합니다.
10
+
11
+ ```sh
12
+ npm install @hurraey/interval-virtualizer
13
+ ```
14
+
15
+ React 훅을 사용할 때는 애플리케이션에 React 18.2 이상 또는 React 19가 필요합니다. 순수 계산 API만 사용하면 React를 설치할 필요가 없습니다.
16
+
17
+ ## 구간 조회
18
+
19
+ ```ts
20
+ import { createIntervalIndex, queryIntervals } from '@hurraey/interval-virtualizer';
21
+
22
+ const clips = [
23
+ { id: 'background', start: 0, end: 100 },
24
+ { id: 'effect', start: 75, end: 78 },
25
+ { id: 'later', start: 110, end: 120 },
26
+ ];
27
+
28
+ const index = createIntervalIndex(clips);
29
+ const result = queryIntervals(index, { start: 70, end: 80 });
30
+
31
+ console.log(result.intervals.map((clip) => clip.id));
32
+ // ['background', 'effect']
33
+ ```
34
+
35
+ - 시작 좌표와 종료 좌표는 같은 단위를 사용합니다. 초, 프레임, 픽셀, 문자 위치 모두 가능합니다.
36
+ - 조회 범위의 경계에 닿는 구간과 길이가 0인 구간도 포함합니다. 겹침 조건은 `start <= range.end && end >= range.start`입니다.
37
+ - 결과는 입력 순서를 유지하며 원래 객체를 반환합니다. 입력 배열을 정렬하거나 수정하지 않습니다.
38
+ - 음수 좌표를 지원합니다. 유한한 숫자가 아닌 좌표와 `end < start`인 항목은 인덱스에서 제외합니다.
39
+ - 잘못된 조회 범위에는 빈 배열과 `inspectedIntervalCount: 0`을 반환합니다.
40
+
41
+ 인덱스는 생성 시점의 좌표를 보관합니다. 항목을 추가·삭제하거나 시작·종료 좌표를 바꾸면 다시 생성해야 합니다. 스크롤과 줌만 바뀌면 같은 인덱스를 재사용합니다.
42
+
43
+ ### 조회 방식과 비용
44
+
45
+ 시작 좌표순으로 정렬한 배열에서 이진 탐색으로 후보의 끝을 찾습니다. 각 하위 구간의 최대 종료 좌표를 저장한 세그먼트 트리를 따라가며, 화면 전에 모두 끝난 구간은 건너뜁니다.
46
+
47
+ 인덱스 생성에는 정렬 비용과 항목 수에 비례하는 메모리가 필요합니다. 조회 후 입력 순서를 복원하는 정렬도 수행하므로 전체 조회가 항상 `O(log N)`에 끝나는 것은 아닙니다.
48
+
49
+ `inspectedIntervalCount`는 실제로 내려가 검사한 개별 항목 수입니다. 이진 탐색 비교, 트리 노드 방문, 정렬 비용이나 실행 시간을 나타내지 않습니다.
50
+
51
+ ## 화면 범위 계산
52
+
53
+ ```ts
54
+ import { getVirtualViewportRange } from '@hurraey/interval-virtualizer';
55
+
56
+ const range = getVirtualViewportRange({
57
+ offset: 4_100,
58
+ viewportSize: 800,
59
+ contentSize: 10_000,
60
+ chunkSize: 1_000,
61
+ overscanChunks: 2,
62
+ padding: 8,
63
+ });
64
+
65
+ console.log(range);
66
+ // { start: 1992, end: 7008 }
67
+ ```
68
+
69
+ `offset`은 현재 스크롤 위치이고 `viewportSize`와 `contentSize`는 각각 화면과 전체 콘텐츠의 크기입니다. `chunkSize`는 조회 범위를 묶는 단위입니다. 같은 청크 안에서는 작은 스크롤 변화에도 같은 범위를 반환할 수 있습니다.
70
+
71
+ `overscanChunks`는 양옆에 추가할 청크 수이며 기본값은 0입니다. 소수는 내림합니다. `padding`은 청크 바깥에 더할 여유 크기이며 기본값은 0입니다. 청크 수를 제외한 옵션은 모두 같은 좌표 단위를 사용합니다.
72
+
73
+ 결과는 콘텐츠 범위를 넘지 않습니다. 화면·콘텐츠·청크 크기가 0 이하이거나 입력이 유효하지 않으면 `null`을 반환합니다.
74
+
75
+ 픽셀 범위를 초 단위 인덱스에 조회하려면 두 경계를 초당 픽셀 수로 나눠 사용합니다.
76
+
77
+ ## React 연결
78
+
79
+ `@hurraey/interval-virtualizer/react`의 `useVirtualViewportRange`는 브라우저에서 사용합니다. 서버 렌더링용 스냅샷은 제공하지 않습니다. Next.js 등에서는 클라이언트에서 마운트된 영역에 연결하세요.
80
+
81
+ ```tsx
82
+ import { useCallback, useRef } from 'react';
83
+ import { useVirtualViewportRange } from '@hurraey/interval-virtualizer/react';
84
+
85
+ export function ScrollRange() {
86
+ const offsetRef = useRef(0);
87
+ const listenersRef = useRef(new Set<() => void>());
88
+ const subscribe = useCallback((listener: () => void) => {
89
+ listenersRef.current.add(listener);
90
+ return () => {
91
+ listenersRef.current.delete(listener);
92
+ };
93
+ }, []);
94
+
95
+ const range = useVirtualViewportRange({
96
+ offsetRef,
97
+ subscribe,
98
+ viewportSize: 800,
99
+ contentSize: 10_000,
100
+ chunkSize: 1_000,
101
+ overscanChunks: 1,
102
+ });
103
+
104
+ return (
105
+ <div
106
+ style={{ width: 800, overflowX: 'auto' }}
107
+ onScroll={(event) => {
108
+ offsetRef.current = event.currentTarget.scrollLeft;
109
+ listenersRef.current.forEach((listener) => listener());
110
+ }}
111
+ >
112
+ <div style={{ width: 10_000 }}>{range ? `${range.start} ~ ${range.end}` : '화면 크기 측정 전'}</div>
113
+ </div>
114
+ );
115
+ }
116
+ ```
117
+
118
+ 스크롤을 처리하는 쪽에서 ref를 먼저 갱신한 뒤 구독자에게 알립니다. `subscribe`는 구독 해제 함수를 반환해야 합니다. 훅은 계산된 범위가 같으면 같은 스냅샷 객체를 반환하며, 언마운트할 때 구독을 해제합니다.
119
+
120
+ DOM 크기 측정, 스크롤 이벤트 등록, `requestAnimationFrame` 예약, 실제 UI 렌더링은 사용하는 애플리케이션에서 담당합니다. 선택 중인 항목을 화면 밖에서도 유지하는 정책 역시 애플리케이션에서 처리합니다.
121
+
122
+ ## 공개 API
123
+
124
+ - `createIntervalIndex<T extends Interval>(intervals)` - 구간 인덱스를 생성합니다.
125
+ - `queryIntervals(index, range)` - `{ intervals, inspectedIntervalCount }`를 반환합니다.
126
+ - `getVirtualViewportRange(options)` - `{ start, end }` 또는 `null`을 반환합니다.
127
+ - `useVirtualViewportRange(options)` - React에서 외부 스크롤 알림을 구독합니다. `/react`에서 가져옵니다.
128
+
129
+ 루트 진입점은 `Interval`, `IntervalIndex`, `IntervalQueryRange`, `IntervalQueryResult`, `VirtualViewportOptions` 타입을 제공합니다. `/react`는 `VirtualViewportSubscriptionOptions` 타입을 제공합니다.
130
+
131
+ ## 개발과 검증
132
+
133
+ 저장소 루트에서 실행합니다.
134
+
135
+ ```sh
136
+ npm ci
137
+ npm run check
138
+ ```
139
+
140
+ 구간 조회·화면 범위·React 구독 테스트, 타입 검사, ESM·CommonJS 빌드와 실제 패키지 설치 검증을 실행합니다. 패키지 설치 검증은 임시 디렉터리에서 실행하고 종료 시 삭제합니다.
141
+
142
+ ## 라이선스
143
+
144
+ [MIT](./LICENSE)
package/dist/index.cjs ADDED
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ createIntervalIndex: () => createIntervalIndex,
24
+ getVirtualViewportRange: () => getVirtualViewportRange,
25
+ queryIntervals: () => queryIntervals
26
+ });
27
+ module.exports = __toCommonJS(src_exports);
28
+
29
+ // src/interval-index.ts
30
+ function findFirstIntervalStartingAfter(sortedIntervals, target) {
31
+ let low = 0;
32
+ let high = sortedIntervals.length;
33
+ while (low < high) {
34
+ const middle = low + Math.floor((high - low) / 2);
35
+ if (sortedIntervals[middle].start <= target) {
36
+ low = middle + 1;
37
+ } else {
38
+ high = middle;
39
+ }
40
+ }
41
+ return low;
42
+ }
43
+ function createMaxEndTree(sortedIntervals) {
44
+ let leafOffset = 1;
45
+ while (leafOffset < sortedIntervals.length) {
46
+ leafOffset *= 2;
47
+ }
48
+ const maxEndTree = Array(leafOffset * 2).fill(Number.NEGATIVE_INFINITY);
49
+ for (let index = 0; index < sortedIntervals.length; index += 1) {
50
+ maxEndTree[leafOffset + index] = sortedIntervals[index].end;
51
+ }
52
+ for (let nodeIndex = leafOffset - 1; nodeIndex > 0; nodeIndex -= 1) {
53
+ maxEndTree[nodeIndex] = Math.max(maxEndTree[nodeIndex * 2], maxEndTree[nodeIndex * 2 + 1]);
54
+ }
55
+ return { maxEndTree, leafOffset };
56
+ }
57
+ function createIntervalIndex(intervals) {
58
+ const sortedIntervals = intervals.map((interval, originalIndex) => ({
59
+ interval,
60
+ start: interval.start,
61
+ end: interval.end,
62
+ originalIndex
63
+ })).filter(
64
+ (indexedInterval) => Number.isFinite(indexedInterval.start) && Number.isFinite(indexedInterval.end) && indexedInterval.end >= indexedInterval.start
65
+ ).sort((left, right) => left.start - right.start || left.originalIndex - right.originalIndex);
66
+ const { maxEndTree, leafOffset } = createMaxEndTree(sortedIntervals);
67
+ return { sortedIntervals, maxEndTree, leafOffset };
68
+ }
69
+ function queryIntervals(index, range) {
70
+ if (!Number.isFinite(range.start) || !Number.isFinite(range.end) || range.end < range.start) {
71
+ return { intervals: [], inspectedIntervalCount: 0 };
72
+ }
73
+ const candidateEndIndex = findFirstIntervalStartingAfter(index.sortedIntervals, range.end);
74
+ if (candidateEndIndex === 0) {
75
+ return { intervals: [], inspectedIntervalCount: 0 };
76
+ }
77
+ const visibleIntervals = [];
78
+ const pendingNodes = [{ nodeIndex: 1, intervalStart: 0, intervalEnd: index.leafOffset }];
79
+ let inspectedIntervalCount = 0;
80
+ while (pendingNodes.length > 0) {
81
+ const currentNode = pendingNodes.pop();
82
+ if (!currentNode || currentNode.intervalStart >= candidateEndIndex || index.maxEndTree[currentNode.nodeIndex] < range.start) {
83
+ continue;
84
+ }
85
+ if (currentNode.intervalEnd - currentNode.intervalStart > 1) {
86
+ const middle = currentNode.intervalStart + Math.floor((currentNode.intervalEnd - currentNode.intervalStart) / 2);
87
+ pendingNodes.push(
88
+ { nodeIndex: currentNode.nodeIndex * 2 + 1, intervalStart: middle, intervalEnd: currentNode.intervalEnd },
89
+ { nodeIndex: currentNode.nodeIndex * 2, intervalStart: currentNode.intervalStart, intervalEnd: middle }
90
+ );
91
+ continue;
92
+ }
93
+ const indexedInterval = index.sortedIntervals[currentNode.intervalStart];
94
+ if (!indexedInterval) {
95
+ continue;
96
+ }
97
+ inspectedIntervalCount += 1;
98
+ if (indexedInterval.end >= range.start) {
99
+ visibleIntervals.push(indexedInterval);
100
+ }
101
+ }
102
+ visibleIntervals.sort((left, right) => left.originalIndex - right.originalIndex);
103
+ return {
104
+ intervals: visibleIntervals.map((indexedInterval) => indexedInterval.interval),
105
+ inspectedIntervalCount
106
+ };
107
+ }
108
+
109
+ // src/viewport-range.ts
110
+ function getVirtualViewportRange({
111
+ offset,
112
+ viewportSize,
113
+ contentSize,
114
+ chunkSize,
115
+ overscanChunks = 0,
116
+ padding = 0
117
+ }) {
118
+ if (![offset, viewportSize, contentSize, chunkSize, overscanChunks, padding].every(Number.isFinite) || viewportSize <= 0 || contentSize <= 0 || chunkSize <= 0 || overscanChunks < 0 || padding < 0) {
119
+ return null;
120
+ }
121
+ const clampedOffset = Math.max(0, Math.min(offset, contentSize - viewportSize));
122
+ const overscan = Math.floor(overscanChunks);
123
+ const firstChunk = Math.max(0, Math.floor(clampedOffset / chunkSize) - overscan);
124
+ const endChunk = Math.ceil((clampedOffset + viewportSize) / chunkSize) + overscan;
125
+ return {
126
+ start: Math.max(0, firstChunk * chunkSize - padding),
127
+ end: Math.min(contentSize, endChunk * chunkSize + padding)
128
+ };
129
+ }
130
+ // Annotate the CommonJS export names for ESM import in node:
131
+ 0 && (module.exports = {
132
+ createIntervalIndex,
133
+ getVirtualViewportRange,
134
+ queryIntervals
135
+ });
136
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/interval-index.ts","../src/viewport-range.ts"],"sourcesContent":["export { createIntervalIndex, queryIntervals } from './interval-index';\nexport type { Interval, IntervalIndex, IntervalQueryRange, IntervalQueryResult } from './interval-index';\nexport { getVirtualViewportRange } from './viewport-range';\nexport type { VirtualViewportOptions } from './viewport-range';\n","export interface Interval {\n start: number;\n end: number;\n}\n\ninterface IndexedInterval<TInterval extends Interval> {\n interval: TInterval;\n start: number;\n end: number;\n originalIndex: number;\n}\n\nexport interface IntervalIndex<TInterval extends Interval> {\n readonly sortedIntervals: ReadonlyArray<IndexedInterval<TInterval>>;\n readonly maxEndTree: readonly number[];\n readonly leafOffset: number;\n}\n\nexport interface IntervalQueryRange {\n start: number;\n end: number;\n}\n\nexport interface IntervalQueryResult<TInterval extends Interval> {\n intervals: TInterval[];\n inspectedIntervalCount: number;\n}\n\nfunction findFirstIntervalStartingAfter<TInterval extends Interval>(\n sortedIntervals: ReadonlyArray<IndexedInterval<TInterval>>,\n target: number,\n): number {\n let low = 0;\n let high = sortedIntervals.length;\n\n while (low < high) {\n const middle = low + Math.floor((high - low) / 2);\n if (sortedIntervals[middle].start <= target) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n\n return low;\n}\n\nfunction createMaxEndTree<TInterval extends Interval>(\n sortedIntervals: ReadonlyArray<IndexedInterval<TInterval>>,\n): {\n maxEndTree: number[];\n leafOffset: number;\n} {\n let leafOffset = 1;\n while (leafOffset < sortedIntervals.length) {\n leafOffset *= 2;\n }\n\n const maxEndTree = Array<number>(leafOffset * 2).fill(Number.NEGATIVE_INFINITY);\n for (let index = 0; index < sortedIntervals.length; index += 1) {\n maxEndTree[leafOffset + index] = sortedIntervals[index].end;\n }\n\n for (let nodeIndex = leafOffset - 1; nodeIndex > 0; nodeIndex -= 1) {\n maxEndTree[nodeIndex] = Math.max(maxEndTree[nodeIndex * 2], maxEndTree[nodeIndex * 2 + 1]);\n }\n\n return { maxEndTree, leafOffset };\n}\n\nexport function createIntervalIndex<TInterval extends Interval>(\n intervals: readonly TInterval[],\n): IntervalIndex<TInterval> {\n const sortedIntervals = intervals\n .map((interval, originalIndex) => ({\n interval,\n start: interval.start,\n end: interval.end,\n originalIndex,\n }))\n .filter(\n (indexedInterval) =>\n Number.isFinite(indexedInterval.start) &&\n Number.isFinite(indexedInterval.end) &&\n indexedInterval.end >= indexedInterval.start,\n )\n .sort((left, right) => left.start - right.start || left.originalIndex - right.originalIndex);\n\n // 하위 트리별 최대 종료 시간을 저장해 긴 구간 하나가 앞쪽의 짧은 구간 전체를 후보로 만들지 않게 한다.\n const { maxEndTree, leafOffset } = createMaxEndTree(sortedIntervals);\n return { sortedIntervals, maxEndTree, leafOffset };\n}\n\nexport function queryIntervals<TInterval extends Interval>(\n index: IntervalIndex<TInterval>,\n range: IntervalQueryRange,\n): IntervalQueryResult<TInterval> {\n if (!Number.isFinite(range.start) || !Number.isFinite(range.end) || range.end < range.start) {\n return { intervals: [], inspectedIntervalCount: 0 };\n }\n\n const candidateEndIndex = findFirstIntervalStartingAfter(index.sortedIntervals, range.end);\n if (candidateEndIndex === 0) {\n return { intervals: [], inspectedIntervalCount: 0 };\n }\n\n const visibleIntervals: Array<IndexedInterval<TInterval>> = [];\n const pendingNodes = [{ nodeIndex: 1, intervalStart: 0, intervalEnd: index.leafOffset }];\n let inspectedIntervalCount = 0;\n\n while (pendingNodes.length > 0) {\n const currentNode = pendingNodes.pop();\n if (\n !currentNode ||\n currentNode.intervalStart >= candidateEndIndex ||\n index.maxEndTree[currentNode.nodeIndex] < range.start\n ) {\n continue;\n }\n\n if (currentNode.intervalEnd - currentNode.intervalStart > 1) {\n const middle = currentNode.intervalStart + Math.floor((currentNode.intervalEnd - currentNode.intervalStart) / 2);\n pendingNodes.push(\n { nodeIndex: currentNode.nodeIndex * 2 + 1, intervalStart: middle, intervalEnd: currentNode.intervalEnd },\n { nodeIndex: currentNode.nodeIndex * 2, intervalStart: currentNode.intervalStart, intervalEnd: middle },\n );\n continue;\n }\n\n const indexedInterval = index.sortedIntervals[currentNode.intervalStart];\n if (!indexedInterval) {\n continue;\n }\n\n inspectedIntervalCount += 1;\n if (indexedInterval.end >= range.start) {\n visibleIntervals.push(indexedInterval);\n }\n }\n\n visibleIntervals.sort((left, right) => left.originalIndex - right.originalIndex);\n\n return {\n intervals: visibleIntervals.map((indexedInterval) => indexedInterval.interval),\n inspectedIntervalCount,\n };\n}\n","import type { Interval } from './interval-index';\n\nexport interface VirtualViewportOptions {\n offset: number;\n viewportSize: number;\n contentSize: number;\n chunkSize: number;\n overscanChunks?: number;\n padding?: number;\n}\n\n/** 같은 청크 안에서는 조회 범위를 유지해 작은 스크롤마다 항목을 다시 조회하지 않는다. */\nexport function getVirtualViewportRange({\n offset,\n viewportSize,\n contentSize,\n chunkSize,\n overscanChunks = 0,\n padding = 0,\n}: VirtualViewportOptions): Interval | null {\n if (\n ![offset, viewportSize, contentSize, chunkSize, overscanChunks, padding].every(Number.isFinite) ||\n viewportSize <= 0 ||\n contentSize <= 0 ||\n chunkSize <= 0 ||\n overscanChunks < 0 ||\n padding < 0\n ) {\n return null;\n }\n\n const clampedOffset = Math.max(0, Math.min(offset, contentSize - viewportSize));\n const overscan = Math.floor(overscanChunks);\n const firstChunk = Math.max(0, Math.floor(clampedOffset / chunkSize) - overscan);\n const endChunk = Math.ceil((clampedOffset + viewportSize) / chunkSize) + overscan;\n\n return {\n start: Math.max(0, firstChunk * chunkSize - padding),\n end: Math.min(contentSize, endChunk * chunkSize + padding),\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,38 @@
1
+ interface Interval {
2
+ start: number;
3
+ end: number;
4
+ }
5
+ interface IndexedInterval<TInterval extends Interval> {
6
+ interval: TInterval;
7
+ start: number;
8
+ end: number;
9
+ originalIndex: number;
10
+ }
11
+ interface IntervalIndex<TInterval extends Interval> {
12
+ readonly sortedIntervals: ReadonlyArray<IndexedInterval<TInterval>>;
13
+ readonly maxEndTree: readonly number[];
14
+ readonly leafOffset: number;
15
+ }
16
+ interface IntervalQueryRange {
17
+ start: number;
18
+ end: number;
19
+ }
20
+ interface IntervalQueryResult<TInterval extends Interval> {
21
+ intervals: TInterval[];
22
+ inspectedIntervalCount: number;
23
+ }
24
+ declare function createIntervalIndex<TInterval extends Interval>(intervals: readonly TInterval[]): IntervalIndex<TInterval>;
25
+ declare function queryIntervals<TInterval extends Interval>(index: IntervalIndex<TInterval>, range: IntervalQueryRange): IntervalQueryResult<TInterval>;
26
+
27
+ interface VirtualViewportOptions {
28
+ offset: number;
29
+ viewportSize: number;
30
+ contentSize: number;
31
+ chunkSize: number;
32
+ overscanChunks?: number;
33
+ padding?: number;
34
+ }
35
+ /** 같은 청크 안에서는 조회 범위를 유지해 작은 스크롤마다 항목을 다시 조회하지 않는다. */
36
+ declare function getVirtualViewportRange({ offset, viewportSize, contentSize, chunkSize, overscanChunks, padding, }: VirtualViewportOptions): Interval | null;
37
+
38
+ export { type Interval, type IntervalIndex, type IntervalQueryRange, type IntervalQueryResult, type VirtualViewportOptions, createIntervalIndex, getVirtualViewportRange, queryIntervals };
@@ -0,0 +1,38 @@
1
+ interface Interval {
2
+ start: number;
3
+ end: number;
4
+ }
5
+ interface IndexedInterval<TInterval extends Interval> {
6
+ interval: TInterval;
7
+ start: number;
8
+ end: number;
9
+ originalIndex: number;
10
+ }
11
+ interface IntervalIndex<TInterval extends Interval> {
12
+ readonly sortedIntervals: ReadonlyArray<IndexedInterval<TInterval>>;
13
+ readonly maxEndTree: readonly number[];
14
+ readonly leafOffset: number;
15
+ }
16
+ interface IntervalQueryRange {
17
+ start: number;
18
+ end: number;
19
+ }
20
+ interface IntervalQueryResult<TInterval extends Interval> {
21
+ intervals: TInterval[];
22
+ inspectedIntervalCount: number;
23
+ }
24
+ declare function createIntervalIndex<TInterval extends Interval>(intervals: readonly TInterval[]): IntervalIndex<TInterval>;
25
+ declare function queryIntervals<TInterval extends Interval>(index: IntervalIndex<TInterval>, range: IntervalQueryRange): IntervalQueryResult<TInterval>;
26
+
27
+ interface VirtualViewportOptions {
28
+ offset: number;
29
+ viewportSize: number;
30
+ contentSize: number;
31
+ chunkSize: number;
32
+ overscanChunks?: number;
33
+ padding?: number;
34
+ }
35
+ /** 같은 청크 안에서는 조회 범위를 유지해 작은 스크롤마다 항목을 다시 조회하지 않는다. */
36
+ declare function getVirtualViewportRange({ offset, viewportSize, contentSize, chunkSize, overscanChunks, padding, }: VirtualViewportOptions): Interval | null;
37
+
38
+ export { type Interval, type IntervalIndex, type IntervalQueryRange, type IntervalQueryResult, type VirtualViewportOptions, createIntervalIndex, getVirtualViewportRange, queryIntervals };
package/dist/index.js ADDED
@@ -0,0 +1,107 @@
1
+ // src/interval-index.ts
2
+ function findFirstIntervalStartingAfter(sortedIntervals, target) {
3
+ let low = 0;
4
+ let high = sortedIntervals.length;
5
+ while (low < high) {
6
+ const middle = low + Math.floor((high - low) / 2);
7
+ if (sortedIntervals[middle].start <= target) {
8
+ low = middle + 1;
9
+ } else {
10
+ high = middle;
11
+ }
12
+ }
13
+ return low;
14
+ }
15
+ function createMaxEndTree(sortedIntervals) {
16
+ let leafOffset = 1;
17
+ while (leafOffset < sortedIntervals.length) {
18
+ leafOffset *= 2;
19
+ }
20
+ const maxEndTree = Array(leafOffset * 2).fill(Number.NEGATIVE_INFINITY);
21
+ for (let index = 0; index < sortedIntervals.length; index += 1) {
22
+ maxEndTree[leafOffset + index] = sortedIntervals[index].end;
23
+ }
24
+ for (let nodeIndex = leafOffset - 1; nodeIndex > 0; nodeIndex -= 1) {
25
+ maxEndTree[nodeIndex] = Math.max(maxEndTree[nodeIndex * 2], maxEndTree[nodeIndex * 2 + 1]);
26
+ }
27
+ return { maxEndTree, leafOffset };
28
+ }
29
+ function createIntervalIndex(intervals) {
30
+ const sortedIntervals = intervals.map((interval, originalIndex) => ({
31
+ interval,
32
+ start: interval.start,
33
+ end: interval.end,
34
+ originalIndex
35
+ })).filter(
36
+ (indexedInterval) => Number.isFinite(indexedInterval.start) && Number.isFinite(indexedInterval.end) && indexedInterval.end >= indexedInterval.start
37
+ ).sort((left, right) => left.start - right.start || left.originalIndex - right.originalIndex);
38
+ const { maxEndTree, leafOffset } = createMaxEndTree(sortedIntervals);
39
+ return { sortedIntervals, maxEndTree, leafOffset };
40
+ }
41
+ function queryIntervals(index, range) {
42
+ if (!Number.isFinite(range.start) || !Number.isFinite(range.end) || range.end < range.start) {
43
+ return { intervals: [], inspectedIntervalCount: 0 };
44
+ }
45
+ const candidateEndIndex = findFirstIntervalStartingAfter(index.sortedIntervals, range.end);
46
+ if (candidateEndIndex === 0) {
47
+ return { intervals: [], inspectedIntervalCount: 0 };
48
+ }
49
+ const visibleIntervals = [];
50
+ const pendingNodes = [{ nodeIndex: 1, intervalStart: 0, intervalEnd: index.leafOffset }];
51
+ let inspectedIntervalCount = 0;
52
+ while (pendingNodes.length > 0) {
53
+ const currentNode = pendingNodes.pop();
54
+ if (!currentNode || currentNode.intervalStart >= candidateEndIndex || index.maxEndTree[currentNode.nodeIndex] < range.start) {
55
+ continue;
56
+ }
57
+ if (currentNode.intervalEnd - currentNode.intervalStart > 1) {
58
+ const middle = currentNode.intervalStart + Math.floor((currentNode.intervalEnd - currentNode.intervalStart) / 2);
59
+ pendingNodes.push(
60
+ { nodeIndex: currentNode.nodeIndex * 2 + 1, intervalStart: middle, intervalEnd: currentNode.intervalEnd },
61
+ { nodeIndex: currentNode.nodeIndex * 2, intervalStart: currentNode.intervalStart, intervalEnd: middle }
62
+ );
63
+ continue;
64
+ }
65
+ const indexedInterval = index.sortedIntervals[currentNode.intervalStart];
66
+ if (!indexedInterval) {
67
+ continue;
68
+ }
69
+ inspectedIntervalCount += 1;
70
+ if (indexedInterval.end >= range.start) {
71
+ visibleIntervals.push(indexedInterval);
72
+ }
73
+ }
74
+ visibleIntervals.sort((left, right) => left.originalIndex - right.originalIndex);
75
+ return {
76
+ intervals: visibleIntervals.map((indexedInterval) => indexedInterval.interval),
77
+ inspectedIntervalCount
78
+ };
79
+ }
80
+
81
+ // src/viewport-range.ts
82
+ function getVirtualViewportRange({
83
+ offset,
84
+ viewportSize,
85
+ contentSize,
86
+ chunkSize,
87
+ overscanChunks = 0,
88
+ padding = 0
89
+ }) {
90
+ if (![offset, viewportSize, contentSize, chunkSize, overscanChunks, padding].every(Number.isFinite) || viewportSize <= 0 || contentSize <= 0 || chunkSize <= 0 || overscanChunks < 0 || padding < 0) {
91
+ return null;
92
+ }
93
+ const clampedOffset = Math.max(0, Math.min(offset, contentSize - viewportSize));
94
+ const overscan = Math.floor(overscanChunks);
95
+ const firstChunk = Math.max(0, Math.floor(clampedOffset / chunkSize) - overscan);
96
+ const endChunk = Math.ceil((clampedOffset + viewportSize) / chunkSize) + overscan;
97
+ return {
98
+ start: Math.max(0, firstChunk * chunkSize - padding),
99
+ end: Math.min(contentSize, endChunk * chunkSize + padding)
100
+ };
101
+ }
102
+ export {
103
+ createIntervalIndex,
104
+ getVirtualViewportRange,
105
+ queryIntervals
106
+ };
107
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/interval-index.ts","../src/viewport-range.ts"],"sourcesContent":["export interface Interval {\n start: number;\n end: number;\n}\n\ninterface IndexedInterval<TInterval extends Interval> {\n interval: TInterval;\n start: number;\n end: number;\n originalIndex: number;\n}\n\nexport interface IntervalIndex<TInterval extends Interval> {\n readonly sortedIntervals: ReadonlyArray<IndexedInterval<TInterval>>;\n readonly maxEndTree: readonly number[];\n readonly leafOffset: number;\n}\n\nexport interface IntervalQueryRange {\n start: number;\n end: number;\n}\n\nexport interface IntervalQueryResult<TInterval extends Interval> {\n intervals: TInterval[];\n inspectedIntervalCount: number;\n}\n\nfunction findFirstIntervalStartingAfter<TInterval extends Interval>(\n sortedIntervals: ReadonlyArray<IndexedInterval<TInterval>>,\n target: number,\n): number {\n let low = 0;\n let high = sortedIntervals.length;\n\n while (low < high) {\n const middle = low + Math.floor((high - low) / 2);\n if (sortedIntervals[middle].start <= target) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n\n return low;\n}\n\nfunction createMaxEndTree<TInterval extends Interval>(\n sortedIntervals: ReadonlyArray<IndexedInterval<TInterval>>,\n): {\n maxEndTree: number[];\n leafOffset: number;\n} {\n let leafOffset = 1;\n while (leafOffset < sortedIntervals.length) {\n leafOffset *= 2;\n }\n\n const maxEndTree = Array<number>(leafOffset * 2).fill(Number.NEGATIVE_INFINITY);\n for (let index = 0; index < sortedIntervals.length; index += 1) {\n maxEndTree[leafOffset + index] = sortedIntervals[index].end;\n }\n\n for (let nodeIndex = leafOffset - 1; nodeIndex > 0; nodeIndex -= 1) {\n maxEndTree[nodeIndex] = Math.max(maxEndTree[nodeIndex * 2], maxEndTree[nodeIndex * 2 + 1]);\n }\n\n return { maxEndTree, leafOffset };\n}\n\nexport function createIntervalIndex<TInterval extends Interval>(\n intervals: readonly TInterval[],\n): IntervalIndex<TInterval> {\n const sortedIntervals = intervals\n .map((interval, originalIndex) => ({\n interval,\n start: interval.start,\n end: interval.end,\n originalIndex,\n }))\n .filter(\n (indexedInterval) =>\n Number.isFinite(indexedInterval.start) &&\n Number.isFinite(indexedInterval.end) &&\n indexedInterval.end >= indexedInterval.start,\n )\n .sort((left, right) => left.start - right.start || left.originalIndex - right.originalIndex);\n\n // 하위 트리별 최대 종료 시간을 저장해 긴 구간 하나가 앞쪽의 짧은 구간 전체를 후보로 만들지 않게 한다.\n const { maxEndTree, leafOffset } = createMaxEndTree(sortedIntervals);\n return { sortedIntervals, maxEndTree, leafOffset };\n}\n\nexport function queryIntervals<TInterval extends Interval>(\n index: IntervalIndex<TInterval>,\n range: IntervalQueryRange,\n): IntervalQueryResult<TInterval> {\n if (!Number.isFinite(range.start) || !Number.isFinite(range.end) || range.end < range.start) {\n return { intervals: [], inspectedIntervalCount: 0 };\n }\n\n const candidateEndIndex = findFirstIntervalStartingAfter(index.sortedIntervals, range.end);\n if (candidateEndIndex === 0) {\n return { intervals: [], inspectedIntervalCount: 0 };\n }\n\n const visibleIntervals: Array<IndexedInterval<TInterval>> = [];\n const pendingNodes = [{ nodeIndex: 1, intervalStart: 0, intervalEnd: index.leafOffset }];\n let inspectedIntervalCount = 0;\n\n while (pendingNodes.length > 0) {\n const currentNode = pendingNodes.pop();\n if (\n !currentNode ||\n currentNode.intervalStart >= candidateEndIndex ||\n index.maxEndTree[currentNode.nodeIndex] < range.start\n ) {\n continue;\n }\n\n if (currentNode.intervalEnd - currentNode.intervalStart > 1) {\n const middle = currentNode.intervalStart + Math.floor((currentNode.intervalEnd - currentNode.intervalStart) / 2);\n pendingNodes.push(\n { nodeIndex: currentNode.nodeIndex * 2 + 1, intervalStart: middle, intervalEnd: currentNode.intervalEnd },\n { nodeIndex: currentNode.nodeIndex * 2, intervalStart: currentNode.intervalStart, intervalEnd: middle },\n );\n continue;\n }\n\n const indexedInterval = index.sortedIntervals[currentNode.intervalStart];\n if (!indexedInterval) {\n continue;\n }\n\n inspectedIntervalCount += 1;\n if (indexedInterval.end >= range.start) {\n visibleIntervals.push(indexedInterval);\n }\n }\n\n visibleIntervals.sort((left, right) => left.originalIndex - right.originalIndex);\n\n return {\n intervals: visibleIntervals.map((indexedInterval) => indexedInterval.interval),\n inspectedIntervalCount,\n };\n}\n","import type { Interval } from './interval-index';\n\nexport interface VirtualViewportOptions {\n offset: number;\n viewportSize: number;\n contentSize: number;\n chunkSize: number;\n overscanChunks?: number;\n padding?: number;\n}\n\n/** 같은 청크 안에서는 조회 범위를 유지해 작은 스크롤마다 항목을 다시 조회하지 않는다. */\nexport function getVirtualViewportRange({\n offset,\n viewportSize,\n contentSize,\n chunkSize,\n overscanChunks = 0,\n padding = 0,\n}: VirtualViewportOptions): Interval | null {\n if (\n ![offset, viewportSize, contentSize, chunkSize, overscanChunks, padding].every(Number.isFinite) ||\n viewportSize <= 0 ||\n contentSize <= 0 ||\n chunkSize <= 0 ||\n overscanChunks < 0 ||\n padding < 0\n ) {\n return null;\n }\n\n const clampedOffset = Math.max(0, Math.min(offset, contentSize - viewportSize));\n const overscan = Math.floor(overscanChunks);\n const firstChunk = Math.max(0, Math.floor(clampedOffset / chunkSize) - overscan);\n const endChunk = Math.ceil((clampedOffset + viewportSize) / chunkSize) + overscan;\n\n return {\n start: Math.max(0, firstChunk * chunkSize - padding),\n end: Math.min(contentSize, endChunk * chunkSize + padding),\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/react/index.ts
21
+ var react_exports = {};
22
+ __export(react_exports, {
23
+ useVirtualViewportRange: () => useVirtualViewportRange
24
+ });
25
+ module.exports = __toCommonJS(react_exports);
26
+
27
+ // src/react/use-virtual-viewport-range.ts
28
+ var import_react = require("react");
29
+
30
+ // src/viewport-range.ts
31
+ function getVirtualViewportRange({
32
+ offset,
33
+ viewportSize,
34
+ contentSize,
35
+ chunkSize,
36
+ overscanChunks = 0,
37
+ padding = 0
38
+ }) {
39
+ if (![offset, viewportSize, contentSize, chunkSize, overscanChunks, padding].every(Number.isFinite) || viewportSize <= 0 || contentSize <= 0 || chunkSize <= 0 || overscanChunks < 0 || padding < 0) {
40
+ return null;
41
+ }
42
+ const clampedOffset = Math.max(0, Math.min(offset, contentSize - viewportSize));
43
+ const overscan = Math.floor(overscanChunks);
44
+ const firstChunk = Math.max(0, Math.floor(clampedOffset / chunkSize) - overscan);
45
+ const endChunk = Math.ceil((clampedOffset + viewportSize) / chunkSize) + overscan;
46
+ return {
47
+ start: Math.max(0, firstChunk * chunkSize - padding),
48
+ end: Math.min(contentSize, endChunk * chunkSize + padding)
49
+ };
50
+ }
51
+
52
+ // src/react/use-virtual-viewport-range.ts
53
+ function createViewportSnapshotReader({
54
+ offsetRef,
55
+ ...options
56
+ }) {
57
+ let snapshot = null;
58
+ return () => {
59
+ const next = getVirtualViewportRange({ ...options, offset: offsetRef?.current ?? 0 });
60
+ if (snapshot?.start !== next?.start || snapshot?.end !== next?.end) {
61
+ snapshot = next;
62
+ }
63
+ return snapshot;
64
+ };
65
+ }
66
+ function useVirtualViewportRange({
67
+ offsetRef,
68
+ subscribe,
69
+ viewportSize,
70
+ contentSize,
71
+ chunkSize,
72
+ overscanChunks = 0,
73
+ padding = 0
74
+ }) {
75
+ const getSnapshot = (0, import_react.useMemo)(
76
+ () => createViewportSnapshotReader({ offsetRef, viewportSize, contentSize, chunkSize, overscanChunks, padding }),
77
+ [offsetRef, viewportSize, contentSize, chunkSize, overscanChunks, padding]
78
+ );
79
+ return (0, import_react.useSyncExternalStore)(subscribe, getSnapshot);
80
+ }
81
+ // Annotate the CommonJS export names for ESM import in node:
82
+ 0 && (module.exports = {
83
+ useVirtualViewportRange
84
+ });
85
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/index.ts","../../src/react/use-virtual-viewport-range.ts","../../src/viewport-range.ts"],"sourcesContent":["export { useVirtualViewportRange } from './use-virtual-viewport-range';\nexport type { VirtualViewportSubscriptionOptions } from './use-virtual-viewport-range';\n","import { useMemo, useSyncExternalStore, type RefObject } from 'react';\nimport { getVirtualViewportRange, type Interval, type VirtualViewportOptions } from '../index';\n\nexport interface VirtualViewportSubscriptionOptions extends Omit<VirtualViewportOptions, 'offset'> {\n offsetRef?: RefObject<number | null>;\n subscribe: (listener: () => void) => () => void;\n}\n\nfunction createViewportSnapshotReader({\n offsetRef,\n ...options\n}: Omit<VirtualViewportSubscriptionOptions, 'subscribe'>): () => Interval | null {\n let snapshot: Interval | null = null;\n\n return () => {\n const next = getVirtualViewportRange({ ...options, offset: offsetRef?.current ?? 0 });\n\n // useSyncExternalStore는 같은 범위의 스크롤 신호에 동일한 객체를 반환해야 렌더링을 건너뛴다.\n if (snapshot?.start !== next?.start || snapshot?.end !== next?.end) {\n snapshot = next;\n }\n return snapshot;\n };\n}\n\nexport function useVirtualViewportRange({\n offsetRef,\n subscribe,\n viewportSize,\n contentSize,\n chunkSize,\n overscanChunks = 0,\n padding = 0,\n}: VirtualViewportSubscriptionOptions): Interval | null {\n const getSnapshot = useMemo(\n () => createViewportSnapshotReader({ offsetRef, viewportSize, contentSize, chunkSize, overscanChunks, padding }),\n [offsetRef, viewportSize, contentSize, chunkSize, overscanChunks, padding],\n );\n\n return useSyncExternalStore(subscribe, getSnapshot);\n}\n","import type { Interval } from './interval-index';\n\nexport interface VirtualViewportOptions {\n offset: number;\n viewportSize: number;\n contentSize: number;\n chunkSize: number;\n overscanChunks?: number;\n padding?: number;\n}\n\n/** 같은 청크 안에서는 조회 범위를 유지해 작은 스크롤마다 항목을 다시 조회하지 않는다. */\nexport function getVirtualViewportRange({\n offset,\n viewportSize,\n contentSize,\n chunkSize,\n overscanChunks = 0,\n padding = 0,\n}: VirtualViewportOptions): Interval | null {\n if (\n ![offset, viewportSize, contentSize, chunkSize, overscanChunks, padding].every(Number.isFinite) ||\n viewportSize <= 0 ||\n contentSize <= 0 ||\n chunkSize <= 0 ||\n overscanChunks < 0 ||\n padding < 0\n ) {\n return null;\n }\n\n const clampedOffset = Math.max(0, Math.min(offset, contentSize - viewportSize));\n const overscan = Math.floor(overscanChunks);\n const firstChunk = Math.max(0, Math.floor(clampedOffset / chunkSize) - overscan);\n const endChunk = Math.ceil((clampedOffset + viewportSize) / chunkSize) + overscan;\n\n return {\n start: Math.max(0, firstChunk * chunkSize - padding),\n end: Math.min(contentSize, endChunk * chunkSize + padding),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAA8D;;;ACYvD,SAAS,wBAAwB;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,iBAAiB;AAAA,EACjB,UAAU;AACZ,GAA4C;AAC1C,MACE,CAAC,CAAC,QAAQ,cAAc,aAAa,WAAW,gBAAgB,OAAO,EAAE,MAAM,OAAO,QAAQ,KAC9F,gBAAgB,KAChB,eAAe,KACf,aAAa,KACb,iBAAiB,KACjB,UAAU,GACV;AACA,WAAO;AAAA,EACT;AAEA,QAAM,gBAAgB,KAAK,IAAI,GAAG,KAAK,IAAI,QAAQ,cAAc,YAAY,CAAC;AAC9E,QAAM,WAAW,KAAK,MAAM,cAAc;AAC1C,QAAM,aAAa,KAAK,IAAI,GAAG,KAAK,MAAM,gBAAgB,SAAS,IAAI,QAAQ;AAC/E,QAAM,WAAW,KAAK,MAAM,gBAAgB,gBAAgB,SAAS,IAAI;AAEzE,SAAO;AAAA,IACL,OAAO,KAAK,IAAI,GAAG,aAAa,YAAY,OAAO;AAAA,IACnD,KAAK,KAAK,IAAI,aAAa,WAAW,YAAY,OAAO;AAAA,EAC3D;AACF;;;ADhCA,SAAS,6BAA6B;AAAA,EACpC;AAAA,EACA,GAAG;AACL,GAAiF;AAC/E,MAAI,WAA4B;AAEhC,SAAO,MAAM;AACX,UAAM,OAAO,wBAAwB,EAAE,GAAG,SAAS,QAAQ,WAAW,WAAW,EAAE,CAAC;AAGpF,QAAI,UAAU,UAAU,MAAM,SAAS,UAAU,QAAQ,MAAM,KAAK;AAClE,iBAAW;AAAA,IACb;AACA,WAAO;AAAA,EACT;AACF;AAEO,SAAS,wBAAwB;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,iBAAiB;AAAA,EACjB,UAAU;AACZ,GAAwD;AACtD,QAAM,kBAAc;AAAA,IAClB,MAAM,6BAA6B,EAAE,WAAW,cAAc,aAAa,WAAW,gBAAgB,QAAQ,CAAC;AAAA,IAC/G,CAAC,WAAW,cAAc,aAAa,WAAW,gBAAgB,OAAO;AAAA,EAC3E;AAEA,aAAO,mCAAqB,WAAW,WAAW;AACpD;","names":[]}
@@ -0,0 +1,10 @@
1
+ import { RefObject } from 'react';
2
+ import { VirtualViewportOptions, Interval } from '../index.cjs';
3
+
4
+ interface VirtualViewportSubscriptionOptions extends Omit<VirtualViewportOptions, 'offset'> {
5
+ offsetRef?: RefObject<number | null>;
6
+ subscribe: (listener: () => void) => () => void;
7
+ }
8
+ declare function useVirtualViewportRange({ offsetRef, subscribe, viewportSize, contentSize, chunkSize, overscanChunks, padding, }: VirtualViewportSubscriptionOptions): Interval | null;
9
+
10
+ export { type VirtualViewportSubscriptionOptions, useVirtualViewportRange };
@@ -0,0 +1,10 @@
1
+ import { RefObject } from 'react';
2
+ import { VirtualViewportOptions, Interval } from '../index.js';
3
+
4
+ interface VirtualViewportSubscriptionOptions extends Omit<VirtualViewportOptions, 'offset'> {
5
+ offsetRef?: RefObject<number | null>;
6
+ subscribe: (listener: () => void) => () => void;
7
+ }
8
+ declare function useVirtualViewportRange({ offsetRef, subscribe, viewportSize, contentSize, chunkSize, overscanChunks, padding, }: VirtualViewportSubscriptionOptions): Interval | null;
9
+
10
+ export { type VirtualViewportSubscriptionOptions, useVirtualViewportRange };
@@ -0,0 +1,58 @@
1
+ // src/react/use-virtual-viewport-range.ts
2
+ import { useMemo, useSyncExternalStore } from "react";
3
+
4
+ // src/viewport-range.ts
5
+ function getVirtualViewportRange({
6
+ offset,
7
+ viewportSize,
8
+ contentSize,
9
+ chunkSize,
10
+ overscanChunks = 0,
11
+ padding = 0
12
+ }) {
13
+ if (![offset, viewportSize, contentSize, chunkSize, overscanChunks, padding].every(Number.isFinite) || viewportSize <= 0 || contentSize <= 0 || chunkSize <= 0 || overscanChunks < 0 || padding < 0) {
14
+ return null;
15
+ }
16
+ const clampedOffset = Math.max(0, Math.min(offset, contentSize - viewportSize));
17
+ const overscan = Math.floor(overscanChunks);
18
+ const firstChunk = Math.max(0, Math.floor(clampedOffset / chunkSize) - overscan);
19
+ const endChunk = Math.ceil((clampedOffset + viewportSize) / chunkSize) + overscan;
20
+ return {
21
+ start: Math.max(0, firstChunk * chunkSize - padding),
22
+ end: Math.min(contentSize, endChunk * chunkSize + padding)
23
+ };
24
+ }
25
+
26
+ // src/react/use-virtual-viewport-range.ts
27
+ function createViewportSnapshotReader({
28
+ offsetRef,
29
+ ...options
30
+ }) {
31
+ let snapshot = null;
32
+ return () => {
33
+ const next = getVirtualViewportRange({ ...options, offset: offsetRef?.current ?? 0 });
34
+ if (snapshot?.start !== next?.start || snapshot?.end !== next?.end) {
35
+ snapshot = next;
36
+ }
37
+ return snapshot;
38
+ };
39
+ }
40
+ function useVirtualViewportRange({
41
+ offsetRef,
42
+ subscribe,
43
+ viewportSize,
44
+ contentSize,
45
+ chunkSize,
46
+ overscanChunks = 0,
47
+ padding = 0
48
+ }) {
49
+ const getSnapshot = useMemo(
50
+ () => createViewportSnapshotReader({ offsetRef, viewportSize, contentSize, chunkSize, overscanChunks, padding }),
51
+ [offsetRef, viewportSize, contentSize, chunkSize, overscanChunks, padding]
52
+ );
53
+ return useSyncExternalStore(subscribe, getSnapshot);
54
+ }
55
+ export {
56
+ useVirtualViewportRange
57
+ };
58
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/react/use-virtual-viewport-range.ts","../../src/viewport-range.ts"],"sourcesContent":["import { useMemo, useSyncExternalStore, type RefObject } from 'react';\nimport { getVirtualViewportRange, type Interval, type VirtualViewportOptions } from '../index';\n\nexport interface VirtualViewportSubscriptionOptions extends Omit<VirtualViewportOptions, 'offset'> {\n offsetRef?: RefObject<number | null>;\n subscribe: (listener: () => void) => () => void;\n}\n\nfunction createViewportSnapshotReader({\n offsetRef,\n ...options\n}: Omit<VirtualViewportSubscriptionOptions, 'subscribe'>): () => Interval | null {\n let snapshot: Interval | null = null;\n\n return () => {\n const next = getVirtualViewportRange({ ...options, offset: offsetRef?.current ?? 0 });\n\n // useSyncExternalStore는 같은 범위의 스크롤 신호에 동일한 객체를 반환해야 렌더링을 건너뛴다.\n if (snapshot?.start !== next?.start || snapshot?.end !== next?.end) {\n snapshot = next;\n }\n return snapshot;\n };\n}\n\nexport function useVirtualViewportRange({\n offsetRef,\n subscribe,\n viewportSize,\n contentSize,\n chunkSize,\n overscanChunks = 0,\n padding = 0,\n}: VirtualViewportSubscriptionOptions): Interval | null {\n const getSnapshot = useMemo(\n () => createViewportSnapshotReader({ offsetRef, viewportSize, contentSize, chunkSize, overscanChunks, padding }),\n [offsetRef, viewportSize, contentSize, chunkSize, overscanChunks, padding],\n );\n\n return useSyncExternalStore(subscribe, getSnapshot);\n}\n","import type { Interval } from './interval-index';\n\nexport interface VirtualViewportOptions {\n offset: number;\n viewportSize: number;\n contentSize: number;\n chunkSize: number;\n overscanChunks?: number;\n padding?: number;\n}\n\n/** 같은 청크 안에서는 조회 범위를 유지해 작은 스크롤마다 항목을 다시 조회하지 않는다. */\nexport function getVirtualViewportRange({\n offset,\n viewportSize,\n contentSize,\n chunkSize,\n overscanChunks = 0,\n padding = 0,\n}: VirtualViewportOptions): Interval | null {\n if (\n ![offset, viewportSize, contentSize, chunkSize, overscanChunks, padding].every(Number.isFinite) ||\n viewportSize <= 0 ||\n contentSize <= 0 ||\n chunkSize <= 0 ||\n overscanChunks < 0 ||\n padding < 0\n ) {\n return null;\n }\n\n const clampedOffset = Math.max(0, Math.min(offset, contentSize - viewportSize));\n const overscan = Math.floor(overscanChunks);\n const firstChunk = Math.max(0, Math.floor(clampedOffset / chunkSize) - overscan);\n const endChunk = Math.ceil((clampedOffset + viewportSize) / chunkSize) + overscan;\n\n return {\n start: Math.max(0, firstChunk * chunkSize - padding),\n end: Math.min(contentSize, endChunk * chunkSize + padding),\n };\n}\n"],"mappings":";AAAA,SAAS,SAAS,4BAA4C;;;ACYvD,SAAS,wBAAwB;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,iBAAiB;AAAA,EACjB,UAAU;AACZ,GAA4C;AAC1C,MACE,CAAC,CAAC,QAAQ,cAAc,aAAa,WAAW,gBAAgB,OAAO,EAAE,MAAM,OAAO,QAAQ,KAC9F,gBAAgB,KAChB,eAAe,KACf,aAAa,KACb,iBAAiB,KACjB,UAAU,GACV;AACA,WAAO;AAAA,EACT;AAEA,QAAM,gBAAgB,KAAK,IAAI,GAAG,KAAK,IAAI,QAAQ,cAAc,YAAY,CAAC;AAC9E,QAAM,WAAW,KAAK,MAAM,cAAc;AAC1C,QAAM,aAAa,KAAK,IAAI,GAAG,KAAK,MAAM,gBAAgB,SAAS,IAAI,QAAQ;AAC/E,QAAM,WAAW,KAAK,MAAM,gBAAgB,gBAAgB,SAAS,IAAI;AAEzE,SAAO;AAAA,IACL,OAAO,KAAK,IAAI,GAAG,aAAa,YAAY,OAAO;AAAA,IACnD,KAAK,KAAK,IAAI,aAAa,WAAW,YAAY,OAAO;AAAA,EAC3D;AACF;;;ADhCA,SAAS,6BAA6B;AAAA,EACpC;AAAA,EACA,GAAG;AACL,GAAiF;AAC/E,MAAI,WAA4B;AAEhC,SAAO,MAAM;AACX,UAAM,OAAO,wBAAwB,EAAE,GAAG,SAAS,QAAQ,WAAW,WAAW,EAAE,CAAC;AAGpF,QAAI,UAAU,UAAU,MAAM,SAAS,UAAU,QAAQ,MAAM,KAAK;AAClE,iBAAW;AAAA,IACb;AACA,WAAO;AAAA,EACT;AACF;AAEO,SAAS,wBAAwB;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,iBAAiB;AAAA,EACjB,UAAU;AACZ,GAAwD;AACtD,QAAM,cAAc;AAAA,IAClB,MAAM,6BAA6B,EAAE,WAAW,cAAc,aAAa,WAAW,gBAAgB,QAAQ,CAAC;AAAA,IAC/G,CAAC,WAAW,cAAc,aAAa,WAAW,gBAAgB,OAAO;AAAA,EAC3E;AAEA,SAAO,qBAAqB,WAAW,WAAW;AACpD;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,96 @@
1
1
  {
2
2
  "name": "@hurraey/interval-virtualizer",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "숫자 구간의 겹침 조회와 청크 기반 화면 범위 계산을 제공하는 TypeScript 라이브러리",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "require": {
17
+ "types": "./dist/index.d.cts",
18
+ "default": "./dist/index.cjs"
19
+ }
20
+ },
21
+ "./react": {
22
+ "import": {
23
+ "types": "./dist/react/index.d.ts",
24
+ "default": "./dist/react/index.js"
25
+ },
26
+ "require": {
27
+ "types": "./dist/react/index.d.cts",
28
+ "default": "./dist/react/index.cjs"
29
+ }
30
+ },
31
+ "./package.json": "./package.json"
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "scripts": {
39
+ "build": "tsup",
40
+ "test": "vitest run",
41
+ "test:package": "node scripts/verify-package.mjs",
42
+ "typecheck": "tsc --noEmit",
43
+ "format": "prettier --write .",
44
+ "format:check": "prettier --check .",
45
+ "check": "npm run format:check && npm run typecheck && npm test && npm run build && npm run test:package",
46
+ "prepublishOnly": "npm run check"
47
+ },
48
+ "engines": {
49
+ "node": ">=20"
50
+ },
51
+ "peerDependencies": {
52
+ "react": "^18.2.0 || ^19.0.0"
53
+ },
54
+ "peerDependenciesMeta": {
55
+ "react": {
56
+ "optional": true
57
+ }
58
+ },
59
+ "publishConfig": {
60
+ "access": "public",
61
+ "registry": "https://registry.npmjs.org/"
62
+ },
63
+ "author": {
64
+ "name": "HURRAEY"
65
+ },
66
+ "repository": {
67
+ "type": "git",
68
+ "url": "git+https://github.com/HURRAEY/interval-virtualizer.git"
69
+ },
70
+ "homepage": "https://github.com/HURRAEY/interval-virtualizer#readme",
71
+ "bugs": {
72
+ "url": "https://github.com/HURRAEY/interval-virtualizer/issues"
73
+ },
74
+ "keywords": [
75
+ "virtualization",
76
+ "virtual-scroll",
77
+ "interval",
78
+ "segment-tree",
79
+ "timeline",
80
+ "typescript",
81
+ "react"
82
+ ],
83
+ "license": "MIT",
84
+ "devDependencies": {
85
+ "@testing-library/react": "16.3.0",
86
+ "@types/react": "^19.2.3",
87
+ "@types/react-dom": "^19.2.3",
88
+ "jsdom": "26.1.0",
89
+ "prettier": "3.8.3",
90
+ "react": "19.2.0",
91
+ "react-dom": "19.2.0",
92
+ "tsup": "8.5.1",
93
+ "typescript": "5.9.3",
94
+ "vitest": "3.2.6"
95
+ }
96
+ }