|
1 | | -import type { Source, SourceMetadata } from "@chunkd/source"; |
| 1 | +import type { |
| 2 | + SourceCallback, |
| 3 | + SourceMiddleware, |
| 4 | + SourceRequest, |
| 5 | +} from "@chunkd/source"; |
2 | 6 |
|
3 | 7 | /** |
4 | 8 | * Numeric priority used to order waiters in a {@link Semaphore}'s queue. Lower |
@@ -63,7 +67,7 @@ interface Waiter { |
63 | 67 |
|
64 | 68 | /** |
65 | 69 | * Counting semaphore with abort-aware acquire and dynamic priority. Internal |
66 | | - * primitive used by {@link PerOriginSemaphore} and {@link LimitedSource}. |
| 70 | + * primitive used by {@link PerOriginSemaphore} and {@link LimiterMiddleware}. |
67 | 71 | * |
68 | 72 | * Hands out up to `maxRequests` concurrent slots. Further `acquire()`s queue. |
69 | 73 | * On every slot-open, the queue is searched for the lowest-priority waiter |
@@ -222,91 +226,66 @@ export class PerOriginSemaphore implements ConcurrencyLimiter { |
222 | 226 | } |
223 | 227 | } |
224 | 228 |
|
225 | | -/** Options for {@link LimitedSource}. */ |
226 | | -interface LimitedSourceOptions { |
227 | | - /** The {@link ConcurrencyLimiter} to gate through. The wrapped source's |
228 | | - * own `url` is passed to `limiter.acquire` for per-origin routing. */ |
| 229 | +/** Options for {@link LimiterMiddleware}. */ |
| 230 | +interface LimiterMiddlewareOptions { |
| 231 | + /** The URL the wrapped source is reading from. Passed to |
| 232 | + * `limiter.acquire(url, signal?)` on every fetch — the limiter uses it for |
| 233 | + * per-origin routing. */ |
| 234 | + url: URL; |
| 235 | + /** The {@link ConcurrencyLimiter} to gate through. */ |
229 | 236 | limiter: ConcurrencyLimiter; |
230 | | - /** Optional dynamic priority for every fetch through this source. The |
| 237 | + /** Optional dynamic priority for every fetch through this middleware. The |
231 | 238 | * limiter re-invokes this callback on each slot-open, so closures over |
232 | 239 | * dynamic state (e.g. layer viewport center) re-sort the queue when that |
233 | 240 | * state changes. Lower = serviced sooner. */ |
234 | 241 | getPriority?: () => Priority; |
235 | 242 | } |
236 | 243 |
|
237 | 244 | /** |
238 | | - * Wraps a {@link Source} so every `fetch` holds a {@link ConcurrencyLimiter} |
239 | | - * slot for its duration — acquiring before the read, releasing when it settles |
240 | | - * (resolve or reject). Forwards the read's `signal` to `limiter.acquire`, so a |
241 | | - * request whose caller aborts while it is still queued for a slot is dropped |
242 | | - * before any network I/O fires. |
| 245 | + * chunkd middleware that holds a {@link ConcurrencyLimiter} slot for the |
| 246 | + * duration of each underlying `fetch` — releasing on resolve, on reject, and |
| 247 | + * never otherwise interfering. Forwards the request's `signal` to |
| 248 | + * `limiter.acquire`, so if the caller aborts while the call is queued the |
| 249 | + * request is dropped before any network I/O fires. |
243 | 250 | * |
244 | | - * Compose this *beneath* `SourceChunk` / `SourceCache` (i.e. as the |
245 | | - * `SourceView`'s underlying source), so a cache hit short-circuits in |
246 | | - * `SourceCache` and never reaches — never burns a slot on — the limiter: |
| 251 | + * Composed into a {@link SourceView}'s middleware list alongside the chunkd |
| 252 | + * middlewares (`SourceChunk`, `SourceCache`, …). Place it after caching so |
| 253 | + * cache hits don't burn a slot. |
247 | 254 | * |
248 | 255 | * @example |
249 | 256 | * ```ts |
250 | 257 | * import { SourceView } from "@chunkd/source"; |
251 | 258 | * import { SourceCache, SourceChunk } from "@chunkd/middleware"; |
252 | 259 | * |
253 | | - * const limited = new LimitedSource(source, { limiter }); |
254 | | - * const view = new SourceView(limited, [ |
| 260 | + * const view = new SourceView(source, [ |
255 | 261 | * new SourceChunk({ size: 64 * 1024 }), |
256 | 262 | * new SourceCache({ size: 8 * 1024 * 1024 }), |
| 263 | + * new LimiterMiddleware({ url, limiter }), |
257 | 264 | * ]); |
258 | 265 | * ``` |
259 | 266 | * |
260 | | - * **Why a source wrapper and not a chunkd `SourceMiddleware`** (which would |
261 | | - * compose more naturally): chunkd's `SourceView` does not forward the request |
262 | | - * `signal` to its middleware, so a middleware cannot observe an abort — only |
263 | | - * the underlying source receives the read options (incl. `signal`) via |
264 | | - * `SourceView`'s terminal handler. Wrapping the source is therefore the only |
265 | | - * layer that can drop a queued request on abort. Revert to a `SourceMiddleware` |
266 | | - * once chunkd forwards the signal (https://github.com/blacha/chunkd/pull/1697); |
267 | | - * tracked in https://github.com/developmentseed/deck.gl-raster/issues/565. |
268 | | - * |
269 | 267 | * @internal |
270 | 268 | */ |
271 | | -export class LimitedSource implements Source { |
272 | | - private readonly source: Source; |
| 269 | +export class LimiterMiddleware implements SourceMiddleware { |
| 270 | + readonly name = "limiter"; |
| 271 | + private readonly url: URL; |
273 | 272 | private readonly limiter: ConcurrencyLimiter; |
274 | 273 | private readonly getPriority?: () => Priority; |
275 | 274 |
|
276 | | - constructor(source: Source, opts: LimitedSourceOptions) { |
277 | | - this.source = source; |
| 275 | + constructor(opts: LimiterMiddlewareOptions) { |
| 276 | + this.url = opts.url; |
278 | 277 | this.limiter = opts.limiter; |
279 | 278 | this.getPriority = opts.getPriority; |
280 | 279 | } |
281 | 280 |
|
282 | | - get type(): string { |
283 | | - return this.source.type; |
284 | | - } |
285 | | - |
286 | | - get url(): URL { |
287 | | - return this.source.url; |
288 | | - } |
289 | | - |
290 | | - get metadata(): SourceMetadata | undefined { |
291 | | - return this.source.metadata; |
292 | | - } |
293 | | - |
294 | | - head(options?: { signal: AbortSignal }): Promise<SourceMetadata> { |
295 | | - return this.source.head(options); |
296 | | - } |
297 | | - |
298 | | - async fetch( |
299 | | - offset: number, |
300 | | - length?: number, |
301 | | - options?: { signal: AbortSignal }, |
302 | | - ): Promise<ArrayBuffer> { |
| 281 | + async fetch(req: SourceRequest, next: SourceCallback): Promise<ArrayBuffer> { |
303 | 282 | const release = await this.limiter.acquire( |
304 | | - this.source.url, |
305 | | - options?.signal, |
| 283 | + this.url, |
| 284 | + req.signal, |
306 | 285 | this.getPriority, |
307 | 286 | ); |
308 | 287 | try { |
309 | | - return await this.source.fetch(offset, length, options); |
| 288 | + return await next(req); |
310 | 289 | } finally { |
311 | 290 | release(); |
312 | 291 | } |
|
0 commit comments