File size: 7,589 Bytes
979bf48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
/**
 * This class is used to detect when all cache reads for a given render are settled.
 * We do this to allow for cache warming the prerender without having to continue rendering
 * the remainder of the page. This feature is really only useful when the cacheComponents flag is on
 * and should only be used in codepaths gated with this feature.
 */ "use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
Object.defineProperty(exports, "CacheSignal", {
    enumerable: true,
    get: function() {
        return CacheSignal;
    }
});
const _invarianterror = require("../../shared/lib/invariant-error");
class CacheSignal {
    constructor(){
        this.count = 0;
        this.earlyListeners = [];
        this.listeners = [];
        this.tickPending = false;
        this.pendingTimeoutCleanup = null;
        this.subscribedSignals = null;
        this.invokeListenersIfNoPendingReads = ()=>{
            this.pendingTimeoutCleanup = null;
            if (this.count === 0) {
                for(let i = 0; i < this.listeners.length; i++){
                    this.listeners[i]();
                }
                this.listeners.length = 0;
            }
        };
        if (process.env.NEXT_RUNTIME === 'edge') {
            // we rely on `process.nextTick`, which is not supported in edge
            throw Object.defineProperty(new _invarianterror.InvariantError('CacheSignal cannot be used in the edge runtime, because `cacheComponents` does not support it.'), "__NEXT_ERROR_CODE", {
                value: "E728",
                enumerable: false,
                configurable: true
            });
        }
    }
    noMorePendingCaches() {
        if (!this.tickPending) {
            this.tickPending = true;
            queueMicrotask(()=>process.nextTick(()=>{
                    this.tickPending = false;
                    if (this.count === 0) {
                        for(let i = 0; i < this.earlyListeners.length; i++){
                            this.earlyListeners[i]();
                        }
                        this.earlyListeners.length = 0;
                    }
                }));
        }
        // After a cache resolves, React will schedule new rendering work:
        // - in a microtask (when prerendering)
        // - in setImmediate (when rendering)
        // To cover both of these, we have to make sure that we let immediates execute at least once after each cache resolved.
        // We don't know when the pending timeout was scheduled (and if it's about to resolve),
        // so by scheduling a new one, we can be sure that we'll go around the event loop at least once.
        if (this.pendingTimeoutCleanup) {
            // We cancel the timeout in beginRead, so this shouldn't ever be active here,
            // but we still cancel it defensively.
            this.pendingTimeoutCleanup();
        }
        this.pendingTimeoutCleanup = scheduleImmediateAndTimeoutWithCleanup(this.invokeListenersIfNoPendingReads);
    }
    /**
   * This promise waits until there are no more in progress cache reads but no later.
   * This allows for adding more cache reads after to delay cacheReady.
   */ inputReady() {
        return new Promise((resolve)=>{
            this.earlyListeners.push(resolve);
            if (this.count === 0) {
                this.noMorePendingCaches();
            }
        });
    }
    /**
   * If there are inflight cache reads this Promise can resolve in a microtask however
   * if there are no inflight cache reads then we wait at least one task to allow initial
   * cache reads to be initiated.
   */ cacheReady() {
        return new Promise((resolve)=>{
            this.listeners.push(resolve);
            if (this.count === 0) {
                this.noMorePendingCaches();
            }
        });
    }
    beginRead() {
        this.count++;
        // There's a new pending cache, so if there's a `noMorePendingCaches` timeout running,
        // we should cancel it.
        if (this.pendingTimeoutCleanup) {
            this.pendingTimeoutCleanup();
            this.pendingTimeoutCleanup = null;
        }
        if (this.subscribedSignals !== null) {
            for (const subscriber of this.subscribedSignals){
                subscriber.beginRead();
            }
        }
    }
    endRead() {
        if (this.count === 0) {
            throw Object.defineProperty(new _invarianterror.InvariantError('CacheSignal got more endRead() calls than beginRead() calls'), "__NEXT_ERROR_CODE", {
                value: "E678",
                enumerable: false,
                configurable: true
            });
        }
        // If this is the last read we need to wait a task before we can claim the cache is settled.
        // The cache read will likely ping a Server Component which can read from the cache again and this
        // will play out in a microtask so we need to only resolve pending listeners if we're still at 0
        // after at least one task.
        // We only want one task scheduled at a time so when we hit count 1 we don't decrement the counter immediately.
        // If intervening reads happen before the scheduled task runs they will never observe count 1 preventing reentrency
        this.count--;
        if (this.count === 0) {
            this.noMorePendingCaches();
        }
        if (this.subscribedSignals !== null) {
            for (const subscriber of this.subscribedSignals){
                subscriber.endRead();
            }
        }
    }
    hasPendingReads() {
        return this.count > 0;
    }
    trackRead(promise) {
        this.beginRead();
        // `promise.finally()` still rejects, so don't use it here to avoid unhandled rejections
        const onFinally = this.endRead.bind(this);
        promise.then(onFinally, onFinally);
        return promise;
    }
    subscribeToReads(subscriber) {
        if (subscriber === this) {
            throw Object.defineProperty(new _invarianterror.InvariantError('A CacheSignal cannot subscribe to itself'), "__NEXT_ERROR_CODE", {
                value: "E679",
                enumerable: false,
                configurable: true
            });
        }
        if (this.subscribedSignals === null) {
            this.subscribedSignals = new Set();
        }
        this.subscribedSignals.add(subscriber);
        // we'll notify the subscriber of each endRead() on this signal,
        // so we need to give it a corresponding beginRead() for each read we have in flight now.
        for(let i = 0; i < this.count; i++){
            subscriber.beginRead();
        }
        return this.unsubscribeFromReads.bind(this, subscriber);
    }
    unsubscribeFromReads(subscriber) {
        if (!this.subscribedSignals) {
            return;
        }
        this.subscribedSignals.delete(subscriber);
    // we don't need to set the set back to `null` if it's empty --
    // if other signals are subscribing to this one, it'll likely get more subscriptions later,
    // so we'd have to allocate a fresh set again when that happens.
    }
}
function scheduleImmediateAndTimeoutWithCleanup(cb) {
    // If we decide to clean up the timeout, we want to remove
    // either the immediate or the timeout, whichever is still pending.
    let clearPending;
    const immediate = setImmediate(()=>{
        const timeout = setTimeout(cb, 0);
        clearPending = clearTimeout.bind(null, timeout);
    });
    clearPending = clearImmediate.bind(null, immediate);
    return ()=>clearPending();
}

//# sourceMappingURL=cache-signal.js.map