@fetchkit/ffetch 5.4.13 → 5.5.1

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.
@@ -110,45 +110,30 @@ function dedupePlugin(options = {}) {
110
110
  }
111
111
  const existing = inFlight.get(key);
112
112
  if (existing) {
113
- return existing.promise;
113
+ return new Promise((resolve, reject) => {
114
+ existing.waiters.push({ resolve, reject });
115
+ });
114
116
  }
115
- let settled = false;
116
- let resolveFn;
117
- let rejectFn;
118
- const placeholder = new Promise((resolve, reject) => {
119
- resolveFn = (value) => {
120
- if (!settled) {
121
- settled = true;
122
- resolve(value);
123
- }
124
- };
125
- rejectFn = (reason) => {
126
- if (!settled) {
127
- settled = true;
128
- reject(reason);
129
- }
130
- };
131
- });
132
- placeholder.catch(() => void 0);
117
+ const waiters = [];
118
+ const actualPromise = next(ctx);
133
119
  inFlight.set(key, {
134
- promise: placeholder,
135
- resolve: resolveFn,
136
- reject: rejectFn,
120
+ promise: actualPromise,
121
+ waiters,
137
122
  createdAt: Date.now()
138
123
  });
139
124
  startSweeper();
140
- const actualPromise = next(ctx);
141
- const entry = inFlight.get(key);
142
- if (entry) {
143
- actualPromise.then(
144
- (result) => entry.resolve(result),
145
- (error) => entry.reject(error)
146
- );
147
- inFlight.set(key, {
148
- ...entry,
149
- promise: actualPromise
150
- });
151
- }
125
+ actualPromise.then(
126
+ (result) => {
127
+ for (const waiter of waiters) {
128
+ waiter.resolve(result.clone());
129
+ }
130
+ },
131
+ (error) => {
132
+ for (const waiter of waiters) {
133
+ waiter.reject(error);
134
+ }
135
+ }
136
+ );
152
137
  return actualPromise.finally(() => {
153
138
  const current = inFlight.get(key);
154
139
  if (current?.promise === actualPromise) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/plugins/dedupe.ts","../../src/dedupeRequestHash.ts"],"sourcesContent":["import type { ClientPlugin, PluginRequestContext } from '../plugins.js'\nimport {\n dedupeRequestHash,\n type DedupeHashParams,\n} from '../dedupeRequestHash.js'\n\ntype DedupeEntry = {\n promise: Promise<Response>\n resolve: (value: Response | PromiseLike<Response>) => void\n reject: (reason?: unknown) => void\n createdAt: number\n}\n\nexport type DedupePluginOptions = {\n hashFn?: (params: DedupeHashParams) => string | undefined\n ttl?: number\n sweepInterval?: number\n order?: number\n}\n\nfunction contextToHashParams(ctx: PluginRequestContext): DedupeHashParams {\n return {\n method: ctx.request.method,\n url: ctx.request.url,\n body: (ctx.init.body ?? null) as DedupeHashParams['body'],\n headers: ctx.request.headers,\n signal:\n ctx.init.signal === undefined || ctx.init.signal === null\n ? undefined\n : ctx.init.signal,\n requestInit: ctx.init,\n request: ctx.request,\n }\n}\n\nexport function dedupePlugin(options: DedupePluginOptions = {}): ClientPlugin {\n const {\n hashFn = dedupeRequestHash,\n ttl,\n sweepInterval = 5000,\n order = 10,\n } = options\n\n const inFlight = new Map<string, DedupeEntry>()\n let sweeper: ReturnType<typeof setInterval> | undefined\n\n function startSweeper() {\n if (sweeper || typeof ttl !== 'number' || ttl <= 0) return\n sweeper = setInterval(() => {\n const now = Date.now()\n for (const [key, entry] of inFlight.entries()) {\n if (now - entry.createdAt > ttl) {\n inFlight.delete(key)\n }\n }\n if (inFlight.size === 0 && sweeper) {\n clearInterval(sweeper)\n sweeper = undefined\n }\n }, sweepInterval)\n }\n\n function stopSweeperIfIdle() {\n if (inFlight.size === 0 && sweeper) {\n clearInterval(sweeper)\n sweeper = undefined\n }\n }\n\n return {\n name: 'dedupe',\n order,\n wrapDispatch: (next) => async (ctx) => {\n const key = hashFn(contextToHashParams(ctx))\n ctx.state.dedupeKey = key\n\n if (!key) {\n return next(ctx)\n }\n\n const existing = inFlight.get(key)\n if (existing) {\n return existing.promise\n }\n\n let settled = false\n let resolveFn: (value: Response | PromiseLike<Response>) => void\n let rejectFn: (reason?: unknown) => void\n\n const placeholder = new Promise<Response>((resolve, reject) => {\n resolveFn = (value) => {\n if (!settled) {\n settled = true\n resolve(value)\n }\n }\n rejectFn = (reason) => {\n if (!settled) {\n settled = true\n reject(reason)\n }\n }\n })\n // Internal placeholder can reject before a consumer attaches handlers.\n // Mark it observed to avoid unhandled-rejection noise.\n placeholder.catch(() => undefined)\n\n inFlight.set(key, {\n promise: placeholder,\n resolve: resolveFn!,\n reject: rejectFn!,\n createdAt: Date.now(),\n })\n startSweeper()\n\n const actualPromise = next(ctx)\n const entry = inFlight.get(key)\n if (entry) {\n actualPromise.then(\n (result) => entry.resolve(result),\n (error) => entry.reject(error)\n )\n inFlight.set(key, {\n ...entry,\n promise: actualPromise,\n })\n }\n\n return actualPromise.finally(() => {\n const current = inFlight.get(key)\n if (current?.promise === actualPromise) {\n inFlight.delete(key)\n stopSweeperIfIdle()\n }\n })\n },\n }\n}\n\nexport { dedupeRequestHash }\nexport type { DedupeHashParams }\n","export type DedupeHashParams = {\n method: string\n url: string\n body:\n | string\n | FormData\n | URLSearchParams\n | Blob\n | ArrayBuffer\n | BufferSource\n | null\n | ReadableStream<unknown>\n headers?: Headers | Record<string, string>\n signal?: AbortSignal\n requestInit?: RequestInit\n request?: Request\n}\n\nexport function dedupeRequestHash(\n params: DedupeHashParams\n): string | undefined {\n const { method, url, body } = params\n let bodyString = ''\n if (body instanceof FormData) {\n // Skip deduplication for FormData\n return undefined\n }\n // Skip deduplication for ReadableStream\n if (typeof ReadableStream !== 'undefined' && body instanceof ReadableStream) {\n return undefined\n }\n if (typeof body === 'string') {\n bodyString = body\n } else if (body instanceof URLSearchParams) {\n bodyString = body.toString()\n } else if (body instanceof ArrayBuffer) {\n bodyString = Buffer.from(body).toString('base64')\n } else if (body instanceof Uint8Array) {\n bodyString = Buffer.from(body).toString('base64')\n } else if (body instanceof Blob) {\n bodyString = `[blob:${body.type}:${body.size}]`\n } else if (body == null) {\n bodyString = ''\n } else {\n try {\n bodyString = JSON.stringify(body)\n } catch {\n bodyString = '[unserializable-body]'\n }\n }\n return `${method.toUpperCase()}|${url}|${bodyString}`\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/plugins/dedupe.ts","../../src/dedupeRequestHash.ts"],"sourcesContent":["import type { ClientPlugin, PluginRequestContext } from '../plugins.js'\nimport {\n dedupeRequestHash,\n type DedupeHashParams,\n} from '../dedupeRequestHash.js'\n\ntype Waiter = {\n resolve: (value: Response) => void\n reject: (reason?: unknown) => void\n}\n\ntype DedupeEntry = {\n promise: Promise<Response>\n waiters: Waiter[]\n createdAt: number\n}\n\nexport type DedupePluginOptions = {\n hashFn?: (params: DedupeHashParams) => string | undefined\n ttl?: number\n sweepInterval?: number\n order?: number\n}\n\nfunction contextToHashParams(ctx: PluginRequestContext): DedupeHashParams {\n return {\n method: ctx.request.method,\n url: ctx.request.url,\n body: (ctx.init.body ?? null) as DedupeHashParams['body'],\n headers: ctx.request.headers,\n signal:\n ctx.init.signal === undefined || ctx.init.signal === null\n ? undefined\n : ctx.init.signal,\n requestInit: ctx.init,\n request: ctx.request,\n }\n}\n\nexport function dedupePlugin(options: DedupePluginOptions = {}): ClientPlugin {\n const {\n hashFn = dedupeRequestHash,\n ttl,\n sweepInterval = 5000,\n order = 10,\n } = options\n\n const inFlight = new Map<string, DedupeEntry>()\n let sweeper: ReturnType<typeof setInterval> | undefined\n\n function startSweeper() {\n if (sweeper || typeof ttl !== 'number' || ttl <= 0) return\n sweeper = setInterval(() => {\n const now = Date.now()\n for (const [key, entry] of inFlight.entries()) {\n if (now - entry.createdAt > ttl) {\n inFlight.delete(key)\n }\n }\n if (inFlight.size === 0 && sweeper) {\n clearInterval(sweeper)\n sweeper = undefined\n }\n }, sweepInterval)\n }\n\n function stopSweeperIfIdle() {\n if (inFlight.size === 0 && sweeper) {\n clearInterval(sweeper)\n sweeper = undefined\n }\n }\n\n return {\n name: 'dedupe',\n order,\n wrapDispatch: (next) => async (ctx) => {\n const key = hashFn(contextToHashParams(ctx))\n ctx.state.dedupeKey = key\n\n if (!key) {\n return next(ctx)\n }\n\n const existing = inFlight.get(key)\n if (existing) {\n return new Promise<Response>((resolve, reject) => {\n existing.waiters.push({ resolve, reject })\n })\n }\n\n const waiters: Waiter[] = []\n const actualPromise = next(ctx)\n\n inFlight.set(key, {\n promise: actualPromise,\n waiters,\n createdAt: Date.now(),\n })\n startSweeper()\n\n actualPromise.then(\n (result) => {\n for (const waiter of waiters) {\n waiter.resolve(result.clone())\n }\n },\n (error) => {\n for (const waiter of waiters) {\n waiter.reject(error)\n }\n }\n )\n\n return actualPromise.finally(() => {\n const current = inFlight.get(key)\n if (current?.promise === actualPromise) {\n inFlight.delete(key)\n stopSweeperIfIdle()\n }\n })\n },\n }\n}\n\nexport { dedupeRequestHash }\nexport type { DedupeHashParams }\n","export type DedupeHashParams = {\n method: string\n url: string\n body:\n | string\n | FormData\n | URLSearchParams\n | Blob\n | ArrayBuffer\n | BufferSource\n | null\n | ReadableStream<unknown>\n headers?: Headers | Record<string, string>\n signal?: AbortSignal\n requestInit?: RequestInit\n request?: Request\n}\n\nexport function dedupeRequestHash(\n params: DedupeHashParams\n): string | undefined {\n const { method, url, body } = params\n let bodyString = ''\n if (body instanceof FormData) {\n // Skip deduplication for FormData\n return undefined\n }\n // Skip deduplication for ReadableStream\n if (typeof ReadableStream !== 'undefined' && body instanceof ReadableStream) {\n return undefined\n }\n if (typeof body === 'string') {\n bodyString = body\n } else if (body instanceof URLSearchParams) {\n bodyString = body.toString()\n } else if (body instanceof ArrayBuffer) {\n bodyString = Buffer.from(body).toString('base64')\n } else if (body instanceof Uint8Array) {\n bodyString = Buffer.from(body).toString('base64')\n } else if (body instanceof Blob) {\n bodyString = `[blob:${body.type}:${body.size}]`\n } else if (body == null) {\n bodyString = ''\n } else {\n try {\n bodyString = JSON.stringify(body)\n } catch {\n bodyString = '[unserializable-body]'\n }\n }\n return `${method.toUpperCase()}|${url}|${bodyString}`\n}\n"],"mappings":"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","names":[]}
@@ -83,45 +83,30 @@ function dedupePlugin(options = {}) {
83
83
  }
84
84
  const existing = inFlight.get(key);
85
85
  if (existing) {
86
- return existing.promise;
86
+ return new Promise((resolve, reject) => {
87
+ existing.waiters.push({ resolve, reject });
88
+ });
87
89
  }
88
- let settled = false;
89
- let resolveFn;
90
- let rejectFn;
91
- const placeholder = new Promise((resolve, reject) => {
92
- resolveFn = (value) => {
93
- if (!settled) {
94
- settled = true;
95
- resolve(value);
96
- }
97
- };
98
- rejectFn = (reason) => {
99
- if (!settled) {
100
- settled = true;
101
- reject(reason);
102
- }
103
- };
104
- });
105
- placeholder.catch(() => void 0);
90
+ const waiters = [];
91
+ const actualPromise = next(ctx);
106
92
  inFlight.set(key, {
107
- promise: placeholder,
108
- resolve: resolveFn,
109
- reject: rejectFn,
93
+ promise: actualPromise,
94
+ waiters,
110
95
  createdAt: Date.now()
111
96
  });
112
97
  startSweeper();
113
- const actualPromise = next(ctx);
114
- const entry = inFlight.get(key);
115
- if (entry) {
116
- actualPromise.then(
117
- (result) => entry.resolve(result),
118
- (error) => entry.reject(error)
119
- );
120
- inFlight.set(key, {
121
- ...entry,
122
- promise: actualPromise
123
- });
124
- }
98
+ actualPromise.then(
99
+ (result) => {
100
+ for (const waiter of waiters) {
101
+ waiter.resolve(result.clone());
102
+ }
103
+ },
104
+ (error) => {
105
+ for (const waiter of waiters) {
106
+ waiter.reject(error);
107
+ }
108
+ }
109
+ );
125
110
  return actualPromise.finally(() => {
126
111
  const current = inFlight.get(key);
127
112
  if (current?.promise === actualPromise) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/dedupeRequestHash.ts","../../src/plugins/dedupe.ts"],"sourcesContent":["export type DedupeHashParams = {\n method: string\n url: string\n body:\n | string\n | FormData\n | URLSearchParams\n | Blob\n | ArrayBuffer\n | BufferSource\n | null\n | ReadableStream<unknown>\n headers?: Headers | Record<string, string>\n signal?: AbortSignal\n requestInit?: RequestInit\n request?: Request\n}\n\nexport function dedupeRequestHash(\n params: DedupeHashParams\n): string | undefined {\n const { method, url, body } = params\n let bodyString = ''\n if (body instanceof FormData) {\n // Skip deduplication for FormData\n return undefined\n }\n // Skip deduplication for ReadableStream\n if (typeof ReadableStream !== 'undefined' && body instanceof ReadableStream) {\n return undefined\n }\n if (typeof body === 'string') {\n bodyString = body\n } else if (body instanceof URLSearchParams) {\n bodyString = body.toString()\n } else if (body instanceof ArrayBuffer) {\n bodyString = Buffer.from(body).toString('base64')\n } else if (body instanceof Uint8Array) {\n bodyString = Buffer.from(body).toString('base64')\n } else if (body instanceof Blob) {\n bodyString = `[blob:${body.type}:${body.size}]`\n } else if (body == null) {\n bodyString = ''\n } else {\n try {\n bodyString = JSON.stringify(body)\n } catch {\n bodyString = '[unserializable-body]'\n }\n }\n return `${method.toUpperCase()}|${url}|${bodyString}`\n}\n","import type { ClientPlugin, PluginRequestContext } from '../plugins.js'\nimport {\n dedupeRequestHash,\n type DedupeHashParams,\n} from '../dedupeRequestHash.js'\n\ntype DedupeEntry = {\n promise: Promise<Response>\n resolve: (value: Response | PromiseLike<Response>) => void\n reject: (reason?: unknown) => void\n createdAt: number\n}\n\nexport type DedupePluginOptions = {\n hashFn?: (params: DedupeHashParams) => string | undefined\n ttl?: number\n sweepInterval?: number\n order?: number\n}\n\nfunction contextToHashParams(ctx: PluginRequestContext): DedupeHashParams {\n return {\n method: ctx.request.method,\n url: ctx.request.url,\n body: (ctx.init.body ?? null) as DedupeHashParams['body'],\n headers: ctx.request.headers,\n signal:\n ctx.init.signal === undefined || ctx.init.signal === null\n ? undefined\n : ctx.init.signal,\n requestInit: ctx.init,\n request: ctx.request,\n }\n}\n\nexport function dedupePlugin(options: DedupePluginOptions = {}): ClientPlugin {\n const {\n hashFn = dedupeRequestHash,\n ttl,\n sweepInterval = 5000,\n order = 10,\n } = options\n\n const inFlight = new Map<string, DedupeEntry>()\n let sweeper: ReturnType<typeof setInterval> | undefined\n\n function startSweeper() {\n if (sweeper || typeof ttl !== 'number' || ttl <= 0) return\n sweeper = setInterval(() => {\n const now = Date.now()\n for (const [key, entry] of inFlight.entries()) {\n if (now - entry.createdAt > ttl) {\n inFlight.delete(key)\n }\n }\n if (inFlight.size === 0 && sweeper) {\n clearInterval(sweeper)\n sweeper = undefined\n }\n }, sweepInterval)\n }\n\n function stopSweeperIfIdle() {\n if (inFlight.size === 0 && sweeper) {\n clearInterval(sweeper)\n sweeper = undefined\n }\n }\n\n return {\n name: 'dedupe',\n order,\n wrapDispatch: (next) => async (ctx) => {\n const key = hashFn(contextToHashParams(ctx))\n ctx.state.dedupeKey = key\n\n if (!key) {\n return next(ctx)\n }\n\n const existing = inFlight.get(key)\n if (existing) {\n return existing.promise\n }\n\n let settled = false\n let resolveFn: (value: Response | PromiseLike<Response>) => void\n let rejectFn: (reason?: unknown) => void\n\n const placeholder = new Promise<Response>((resolve, reject) => {\n resolveFn = (value) => {\n if (!settled) {\n settled = true\n resolve(value)\n }\n }\n rejectFn = (reason) => {\n if (!settled) {\n settled = true\n reject(reason)\n }\n }\n })\n // Internal placeholder can reject before a consumer attaches handlers.\n // Mark it observed to avoid unhandled-rejection noise.\n placeholder.catch(() => undefined)\n\n inFlight.set(key, {\n promise: placeholder,\n resolve: resolveFn!,\n reject: rejectFn!,\n createdAt: Date.now(),\n })\n startSweeper()\n\n const actualPromise = next(ctx)\n const entry = inFlight.get(key)\n if (entry) {\n actualPromise.then(\n (result) => entry.resolve(result),\n (error) => entry.reject(error)\n )\n inFlight.set(key, {\n ...entry,\n promise: actualPromise,\n })\n }\n\n return actualPromise.finally(() => {\n const current = inFlight.get(key)\n if (current?.promise === actualPromise) {\n inFlight.delete(key)\n stopSweeperIfIdle()\n }\n })\n },\n }\n}\n\nexport { dedupeRequestHash }\nexport type { DedupeHashParams }\n"],"mappings":";AAkBO,SAAS,kBACd,QACoB;AACpB,QAAM,EAAE,QAAQ,KAAK,KAAK,IAAI;AAC9B,MAAI,aAAa;AACjB,MAAI,gBAAgB,UAAU;AAE5B,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,mBAAmB,eAAe,gBAAgB,gBAAgB;AAC3E,WAAO;AAAA,EACT;AACA,MAAI,OAAO,SAAS,UAAU;AAC5B,iBAAa;AAAA,EACf,WAAW,gBAAgB,iBAAiB;AAC1C,iBAAa,KAAK,SAAS;AAAA,EAC7B,WAAW,gBAAgB,aAAa;AACtC,iBAAa,OAAO,KAAK,IAAI,EAAE,SAAS,QAAQ;AAAA,EAClD,WAAW,gBAAgB,YAAY;AACrC,iBAAa,OAAO,KAAK,IAAI,EAAE,SAAS,QAAQ;AAAA,EAClD,WAAW,gBAAgB,MAAM;AAC/B,iBAAa,SAAS,KAAK,IAAI,IAAI,KAAK,IAAI;AAAA,EAC9C,WAAW,QAAQ,MAAM;AACvB,iBAAa;AAAA,EACf,OAAO;AACL,QAAI;AACF,mBAAa,KAAK,UAAU,IAAI;AAAA,IAClC,QAAQ;AACN,mBAAa;AAAA,IACf;AAAA,EACF;AACA,SAAO,GAAG,OAAO,YAAY,CAAC,IAAI,GAAG,IAAI,UAAU;AACrD;;;AC/BA,SAAS,oBAAoB,KAA6C;AACxE,SAAO;AAAA,IACL,QAAQ,IAAI,QAAQ;AAAA,IACpB,KAAK,IAAI,QAAQ;AAAA,IACjB,MAAO,IAAI,KAAK,QAAQ;AAAA,IACxB,SAAS,IAAI,QAAQ;AAAA,IACrB,QACE,IAAI,KAAK,WAAW,UAAa,IAAI,KAAK,WAAW,OACjD,SACA,IAAI,KAAK;AAAA,IACf,aAAa,IAAI;AAAA,IACjB,SAAS,IAAI;AAAA,EACf;AACF;AAEO,SAAS,aAAa,UAA+B,CAAC,GAAiB;AAC5E,QAAM;AAAA,IACJ,SAAS;AAAA,IACT;AAAA,IACA,gBAAgB;AAAA,IAChB,QAAQ;AAAA,EACV,IAAI;AAEJ,QAAM,WAAW,oBAAI,IAAyB;AAC9C,MAAI;AAEJ,WAAS,eAAe;AACtB,QAAI,WAAW,OAAO,QAAQ,YAAY,OAAO,EAAG;AACpD,cAAU,YAAY,MAAM;AAC1B,YAAM,MAAM,KAAK,IAAI;AACrB,iBAAW,CAAC,KAAK,KAAK,KAAK,SAAS,QAAQ,GAAG;AAC7C,YAAI,MAAM,MAAM,YAAY,KAAK;AAC/B,mBAAS,OAAO,GAAG;AAAA,QACrB;AAAA,MACF;AACA,UAAI,SAAS,SAAS,KAAK,SAAS;AAClC,sBAAc,OAAO;AACrB,kBAAU;AAAA,MACZ;AAAA,IACF,GAAG,aAAa;AAAA,EAClB;AAEA,WAAS,oBAAoB;AAC3B,QAAI,SAAS,SAAS,KAAK,SAAS;AAClC,oBAAc,OAAO;AACrB,gBAAU;AAAA,IACZ;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IACA,cAAc,CAAC,SAAS,OAAO,QAAQ;AACrC,YAAM,MAAM,OAAO,oBAAoB,GAAG,CAAC;AAC3C,UAAI,MAAM,YAAY;AAEtB,UAAI,CAAC,KAAK;AACR,eAAO,KAAK,GAAG;AAAA,MACjB;AAEA,YAAM,WAAW,SAAS,IAAI,GAAG;AACjC,UAAI,UAAU;AACZ,eAAO,SAAS;AAAA,MAClB;AAEA,UAAI,UAAU;AACd,UAAI;AACJ,UAAI;AAEJ,YAAM,cAAc,IAAI,QAAkB,CAAC,SAAS,WAAW;AAC7D,oBAAY,CAAC,UAAU;AACrB,cAAI,CAAC,SAAS;AACZ,sBAAU;AACV,oBAAQ,KAAK;AAAA,UACf;AAAA,QACF;AACA,mBAAW,CAAC,WAAW;AACrB,cAAI,CAAC,SAAS;AACZ,sBAAU;AACV,mBAAO,MAAM;AAAA,UACf;AAAA,QACF;AAAA,MACF,CAAC;AAGD,kBAAY,MAAM,MAAM,MAAS;AAEjC,eAAS,IAAI,KAAK;AAAA,QAChB,SAAS;AAAA,QACT,SAAS;AAAA,QACT,QAAQ;AAAA,QACR,WAAW,KAAK,IAAI;AAAA,MACtB,CAAC;AACD,mBAAa;AAEb,YAAM,gBAAgB,KAAK,GAAG;AAC9B,YAAM,QAAQ,SAAS,IAAI,GAAG;AAC9B,UAAI,OAAO;AACT,sBAAc;AAAA,UACZ,CAAC,WAAW,MAAM,QAAQ,MAAM;AAAA,UAChC,CAAC,UAAU,MAAM,OAAO,KAAK;AAAA,QAC/B;AACA,iBAAS,IAAI,KAAK;AAAA,UAChB,GAAG;AAAA,UACH,SAAS;AAAA,QACX,CAAC;AAAA,MACH;AAEA,aAAO,cAAc,QAAQ,MAAM;AACjC,cAAM,UAAU,SAAS,IAAI,GAAG;AAChC,YAAI,SAAS,YAAY,eAAe;AACtC,mBAAS,OAAO,GAAG;AACnB,4BAAkB;AAAA,QACpB;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/dedupeRequestHash.ts","../../src/plugins/dedupe.ts"],"sourcesContent":["export type DedupeHashParams = {\n method: string\n url: string\n body:\n | string\n | FormData\n | URLSearchParams\n | Blob\n | ArrayBuffer\n | BufferSource\n | null\n | ReadableStream<unknown>\n headers?: Headers | Record<string, string>\n signal?: AbortSignal\n requestInit?: RequestInit\n request?: Request\n}\n\nexport function dedupeRequestHash(\n params: DedupeHashParams\n): string | undefined {\n const { method, url, body } = params\n let bodyString = ''\n if (body instanceof FormData) {\n // Skip deduplication for FormData\n return undefined\n }\n // Skip deduplication for ReadableStream\n if (typeof ReadableStream !== 'undefined' && body instanceof ReadableStream) {\n return undefined\n }\n if (typeof body === 'string') {\n bodyString = body\n } else if (body instanceof URLSearchParams) {\n bodyString = body.toString()\n } else if (body instanceof ArrayBuffer) {\n bodyString = Buffer.from(body).toString('base64')\n } else if (body instanceof Uint8Array) {\n bodyString = Buffer.from(body).toString('base64')\n } else if (body instanceof Blob) {\n bodyString = `[blob:${body.type}:${body.size}]`\n } else if (body == null) {\n bodyString = ''\n } else {\n try {\n bodyString = JSON.stringify(body)\n } catch {\n bodyString = '[unserializable-body]'\n }\n }\n return `${method.toUpperCase()}|${url}|${bodyString}`\n}\n","import type { ClientPlugin, PluginRequestContext } from '../plugins.js'\nimport {\n dedupeRequestHash,\n type DedupeHashParams,\n} from '../dedupeRequestHash.js'\n\ntype Waiter = {\n resolve: (value: Response) => void\n reject: (reason?: unknown) => void\n}\n\ntype DedupeEntry = {\n promise: Promise<Response>\n waiters: Waiter[]\n createdAt: number\n}\n\nexport type DedupePluginOptions = {\n hashFn?: (params: DedupeHashParams) => string | undefined\n ttl?: number\n sweepInterval?: number\n order?: number\n}\n\nfunction contextToHashParams(ctx: PluginRequestContext): DedupeHashParams {\n return {\n method: ctx.request.method,\n url: ctx.request.url,\n body: (ctx.init.body ?? null) as DedupeHashParams['body'],\n headers: ctx.request.headers,\n signal:\n ctx.init.signal === undefined || ctx.init.signal === null\n ? undefined\n : ctx.init.signal,\n requestInit: ctx.init,\n request: ctx.request,\n }\n}\n\nexport function dedupePlugin(options: DedupePluginOptions = {}): ClientPlugin {\n const {\n hashFn = dedupeRequestHash,\n ttl,\n sweepInterval = 5000,\n order = 10,\n } = options\n\n const inFlight = new Map<string, DedupeEntry>()\n let sweeper: ReturnType<typeof setInterval> | undefined\n\n function startSweeper() {\n if (sweeper || typeof ttl !== 'number' || ttl <= 0) return\n sweeper = setInterval(() => {\n const now = Date.now()\n for (const [key, entry] of inFlight.entries()) {\n if (now - entry.createdAt > ttl) {\n inFlight.delete(key)\n }\n }\n if (inFlight.size === 0 && sweeper) {\n clearInterval(sweeper)\n sweeper = undefined\n }\n }, sweepInterval)\n }\n\n function stopSweeperIfIdle() {\n if (inFlight.size === 0 && sweeper) {\n clearInterval(sweeper)\n sweeper = undefined\n }\n }\n\n return {\n name: 'dedupe',\n order,\n wrapDispatch: (next) => async (ctx) => {\n const key = hashFn(contextToHashParams(ctx))\n ctx.state.dedupeKey = key\n\n if (!key) {\n return next(ctx)\n }\n\n const existing = inFlight.get(key)\n if (existing) {\n return new Promise<Response>((resolve, reject) => {\n existing.waiters.push({ resolve, reject })\n })\n }\n\n const waiters: Waiter[] = []\n const actualPromise = next(ctx)\n\n inFlight.set(key, {\n promise: actualPromise,\n waiters,\n createdAt: Date.now(),\n })\n startSweeper()\n\n actualPromise.then(\n (result) => {\n for (const waiter of waiters) {\n waiter.resolve(result.clone())\n }\n },\n (error) => {\n for (const waiter of waiters) {\n waiter.reject(error)\n }\n }\n )\n\n return actualPromise.finally(() => {\n const current = inFlight.get(key)\n if (current?.promise === actualPromise) {\n inFlight.delete(key)\n stopSweeperIfIdle()\n }\n })\n },\n }\n}\n\nexport { dedupeRequestHash }\nexport type { DedupeHashParams }\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fetchkit/ffetch",
3
- "version": "5.4.13",
3
+ "version": "5.5.1",
4
4
  "description": "Fetch wrapper with configurable timeouts, retries, and TypeScript-first DX",
5
5
  "keywords": [
6
6
  "fetch",