PageSourceSearch

https://fortune-cookie.tokyo/assets/js/countdown-sync-clock.js?v=1.0.610

js fortune-cookie.tokyo collected 2026-10-07 17:57:57 UTC 8,534 bytes, 235 lines download raw bytes

1/**
2 * CountdownSyncClock
3 *
4 * カウントダウン系の表示が「秒境界で同期して見える」ようにするための単一クロック。
5 * setInterval / requestAnimationFrame を要素ごとに張ると位相がバラつき、CPU も無駄に使うため、
6 * サイト全体で共有するスケジューラ 3 種を提供する。
7 *
8 * Usage:
9 *   var fn = window.CountdownSyncClock.subscribe(function (nowMs) { ... });       // 200ms 周期(既存)
10 *   window.CountdownSyncClock.subscribeSecond(function (nowMs) { ... });          // 1 秒周期(秒境界 +15ms)
11 *   window.CountdownSyncClock.subscribeFrame(function (nowMs) { ... });           // 毎フレーム(共有 rAF 1 本)
12 *   window.CountdownSyncClock.unsubscribe(fn);                                    // どの種類からも外す
13 *
14 * 契約(spec: docs/features/mobile-perf-js-loops/spec.md §6.2):
15 *   - subscribe / subscribeSecond は購読時に即 1 回 fn(Date.now()) を呼び、fn を返す。
16 *     subscribeFrame は即時呼び出しをしない(次のフレームで呼ぶ)
17 *   - 同じ fn を同じ種類に二重購読しない(既に購読済みなら何もしない)
18 *   - 各 tick は購読者配列のコピーを回す。tick 中・即時呼び出し中の unsubscribe / 種類の切替は安全
19 *   - 購読者が 0 の種類はタイマー / rAF を張らない
20 *   - 非表示タブ(document.hidden)では 3 種とも止め、表示に戻ったら(pageshow の persisted 復帰を含む)
21 *     全購読者を 1 回即時に呼んでから再開する。非表示のまま読み込まれたページは表示されるまで張らない
22 *   - 開始 / 再開は冪等(既に張られていれば何もしない)
23 *   - 購読者の例外は握りつぶす(1 人の例外で他を止めない)
24 *
25 * 注意:
26 *   - subscriber は数 ms 以内で済む軽い処理に保つ(重い処理は他の subscriber を遅延させる)
27 *   - 購読者は絶対時刻から毎回再計算する作りにすること(非表示中は呼ばれない)
28 */
29(function () {
30    if (window.CountdownSyncClock) return;
31
32    var TICK_MS = 200; // 5Hz
33    var SECOND_SLACK_MS = 15; // 秒境界の直後に発火させるための余裕
34
35    var tickSubs = [];
36    var secondSubs = [];
37    var frameSubs = [];
38
39    var intervalId = null;
40    var secondTimerId = null;
41    var secondExpected = null; // 次に呼ぶべき秒(Math.floor(ms / 1000))
42    var rafId = null;
43
44    var raf = typeof window.requestAnimationFrame === 'function'
45        ? function (cb) { return window.requestAnimationFrame(cb); }
46        : function (cb) { return setTimeout(cb, 16); };
47    var cancelRaf = typeof window.cancelAnimationFrame === 'function'
48        ? function (id) { window.cancelAnimationFrame(id); }
49        : function (id) { clearTimeout(id); };
50
51    function isHidden() {
52        return document.hidden === true;
53    }
54
55    function isSubscribed(fn) {
56        return tickSubs.indexOf(fn) !== -1
57            || secondSubs.indexOf(fn) !== -1
58            || frameSubs.indexOf(fn) !== -1;
59    }
60
61    function safeCall(fn, now) {
62        try { fn(now); } catch (e) { /* swallow */ }
63    }
64
65    // 購読者配列のコピーを回す。途中で外れた購読者は呼ばない(外れた人のせいで他を飛ばさない)
66    function callList(list, now) {
67        var copy = list.slice();
68        for (var i = 0; i < copy.length; i++) {
69            if (list.indexOf(copy[i]) === -1) continue;
70            safeCall(copy[i], now);
71        }
72    }
73
74    // ---- 200ms ----
75    function tick() {
76        callList(tickSubs, Date.now());
77    }
78
79    function ensureTick() {
80        if (intervalId !== null || tickSubs.length === 0 || isHidden()) return;
81        intervalId = setInterval(tick, TICK_MS);
82    }
83
84    function stopTick() {
85        if (intervalId === null) return;
86        clearInterval(intervalId);
87        intervalId = null;
88    }
89
90    // ---- 1 秒(秒境界 +15ms) ----
91    function armSecond(now) {
92        secondTimerId = setTimeout(onSecond, 1000 - (now % 1000) + SECOND_SLACK_MS);
93    }
94
95    function ensureSecond() {
96        if (secondTimerId !== null || secondSubs.length === 0 || isHidden()) return;
97        var now = Date.now();
98        secondExpected = Math.floor(now / 1000) + 1;
99        armSecond(now);
100    }
101
102    function onSecond() {
103        secondTimerId = null;
104        var now = Date.now();
105        var sec = Math.floor(now / 1000);
106        try {
107            if (secondExpected !== null && sec < secondExpected && secondExpected - sec <= 1) {
108                // 境界より前に早く発火した: 購読者を呼ばずに次の境界へ張り直す(秒の重複を防ぐ)
109                // 差が 1 秒を超えるなら端末の時計が過去へ戻った(手動変更・NTP 補正)ので、
110                // 早発火とはみなさず購読者を呼び、finally で基準を取り直す(戻った秒数ぶん止まらない)
111                if (secondSubs.length > 0 && !isHidden()) armSecond(now);
112                return;
113            }
114            callList(secondSubs, now);
115        } finally {
116            // 張り直しは購読者の例外に関わらず必ず行う(次の tick を失わない)
117            if (secondTimerId === null && secondSubs.length > 0 && !isHidden()) {
118                var after = Date.now();
119                secondExpected = Math.floor(after / 1000) + 1;
120                armSecond(after);
121            }
122        }
123    }
124
125    function stopSecond() {
126        if (secondTimerId === null) return;
127        clearTimeout(secondTimerId);
128        secondTimerId = null;
129    }
130
131    // ---- 毎フレーム ----
132    function onFrame() {
133        rafId = null;
134        try {
135            callList(frameSubs, Date.now());
136        } finally {
137            ensureFrame();
138        }
139    }
140
141    function ensureFrame() {
142        if (rafId !== null || frameSubs.length === 0 || isHidden()) return;
143        rafId = raf(onFrame);
144    }
145
146    function stopFrame() {
147        if (rafId === null) return;
148        cancelRaf(rafId);
149        rafId = null;
150    }
151
152    function ensureAll() {
153        ensureTick();
154        ensureSecond();
155        ensureFrame();
156    }
157
158    function stopAll() {
159        stopTick();
160        stopSecond();
161        stopFrame();
162    }
163
164    // 表示復帰: 全購読者を 1 回即時に呼んでから再開する
165    function resume() {
166        if (isHidden()) return;
167        var all = [];
168        var lists = [tickSubs, secondSubs, frameSubs];
169        for (var l = 0; l < lists.length; l++) {
170            for (var i = 0; i < lists[l].length; i++) {
171                if (all.indexOf(lists[l][i]) === -1) all.push(lists[l][i]);
172            }
173        }
174        var now = Date.now();
175        for (var j = 0; j < all.length; j++) {
176            if (!isSubscribed(all[j])) continue;
177            safeCall(all[j], now);
178        }
179        ensureAll();
180    }
181
182    function add(list, fn) {
183        if (list.indexOf(fn) !== -1) return false;
184        list.push(fn);
185        return true;
186    }
187
188    function remove(list, fn) {
189        var i = list.indexOf(fn);
190        if (i > -1) list.splice(i, 1);
191    }
192
193    window.CountdownSyncClock = {
194        subscribe: function (fn) {
195            if (typeof fn !== 'function') return null;
196            if (!add(tickSubs, fn)) return fn;
197            safeCall(fn, Date.now());
198            ensureTick();
199            return fn;
200        },
201        subscribeSecond: function (fn) {
202            if (typeof fn !== 'function') return null;
203            if (!add(secondSubs, fn)) return fn;
204            safeCall(fn, Date.now());
205            ensureSecond();
206            return fn;
207        },
208        subscribeFrame: function (fn) {
209            if (typeof fn !== 'function') return null;
210            if (!add(frameSubs, fn)) return fn;
211            ensureFrame();
212            return fn;
213        },
214        unsubscribe: function (fn) {
215            remove(tickSubs, fn);
216            remove(secondSubs, fn);
217            remove(frameSubs, fn);
218            if (tickSubs.length === 0) stopTick();
219            if (secondSubs.length === 0) stopSecond();
220            if (frameSubs.length === 0) stopFrame();
221        }
222    };
223
224    document.addEventListener('visibilitychange', function () {
225        if (isHidden()) {
226            stopAll();
227        } else {
228            resume();
229        }
230    });
231
232    window.addEventListener('pageshow', function (e) {
233        if (e && e.persisted) resume();
234    });
235})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.