Merge remote-tracking branch 'origin/main' into sqlite-worker
This commit is contained in:
commit
01886059ab
|
@ -1 +1 @@
|
||||||
deno 1.37.1
|
deno 1.38.3
|
||||||
|
|
|
@ -115,7 +115,7 @@ app.post('/oauth/revoke', emptyObjectController);
|
||||||
app.post('/oauth/authorize', oauthAuthorizeController);
|
app.post('/oauth/authorize', oauthAuthorizeController);
|
||||||
app.get('/oauth/authorize', oauthController);
|
app.get('/oauth/authorize', oauthController);
|
||||||
|
|
||||||
app.post('/api/v1/accounts', requireProof(), createAccountController);
|
app.post('/api/v1/accounts', requireProof({ pow: 20 }), createAccountController);
|
||||||
app.get('/api/v1/accounts/verify_credentials', requirePubkey, verifyCredentialsController);
|
app.get('/api/v1/accounts/verify_credentials', requirePubkey, verifyCredentialsController);
|
||||||
app.patch(
|
app.patch(
|
||||||
'/api/v1/accounts/update_credentials',
|
'/api/v1/accounts/update_credentials',
|
||||||
|
|
|
@ -28,8 +28,8 @@ const streamSchema = z.enum([
|
||||||
type Stream = z.infer<typeof streamSchema>;
|
type Stream = z.infer<typeof streamSchema>;
|
||||||
|
|
||||||
const streamingController: AppController = (c) => {
|
const streamingController: AppController = (c) => {
|
||||||
const upgrade = c.req.headers.get('upgrade');
|
const upgrade = c.req.header('upgrade');
|
||||||
const token = c.req.headers.get('sec-websocket-protocol');
|
const token = c.req.header('sec-websocket-protocol');
|
||||||
const stream = streamSchema.optional().catch(undefined).parse(c.req.query('stream'));
|
const stream = streamSchema.optional().catch(undefined).parse(c.req.query('stream'));
|
||||||
|
|
||||||
if (upgrade?.toLowerCase() !== 'websocket') {
|
if (upgrade?.toLowerCase() !== 'websocket') {
|
||||||
|
|
|
@ -117,7 +117,7 @@ function prepareFilters(filters: ClientREQ[2][]): Filter[] {
|
||||||
}
|
}
|
||||||
|
|
||||||
const relayController: AppController = (c) => {
|
const relayController: AppController = (c) => {
|
||||||
const upgrade = c.req.headers.get('upgrade');
|
const upgrade = c.req.header('upgrade');
|
||||||
|
|
||||||
if (upgrade?.toLowerCase() !== 'websocket') {
|
if (upgrade?.toLowerCase() !== 'websocket') {
|
||||||
return c.text('Please use a Nostr client to connect.', 400);
|
return c.text('Please use a Nostr client to connect.', 400);
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export { assert, assertEquals, assertThrows } from 'https://deno.land/std@0.198.0/assert/mod.ts';
|
export { assert, assertEquals, assertRejects, assertThrows } from 'https://deno.land/std@0.198.0/assert/mod.ts';
|
||||||
|
|
10
src/deps.ts
10
src/deps.ts
|
@ -6,8 +6,8 @@ export {
|
||||||
Hono,
|
Hono,
|
||||||
HTTPException,
|
HTTPException,
|
||||||
type MiddlewareHandler,
|
type MiddlewareHandler,
|
||||||
} from 'https://deno.land/x/hono@v3.7.5/mod.ts';
|
} from 'https://deno.land/x/hono@v3.10.1/mod.ts';
|
||||||
export { cors, logger, serveStatic } from 'https://deno.land/x/hono@v3.7.5/middleware.ts';
|
export { cors, logger, serveStatic } from 'https://deno.land/x/hono@v3.10.1/middleware.ts';
|
||||||
export { z } from 'https://deno.land/x/zod@v3.21.4/mod.ts';
|
export { z } from 'https://deno.land/x/zod@v3.21.4/mod.ts';
|
||||||
export { Author, RelayPool } from 'https://dev.jspm.io/nostr-relaypool@0.6.28';
|
export { Author, RelayPool } from 'https://dev.jspm.io/nostr-relaypool@0.6.28';
|
||||||
export {
|
export {
|
||||||
|
@ -25,8 +25,9 @@ export {
|
||||||
nip19,
|
nip19,
|
||||||
nip21,
|
nip21,
|
||||||
type UnsignedEvent,
|
type UnsignedEvent,
|
||||||
|
type VerifiedEvent,
|
||||||
verifySignature,
|
verifySignature,
|
||||||
} from 'npm:nostr-tools@^1.14.0';
|
} from 'npm:nostr-tools@^1.17.0';
|
||||||
export { findReplyTag } from 'https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ade5e01597c6d23e22e53c628ef0e2/src/nostr/tags.ts';
|
export { findReplyTag } from 'https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ade5e01597c6d23e22e53c628ef0e2/src/nostr/tags.ts';
|
||||||
export { parseFormData } from 'npm:formdata-helper@^0.3.0';
|
export { parseFormData } from 'npm:formdata-helper@^0.3.0';
|
||||||
// @deno-types="npm:@types/lodash@4.14.194"
|
// @deno-types="npm:@types/lodash@4.14.194"
|
||||||
|
@ -73,7 +74,8 @@ export { S3Client } from 'https://deno.land/x/s3_lite_client@0.6.1/mod.ts';
|
||||||
export { default as IpfsHash } from 'npm:ipfs-only-hash@^4.0.0';
|
export { default as IpfsHash } from 'npm:ipfs-only-hash@^4.0.0';
|
||||||
export { default as uuid62 } from 'npm:uuid62@^1.0.2';
|
export { default as uuid62 } from 'npm:uuid62@^1.0.2';
|
||||||
export { Machina } from 'https://gitlab.com/soapbox-pub/nostr-machina/-/raw/08a157d39f2741c9a3a4364cb97db36e71d8c03a/mod.ts';
|
export { Machina } from 'https://gitlab.com/soapbox-pub/nostr-machina/-/raw/08a157d39f2741c9a3a4364cb97db36e71d8c03a/mod.ts';
|
||||||
export * as Sentry from 'npm:@sentry/node@^7.73.0';
|
export * as Sentry from 'https://deno.land/x/sentry@7.78.0/index.js';
|
||||||
export { sentry as sentryMiddleware } from 'npm:@hono/sentry@^1.0.0';
|
export { sentry as sentryMiddleware } from 'npm:@hono/sentry@^1.0.0';
|
||||||
|
export * as Comlink from 'npm:comlink@^4.4.1';
|
||||||
|
|
||||||
export type * as TypeFest from 'npm:type-fest@^4.3.0';
|
export type * as TypeFest from 'npm:type-fest@^4.3.0';
|
||||||
|
|
|
@ -6,7 +6,7 @@ const BEARER_REGEX = new RegExp(`^Bearer (${nip19.BECH32_REGEX.source})$`);
|
||||||
|
|
||||||
/** NIP-19 auth middleware. */
|
/** NIP-19 auth middleware. */
|
||||||
const auth19: AppMiddleware = async (c, next) => {
|
const auth19: AppMiddleware = async (c, next) => {
|
||||||
const authHeader = c.req.headers.get('authorization');
|
const authHeader = c.req.header('authorization');
|
||||||
const match = authHeader?.match(BEARER_REGEX);
|
const match = authHeader?.match(BEARER_REGEX);
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
|
|
|
@ -91,7 +91,7 @@ function withProof(
|
||||||
async function obtainProof(c: AppContext, opts?: ParseAuthRequestOpts) {
|
async function obtainProof(c: AppContext, opts?: ParseAuthRequestOpts) {
|
||||||
const req = localRequest(c);
|
const req = localRequest(c);
|
||||||
const reqEvent = await buildAuthEventTemplate(req, opts);
|
const reqEvent = await buildAuthEventTemplate(req, opts);
|
||||||
const resEvent = await signEvent(reqEvent, c);
|
const resEvent = await signEvent(reqEvent, c, opts);
|
||||||
const result = await validateAuthEvent(req, resEvent, opts);
|
const result = await validateAuthEvent(req, resEvent, opts);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
|
25
src/sign.ts
25
src/sign.ts
|
@ -8,22 +8,31 @@ import { Sub } from '@/subs.ts';
|
||||||
import { eventMatchesTemplate, Time } from '@/utils.ts';
|
import { eventMatchesTemplate, Time } from '@/utils.ts';
|
||||||
import { createAdminEvent } from '@/utils/web.ts';
|
import { createAdminEvent } from '@/utils/web.ts';
|
||||||
|
|
||||||
|
interface SignEventOpts {
|
||||||
|
/** Target proof-of-work difficulty for the signed event. */
|
||||||
|
pow?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sign Nostr event using the app context.
|
* Sign Nostr event using the app context.
|
||||||
*
|
*
|
||||||
* - If a secret key is provided, it will be used to sign the event.
|
* - If a secret key is provided, it will be used to sign the event.
|
||||||
* - If `X-Nostr-Sign` is passed, it will use NIP-46 to sign the event.
|
* - If `X-Nostr-Sign` is passed, it will use NIP-46 to sign the event.
|
||||||
*/
|
*/
|
||||||
async function signEvent<K extends number = number>(event: EventTemplate<K>, c: AppContext): Promise<Event<K>> {
|
async function signEvent<K extends number = number>(
|
||||||
|
event: EventTemplate<K>,
|
||||||
|
c: AppContext,
|
||||||
|
opts: SignEventOpts = {},
|
||||||
|
): Promise<Event<K>> {
|
||||||
const seckey = c.get('seckey');
|
const seckey = c.get('seckey');
|
||||||
const header = c.req.headers.get('x-nostr-sign');
|
const header = c.req.header('x-nostr-sign');
|
||||||
|
|
||||||
if (seckey) {
|
if (seckey) {
|
||||||
return finishEvent(event, seckey);
|
return finishEvent(event, seckey);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (header) {
|
if (header) {
|
||||||
return await signNostrConnect(event, c);
|
return await signNostrConnect(event, c, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new HTTPException(400, {
|
throw new HTTPException(400, {
|
||||||
|
@ -32,7 +41,11 @@ async function signEvent<K extends number = number>(event: EventTemplate<K>, c:
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sign event with NIP-46, waiting in the background for the signed event. */
|
/** Sign event with NIP-46, waiting in the background for the signed event. */
|
||||||
async function signNostrConnect<K extends number = number>(event: EventTemplate<K>, c: AppContext): Promise<Event<K>> {
|
async function signNostrConnect<K extends number = number>(
|
||||||
|
event: EventTemplate<K>,
|
||||||
|
c: AppContext,
|
||||||
|
opts: SignEventOpts = {},
|
||||||
|
): Promise<Event<K>> {
|
||||||
const pubkey = c.get('pubkey');
|
const pubkey = c.get('pubkey');
|
||||||
|
|
||||||
if (!pubkey) {
|
if (!pubkey) {
|
||||||
|
@ -48,7 +61,9 @@ async function signNostrConnect<K extends number = number>(event: EventTemplate<
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
id: messageId,
|
id: messageId,
|
||||||
method: 'sign_event',
|
method: 'sign_event',
|
||||||
params: [event],
|
params: [event, {
|
||||||
|
pow: opts.pow,
|
||||||
|
}],
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
tags: [['p', pubkey]],
|
tags: [['p', pubkey]],
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Conf } from '@/config.ts';
|
import { Conf } from '@/config.ts';
|
||||||
import { z } from '@/deps.ts';
|
import { z } from '@/deps.ts';
|
||||||
|
import { fetchWorker } from '@/workers/fetch.ts';
|
||||||
|
|
||||||
import type { Uploader } from './types.ts';
|
import type { Uploader } from './types.ts';
|
||||||
|
|
||||||
|
@ -22,7 +23,7 @@ const ipfsUploader: Uploader = {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetchWorker(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
|
@ -41,7 +42,7 @@ const ipfsUploader: Uploader = {
|
||||||
|
|
||||||
url.search = query.toString();
|
url.search = query.toString();
|
||||||
|
|
||||||
await fetch(url, {
|
await fetchWorker(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
24
src/utils.ts
24
src/utils.ts
|
@ -107,9 +107,31 @@ function dedupeEvents<K extends number>(events: Event<K>[]): Event<K>[] {
|
||||||
return [...new Map(events.map((event) => [event.id, event])).values()];
|
return [...new Map(events.map((event) => [event.id, event])).values()];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Return a copy of the event with the given tags removed. */
|
||||||
|
function stripTags<E extends EventTemplate>(event: E, tags: string[] = []): E {
|
||||||
|
if (!tags.length) return event;
|
||||||
|
return {
|
||||||
|
...event,
|
||||||
|
tags: event.tags.filter(([name]) => !tags.includes(name)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** Ensure the template and event match on their shared keys. */
|
/** Ensure the template and event match on their shared keys. */
|
||||||
function eventMatchesTemplate(event: Event, template: EventTemplate): boolean {
|
function eventMatchesTemplate(event: Event, template: EventTemplate): boolean {
|
||||||
return getEventHash(event) === getEventHash({ pubkey: event.pubkey, ...template });
|
const whitelist = ['nonce'];
|
||||||
|
|
||||||
|
event = stripTags(event, whitelist);
|
||||||
|
template = stripTags(template, whitelist);
|
||||||
|
|
||||||
|
if (template.created_at > event.created_at) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getEventHash(event) === getEventHash({
|
||||||
|
pubkey: event.pubkey,
|
||||||
|
...template,
|
||||||
|
created_at: event.created_at,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Test whether the value is a Nostr ID. */
|
/** Test whether the value is a Nostr ID. */
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { TTLCache, z } from '@/deps.ts';
|
import { TTLCache, z } from '@/deps.ts';
|
||||||
import { Time } from '@/utils/time.ts';
|
import { Time } from '@/utils/time.ts';
|
||||||
|
import { fetchWorker } from '@/workers/fetch.ts';
|
||||||
|
|
||||||
const nip05Cache = new TTLCache<string, Promise<string | null>>({ ttl: Time.hours(1), max: 5000 });
|
const nip05Cache = new TTLCache<string, Promise<string | null>>({ ttl: Time.hours(1), max: 5000 });
|
||||||
|
|
||||||
|
@ -19,7 +20,7 @@ async function lookup(value: string, opts: LookupOpts = {}): Promise<string | nu
|
||||||
const [_, name = '_', domain] = match;
|
const [_, name = '_', domain] = match;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`https://${domain}/.well-known/nostr.json?name=${name}`, {
|
const res = await fetchWorker(`https://${domain}/.well-known/nostr.json?name=${name}`, {
|
||||||
signal: AbortSignal.timeout(timeout),
|
signal: AbortSignal.timeout(timeout),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { type Event, type EventTemplate, nip13 } from '@/deps.ts';
|
import { type Event, type EventTemplate, nip13, type VerifiedEvent } from '@/deps.ts';
|
||||||
import { decode64Schema, jsonSchema } from '@/schema.ts';
|
import { decode64Schema, jsonSchema } from '@/schema.ts';
|
||||||
import { signedEventSchema } from '@/schemas/nostr.ts';
|
import { signedEventSchema } from '@/schemas/nostr.ts';
|
||||||
import { eventAge, findTag, nostrNow, sha256 } from '@/utils.ts';
|
import { eventAge, findTag, nostrNow, sha256 } from '@/utils.ts';
|
||||||
|
@ -32,7 +32,7 @@ function validateAuthEvent(req: Request, event: Event, opts: ParseAuthRequestOpt
|
||||||
const { maxAge = Time.minutes(1), validatePayload = true, pow = 0 } = opts;
|
const { maxAge = Time.minutes(1), validatePayload = true, pow = 0 } = opts;
|
||||||
|
|
||||||
const schema = signedEventSchema
|
const schema = signedEventSchema
|
||||||
.refine((event): event is Event<27235> => event.kind === 27235, 'Event must be kind 27235')
|
.refine((event): event is VerifiedEvent<27235> => event.kind === 27235, 'Event must be kind 27235')
|
||||||
.refine((event) => eventAge(event) < maxAge, 'Event expired')
|
.refine((event) => eventAge(event) < maxAge, 'Event expired')
|
||||||
.refine((event) => tagValue(event, 'method') === req.method, 'Event method does not match HTTP request method')
|
.refine((event) => tagValue(event, 'method') === req.method, 'Event method does not match HTTP request method')
|
||||||
.refine((event) => tagValue(event, 'u') === req.url, 'Event URL does not match request URL')
|
.refine((event) => tagValue(event, 'u') === req.url, 'Event URL does not match request URL')
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { TTLCache, unfurl } from '@/deps.ts';
|
import { TTLCache, unfurl } from '@/deps.ts';
|
||||||
import { Time } from '@/utils/time.ts';
|
import { Time } from '@/utils/time.ts';
|
||||||
|
import { fetchWorker } from '@/workers/fetch.ts';
|
||||||
|
|
||||||
interface PreviewCard {
|
interface PreviewCard {
|
||||||
url: string;
|
url: string;
|
||||||
|
@ -22,7 +23,7 @@ async function unfurlCard(url: string, signal: AbortSignal): Promise<PreviewCard
|
||||||
console.log(`Unfurling ${url}...`);
|
console.log(`Unfurling ${url}...`);
|
||||||
try {
|
try {
|
||||||
const result = await unfurl(url, {
|
const result = await unfurl(url, {
|
||||||
fetch: (url) => fetch(url, { signal }),
|
fetch: (url) => fetchWorker(url, { signal }),
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { assert, assertRejects } from '@/deps-test.ts';
|
||||||
|
|
||||||
|
import { fetchWorker } from './fetch.ts';
|
||||||
|
|
||||||
|
await sleep(2000);
|
||||||
|
|
||||||
|
Deno.test({
|
||||||
|
name: 'fetchWorker',
|
||||||
|
async fn() {
|
||||||
|
const response = await fetchWorker('https://example.com');
|
||||||
|
const text = await response.text();
|
||||||
|
assert(text.includes('Example Domain'));
|
||||||
|
},
|
||||||
|
sanitizeResources: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test({
|
||||||
|
name: 'fetchWorker with AbortSignal',
|
||||||
|
async fn() {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const signal = controller.signal;
|
||||||
|
|
||||||
|
setTimeout(() => controller.abort(), 100);
|
||||||
|
assertRejects(() => fetchWorker('http://httpbin.org/delay/10', { signal }));
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
signal.addEventListener('abort', () => resolve(), { once: true });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
sanitizeResources: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
function sleep(ms: number) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { Comlink } from '@/deps.ts';
|
||||||
|
|
||||||
|
import './handlers/abortsignal.ts';
|
||||||
|
|
||||||
|
import type { FetchWorker } from './fetch.worker.ts';
|
||||||
|
|
||||||
|
const _worker = Comlink.wrap<typeof FetchWorker>(
|
||||||
|
new Worker(
|
||||||
|
new URL('./fetch.worker.ts', import.meta.url),
|
||||||
|
{ type: 'module' },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch implementation with a Web Worker.
|
||||||
|
* Calling this performs the fetch in a separate CPU thread so it doesn't block the main thread.
|
||||||
|
*/
|
||||||
|
const fetchWorker: typeof fetch = async (...args) => {
|
||||||
|
const [url, init] = serializeFetchArgs(args);
|
||||||
|
const { body, signal, ...rest } = init;
|
||||||
|
const result = await _worker.fetch(url, { ...rest, body: await prepareBodyForWorker(body) }, signal);
|
||||||
|
return new Response(...result);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Take arguments to `fetch`, and turn them into something we can send over Comlink. */
|
||||||
|
function serializeFetchArgs(args: Parameters<typeof fetch>): [string, RequestInit] {
|
||||||
|
const request = normalizeRequest(args);
|
||||||
|
const init = requestToInit(request);
|
||||||
|
return [request.url, init];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get a `Request` object from arguments to `fetch`. */
|
||||||
|
function normalizeRequest(args: Parameters<typeof fetch>): Request {
|
||||||
|
return new Request(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the body as a type we can transfer over Web Workers. */
|
||||||
|
async function prepareBodyForWorker(
|
||||||
|
body: BodyInit | undefined | null,
|
||||||
|
): Promise<ArrayBuffer | Blob | string | undefined | null> {
|
||||||
|
if (!body || typeof body === 'string' || body instanceof ArrayBuffer || body instanceof Blob) {
|
||||||
|
return body;
|
||||||
|
} else {
|
||||||
|
const response = new Response(body);
|
||||||
|
return await response.arrayBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a `Request` object into its serialized `RequestInit` format.
|
||||||
|
* `RequestInit` is a subset of `Request`, just lacking helper methods like `json()`,
|
||||||
|
* making it easier to serialize (exceptions: `body` and `signal`).
|
||||||
|
*/
|
||||||
|
function requestToInit(request: Request): RequestInit {
|
||||||
|
return {
|
||||||
|
method: request.method,
|
||||||
|
headers: [...request.headers.entries()],
|
||||||
|
body: request.body,
|
||||||
|
referrer: request.referrer,
|
||||||
|
referrerPolicy: request.referrerPolicy,
|
||||||
|
mode: request.mode,
|
||||||
|
credentials: request.credentials,
|
||||||
|
cache: request.cache,
|
||||||
|
redirect: request.redirect,
|
||||||
|
integrity: request.integrity,
|
||||||
|
keepalive: request.keepalive,
|
||||||
|
signal: request.signal,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { fetchWorker };
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { Comlink } from '@/deps.ts';
|
||||||
|
|
||||||
|
import './handlers/abortsignal.ts';
|
||||||
|
|
||||||
|
export const FetchWorker = {
|
||||||
|
async fetch(
|
||||||
|
url: string,
|
||||||
|
init: Omit<RequestInit, 'signal'>,
|
||||||
|
signal: AbortSignal | null | undefined,
|
||||||
|
): Promise<[BodyInit, ResponseInit]> {
|
||||||
|
const response = await fetch(url, { ...init, signal });
|
||||||
|
return [
|
||||||
|
await response.arrayBuffer(),
|
||||||
|
{
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
headers: [...response.headers.entries()],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Comlink.expose(FetchWorker);
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { Comlink } from '@/deps.ts';
|
||||||
|
|
||||||
|
const signalFinalizers = new FinalizationRegistry((port: MessagePort) => {
|
||||||
|
port.postMessage(null);
|
||||||
|
port.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
Comlink.transferHandlers.set('abortsignal', {
|
||||||
|
canHandle(value) {
|
||||||
|
return value instanceof AbortSignal || value?.constructor?.name === 'AbortSignal';
|
||||||
|
},
|
||||||
|
serialize(signal) {
|
||||||
|
if (signal.aborted) {
|
||||||
|
return [{ aborted: true }];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { port1, port2 } = new MessageChannel();
|
||||||
|
signal.addEventListener(
|
||||||
|
'abort',
|
||||||
|
() => port1.postMessage({ reason: signal.reason }),
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
signalFinalizers?.register(signal, port1);
|
||||||
|
|
||||||
|
return [{ aborted: false, port: port2 }, [port2]];
|
||||||
|
},
|
||||||
|
deserialize({ aborted, port }) {
|
||||||
|
if (aborted || !port) {
|
||||||
|
return AbortSignal.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctrl = new AbortController();
|
||||||
|
|
||||||
|
port.addEventListener('message', (ev) => {
|
||||||
|
if (ev.data && 'reason' in ev.data) {
|
||||||
|
ctrl.abort(ev.data.reason);
|
||||||
|
}
|
||||||
|
port.close();
|
||||||
|
}, { once: true });
|
||||||
|
|
||||||
|
port.start();
|
||||||
|
|
||||||
|
return ctrl.signal;
|
||||||
|
},
|
||||||
|
} as Comlink.TransferHandler<AbortSignal, { aborted: boolean; port?: MessagePort }>);
|
Loading…
Reference in New Issue