@volter/world-core 3.0.163 → 3.0.164

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.
@@ -11,7 +11,7 @@ import { parseExactJson, compareExactNumbers, exactInteger, isExactIntegerValue
11
11
  import { applyTwinWrite, applyTwinWriteAtomic, applyTwinWrites, twinResources, type TwinResource } from './serve.ts';
12
12
  import { copyResource, foldLocalWrite, heldSubject, ownFields, parentStamp, subjectHistory, treeChangesSince, treeResources, treeStamp, type Subject } from './log.ts';
13
13
  import { performsAtHeadNow } from './head.ts';
14
- import { getActiveWorldStore } from './world-store.ts';
14
+ import { getActiveWorldStore, type WorldStore } from './world-store.ts';
15
15
  import { getActiveBlobStore } from './blob-store.ts';
16
16
  import { worldPaths } from './storage.ts';
17
17
  import { GitObjectStore } from './git/objects.ts';
@@ -937,7 +937,7 @@ export type CoreScope = {
937
937
  root?: string; clock?: () => string; database?: string; readOnly?: boolean;
938
938
  /** `semantics/tenant.ts`: the tenant the caller acts in (the manifest's `tenant`), or undefined for none */
939
939
  tenant?: (ctx: HandlerContext) => string | undefined | Promise<string | undefined>;
940
- /** the application a walk stands in for: what the vendor asks it (`ctx.ask`) is answered by it (events.ts) */
940
+ /** the application a walk stands in for: vendor requests and declared events are answered by it (events.ts) */
941
941
  application?: ApplicationStandIn;
942
942
  };
943
943
 
@@ -1138,9 +1138,9 @@ function mintId(m: DerivedManifest, resource: string, root: string | undefined,
1138
1138
  * increment. One space over every snowflake resource of the vendor, as its one generator's: the instant's first, or one
1139
1139
  * past the greatest the World holds when that is later (a frozen clock, or several ids in one millisecond), so ids
1140
1140
  * rise with time and never repeat. */
1141
- /** The last snowflake minted per vendor and World in this process: two mints before the first is written (a message
1141
+ /** The last snowflake minted per store, vendor and World: two mints before the first is written (a message
1142
1142
  * and its attachment) must not meet. */
1143
- const lastSnowflake = new Map<string, bigint>();
1143
+ const lastSnowflake = new WeakMap<WorldStore, Map<string, bigint>>();
1144
1144
  function mintSnowflake(m: DerivedManifest, epoch: bigint, root: string | undefined, at: string, snapshot?: readonly TwinResource[]): string {
1145
1145
  const ms = BigInt(Date.parse(at) || 0);
1146
1146
  const base = ms > epoch ? (ms - epoch) << 22n : 0n;
@@ -1149,11 +1149,13 @@ function mintSnowflake(m: DerivedManifest, epoch: bigint, root: string | undefin
1149
1149
  if (!/^\{snowflake:\d+\}$/.test(decl.ids ?? m.ids.template)) continue;
1150
1150
  for (const r of mintRows(m, name, root, snapshot)) if (/^\d+$/.test(r.id)) { const v = BigInt(r.id); if (v > max) max = v; }
1151
1151
  }
1152
+ const store = getActiveWorldStore();
1153
+ const reservations = lastSnowflake.get(store) ?? lastSnowflake.set(store, new Map()).get(store)!;
1152
1154
  const key = `${m.service}\0${root ?? ''}`;
1153
- const last = lastSnowflake.get(key) ?? -1n;
1155
+ const last = reservations.get(key) ?? -1n;
1154
1156
  const floor = max > last ? max : last;
1155
1157
  const id = base > floor ? base : floor + 1n;
1156
- lastSnowflake.set(key, id);
1158
+ reservations.set(key, id);
1157
1159
  return String(id);
1158
1160
  }
1159
1161
 
@@ -1452,7 +1454,7 @@ async function finishWrite(m: DerivedManifest, call: DerivedCall, resource: stri
1452
1454
  const endpoint = (kind: import('./events.ts').EndpointsDecl): import('./events.ts').EndpointsDecl => ({ ...kind, storedAs: storedType(m, kind.storedAs), ...(kind.joins ? { joins: Object.fromEntries(Object.entries(kind.joins).map(([name, join]) => [name, { ...join, storedAs: storedType(m, join.storedAs) }])) } : {}) });
1453
1455
  const events = { ...m.events, ...(m.events.store ? { store: storedType(m, m.events.store) } : {}), endpoints: 'storedAs' in m.events.endpoints ? endpoint(m.events.endpoints) : m.events.endpoints.map(endpoint) };
1454
1456
  const write = { operation, storedType: m.resources[resource]?.storedAs ?? resource, body, ...(root !== undefined ? { root } : {}), occurredAt, request: request.clone() };
1455
- await deliverEvents(m.service, events, write, async (type) => (events.render ? events.render(await context(), write, type) : body), undefined, events.values ? async (type) => events.values!(await context(), write, type) : undefined);
1457
+ await deliverEvents(m.service, events, write, async (type) => (events.render ? events.render(await context(), write, type) : body), undefined, events.values ? async (type) => events.values!(await context(), write, type) : undefined, scopeOfCall.get(call)?.application);
1456
1458
  }
1457
1459
  if (m.sockets?.length) socketWrite(m.service, root, { operation, storedType: m.resources[resource]?.storedAs ?? resource, body, ...(root !== undefined ? { root } : {}), occurredAt, request: request.clone() });
1458
1460
  // a read held until the World writes (held-reads.ts) hears it
@@ -583,8 +583,8 @@ function refreshOf(units: Unit[], service: string): NonNullable<StateSystemAdapt
583
583
  const sent = asked.length ? `${target}${target.includes('?') ? '&' : '?'}${new URLSearchParams(asked)}` : target;
584
584
  const answer = await execute({ method: op.method.toUpperCase(), path: sentTo(m, sent, subject), ...onLane, headers: fixedHeaders(op) });
585
585
  // a door's subject the vendor does not show the root (another bot's private chat) stays as the World holds it
586
- if (!rooted.has(key) && answer.status >= 400 && answer.status !== 429) continue;
587
586
  if (answer.status === 401) throw new Error(`401: the vendor refused the root's credential (${op.method.toUpperCase()} ${path}); nothing folded`);
587
+ if (!rooted.has(key) && answer.status >= 400 && answer.status !== 429) continue;
588
588
  if (answer.status === 403) { unread.push({ type, path, status: 403 }); refused = true; break; }
589
589
  if (answer.status === 429) throw new Error(`429: rate limited${answer.headers['retry-after'] ? `, retry after ${answer.headers['retry-after']}s` : ''}`);
590
590
  read.push(String(subject.id));
@@ -592,7 +592,15 @@ function refreshOf(units: Unit[], service: string): NonNullable<StateSystemAdapt
592
592
  // the vendor's answer for a subject it no longer has: a 404, or the scope's `gone` (Telegram's 400 `chat not found`)
593
593
  if (answer.status === 404 || (scope.gone && answer.status === scope.gone.status && refusalWords(m, parse(answer)).includes(scope.gone.says))) continue;
594
594
  if (answer.status >= 400 || refusedIn(m, parse(answer))) throw new Error(`${m.vendor}: ${resource} refresh ${op.id} (${op.method.toUpperCase()} ${op.path}): ${answer.status}: ${refusalWords(m, parse(answer))}`);
595
- const full = object(op.answers?.key ? at(parse(answer), op.answers.key) : parse(answer)) ?? {};
595
+ const parsed = parse(answer);
596
+ let full: Row;
597
+ if (op.answers?.list) {
598
+ const items = itemsOf(op, parsed);
599
+ const matching = items.find((item) => (scopeIdOf(scope, item) ?? storedIdOf(decl, item)) === vendorId);
600
+ if (!matching && items.length) throw new Error(`${m.vendor}: ${resource} refresh ${op.id} omitted the requested subject; nothing folded`);
601
+ if (!matching) continue;
602
+ full = matching;
603
+ } else full = object(op.answers?.key ? at(parsed, op.answers.key) : parsed) ?? {};
596
604
  const local = (m as { worldFields?: Record<string, Record<string, unknown>> }).worldFields?.[resource] ?? {};
597
605
  const fields = Object.fromEntries(Object.entries(full).filter(([k]) => !(k in local)));
598
606
  if (decl.parent) {
package/src/events.ts CHANGED
@@ -7,11 +7,12 @@
7
7
  // Imports stay free of side effects: the delivery's transport (Node's http, the World's egress policy) is loaded when a
8
8
  // delivery is made, never when a browser client imports the kernel.
9
9
  import { createHash, createHmac } from 'node:crypto';
10
+ import { resolve } from 'node:path';
10
11
  import { applyTwinWrite, applyTwinWriteAtomic, twinResources } from './serve.ts';
11
12
  import { equalSecrets as same, ksuidFrom } from './signing.ts';
12
13
  import { deliveryTraceHeaders } from './trace-context.ts';
13
14
  import { sendRecordedEvent } from './event-delivery.ts';
14
- import { getActiveWorldStore, withWorldStore } from './world-store.ts';
15
+ import { getActiveWorldStore, withWorldStore, type WorldStore } from './world-store.ts';
15
16
  import { worldPaths } from './storage.ts';
16
17
 
17
18
  /** How the vendor signs a delivery. */
@@ -306,6 +307,26 @@ const worldTransport: EventTransport = async (url, body, headers) => {
306
307
  try { return ((await appFetch(url, { method: 'POST', headers: { 'content-type': 'application/json', ...headers }, body })) as { status: number }).status; } catch { return 0; }
307
308
  };
308
309
 
310
+ const pendingDeliveries = new WeakMap<WorldStore, Map<string, Set<Promise<void>>>>();
311
+
312
+ function forgetDeliveries(store: WorldStore, root: string, deliveries: Iterable<Promise<void>>): void {
313
+ const roots = pendingDeliveries.get(store), pending = roots?.get(root);
314
+ if (!pending) return;
315
+ for (const delivery of deliveries) pending.delete(delivery);
316
+ if (!pending.size) roots!.delete(root);
317
+ if (!roots!.size) pendingDeliveries.delete(store);
318
+ }
319
+
320
+ /** Await already-scheduled first dispatches and consume their retained persistence failures. */
321
+ export async function settleEventDeliveries(root: string): Promise<void> {
322
+ const store = getActiveWorldStore(), path = resolve(root);
323
+ const deliveries = [...(pendingDeliveries.get(store)?.get(path) ?? [])];
324
+ const outcomes = await Promise.allSettled(deliveries);
325
+ forgetDeliveries(store, path, deliveries);
326
+ const failures = outcomes.flatMap((outcome) => outcome.status === 'rejected' ? [outcome.reason] : []);
327
+ if (failures.length) throw new AggregateError(failures, 'Declared-event dispatch failed');
328
+ }
329
+
309
330
  /** Who watches signed packets before dispatch (a walk reading each as its receiver would). This is not evidence of
310
331
  * transport or acceptance. Set and restored by the walk, as its World store is; unset, nothing watches. */
311
332
  export type DeliveryObserver = (delivery: EventDelivery & { service: string }) => void;
@@ -321,13 +342,14 @@ export function setDeliveryObserver(observer: DeliveryObserver | undefined): Del
321
342
  * (`unreachable`), or it did not answer in time (`timeout`). */
322
343
  export type ApplicationAnswer = { status: number; headers: Record<string, string>; body: string; missed?: 'timeout' | 'unreachable' };
323
344
 
324
- /** Who answers what the vendor asks the application (`ctx.ask`) before the World's application route does, given to a
345
+ /** Who answers a vendor's requests to the application before the World's application route does, given to a
325
346
  * pack's fetch (`PackFetchOptions.application`) and belonging to that fetch alone: an answer it gives is the
326
347
  * application's; `'unreachable'` is a question nobody answers (a walk's life, a sealed World, takes no other); undefined
327
348
  * is a question it does not take, which goes on as any other does: the application route, the World's egress rule, the
328
349
  * network. A walk's stand-in answers every question (its life's answers, else unreachable); a hosted World's router
329
350
  * answers only what it can answer truthfully (WorldDoors.askInWorld) and leaves the rest. `bodyError` models a
330
- * disconnect after the response headers; a caller needing the complete body receives no complete answer. */
351
+ * disconnect after the response headers; a caller needing the complete body receives no complete answer, while an
352
+ * event dispatch acknowledges the response status without reading its body. */
331
353
  export type ApplicationStandIn = (url: string, init: RequestInit) => Promise<{ status: number; headers?: Record<string, string>; body: string; bodyError?: string } | 'unreachable' | undefined>;
332
354
 
333
355
  /** A message the vendor sends the application's server and decides by its answer (Stripe's real-time authorization
@@ -367,10 +389,15 @@ export async function deliverEvents(
367
389
  data: (type: string) => Promise<Record<string, unknown>>,
368
390
  transport: EventTransport = worldTransport,
369
391
  values?: (type: string) => Promise<Record<string, unknown>>,
392
+ application?: ApplicationStandIn,
370
393
  ): Promise<EventDelivery[]> {
371
394
  if (write.storedType === decl.store) return [];
395
+ const dispatch: EventTransport = application ? async (url, body, headers) => {
396
+ const answer = await application(url, { method: 'POST', headers: { 'content-type': 'application/json', ...headers }, body });
397
+ return answer === undefined ? transport(url, body, headers) : answer === 'unreachable' ? 0 : answer.status;
398
+ } : transport;
372
399
  const sent: EventDelivery[] = [];
373
- for (const type of eventTypesOf(decl, write.operation)) sent.push(...(await deliverEventType(service, decl, write, data, transport, values, type)));
400
+ for (const type of eventTypesOf(decl, write.operation)) sent.push(...(await deliverEventType(service, decl, write, data, dispatch, values, type)));
374
401
  return sent;
375
402
  }
376
403
 
@@ -466,10 +493,28 @@ async function deliverEventType(
466
493
  // active before it runs. Transport completion writes to the same retained state.
467
494
  const store = getActiveWorldStore(), deliveryRoot = worldPaths(service, write.root).root;
468
495
  for (const d of sent) deliveryObserver?.({ ...d, service });
469
- for (const packet of recorded) setTimeout(() => {
470
- // Persistence failure leaves a queued/sending claim for explicit recovery;
471
- // it must not fail the already-committed vendor write or cause a blind resend.
472
- void withWorldStore(store, () => sendRecordedEvent(service, packet.type, packet.id, deliveryRoot, transport, trace)).catch(() => {});
473
- }, 0);
496
+ const roots = pendingDeliveries.get(store) ?? new Map<string, Set<Promise<void>>>();
497
+ pendingDeliveries.set(store, roots);
498
+ const pending = roots.get(deliveryRoot) ?? new Set<Promise<void>>();
499
+ roots.set(deliveryRoot, pending);
500
+ for (const packet of recorded) {
501
+ let finish!: () => void;
502
+ let fail!: (reason: unknown) => void;
503
+ const dispatched = new Promise<void>((done, reject) => { finish = done; fail = reject; });
504
+ // Live writes return before dispatch; an explicit settlement reports persistence failures.
505
+ void dispatched.catch(() => {});
506
+ pending.add(dispatched);
507
+ setTimeout(async () => {
508
+ try {
509
+ await withWorldStore(store, () => sendRecordedEvent(service, packet.type, packet.id, deliveryRoot, transport, trace));
510
+ finish();
511
+ forgetDeliveries(store, deliveryRoot, [dispatched]);
512
+ } catch (error) {
513
+ // Keep the rejection until explicit settlement consumes it. The queued/sending claim remains for
514
+ // native recovery; this must not fail the already-committed vendor write or cause a blind resend.
515
+ fail(error);
516
+ }
517
+ }, 0);
518
+ }
474
519
  return sent;
475
520
  }
@@ -25,10 +25,16 @@ export function fileResponse(path: string, init?: ResponseInit): Response {
25
25
  * Stored bytes as a response, honouring a single `Range: bytes=` as RFC 9110 §14 has it: every satisfiable range is a
26
26
  * 206 with its Content-Range, `bytes=0-` (the whole body) included; a range that does not parse, a first-byte-pos past
27
27
  * its last-byte-pos (`bytes=5-2`) included, is ignored and the whole body is a 200; a well-formed range that starts past
28
- * the end (any range of an empty body) is a 416. `nosniff` stops a browser second-guessing the content type the vendor gave.
28
+ * the end (any range of an empty body) is a 416. Lazy sources supply inclusive ranges and stream in bounded reads.
29
+ * `nosniff` stops a browser second-guessing the content type the vendor gave.
29
30
  */
30
- export function bytesResponse(request: Request, bytes: Uint8Array, contentType: string, cacheControl = 'public, max-age=604800, immutable'): Response {
31
- const total = bytes.length;
31
+ export function bytesResponse(
32
+ request: Request,
33
+ bytes: Uint8Array | { size: number; range(start: number, end: number): Promise<Uint8Array | null> },
34
+ contentType: string,
35
+ cacheControl = 'public, max-age=604800, immutable',
36
+ ): Response {
37
+ const total = bytes instanceof Uint8Array ? bytes.length : bytes.size;
32
38
  const base = { 'content-type': contentType, 'x-content-type-options': 'nosniff', 'accept-ranges': 'bytes', 'cache-control': cacheControl };
33
39
  const range = /^bytes=(\d*)-(\d*)$/.exec((request.headers.get('range') ?? '').trim());
34
40
  let start = 0;
@@ -44,9 +50,25 @@ export function bytesResponse(request: Request, bytes: Uint8Array, contentType:
44
50
  // every satisfiable range is answered as one, the whole body's `bytes=0-` included: a media player (Safari's) reads a
45
51
  // 206 with its Content-Range as the server taking ranges, and a 200 as one it cannot seek in
46
52
  const partial = valid;
47
- const body = bytes.slice(start, end + 1);
48
- return new Response(request.method === 'HEAD' ? null : new Blob([body]), {
53
+ let body: BodyInit | null = null;
54
+ if (request.method !== 'HEAD') {
55
+ if (bytes instanceof Uint8Array) body = new Blob([bytes.slice(start, end + 1)]);
56
+ else {
57
+ let at = start;
58
+ body = new ReadableStream<Uint8Array>({
59
+ async pull(controller) {
60
+ if (at > end) { controller.close(); return; }
61
+ const last = Math.min(at + 8 * 1024 * 1024 - 1, end);
62
+ const chunk = await bytes.range(at, last);
63
+ if (!chunk) { controller.error(new Error('stored bytes are gone')); return; }
64
+ controller.enqueue(chunk);
65
+ at = last + 1;
66
+ },
67
+ });
68
+ }
69
+ }
70
+ return new Response(body, {
49
71
  status: partial ? 206 : 200,
50
- headers: { ...base, 'content-length': String(body.length), ...(partial ? { 'content-range': `bytes ${start}-${end}/${total}` } : {}) },
72
+ headers: { ...base, 'content-length': String(end + 1 - start), ...(partial ? { 'content-range': `bytes ${start}-${end}/${total}` } : {}) },
51
73
  });
52
74
  }
package/src/index.ts CHANGED
@@ -89,6 +89,7 @@ export * as protobuf from './protobuf.ts';
89
89
  export * as grpc from './grpc-wire.ts';
90
90
  export { acceptH2Connection, type H2Handler, type Http1Handler } from './h2.ts';
91
91
  export { placeholderPng } from './placeholder-image.ts';
92
+ export { imageDimensions, readImageHeader, readMp4Header } from './media-header.ts';
92
93
  export { presignSigV4, signSigV4, verifySigV4, type SigV4Verdict } from './sigv4.ts';
93
94
  export * as redis from './redis/index.ts';
94
95
  export * as mysql from './mysql/index.ts';
@@ -102,7 +103,7 @@ export type { PackScenarioAdapter, ScenarioDecision, ScenarioDocument, ScenarioE
102
103
 
103
104
  export { verifyAttestationBundle } from './attestation.ts';
104
105
 
105
- export { browserSessionToken, startBrowserSession, endBrowserSession } from './browser-session.ts';
106
+ export { cookieOf, signedIn, browserSessionToken, startBrowserSession, endBrowserSession } from './browser-session.ts';
106
107
 
107
108
  export { readHostPort } from './host-port.ts';
108
109
  export { assertBrowserName, browserOrigin, validateBrowserState, validateBrowserStorageState, validateBrowserContext, browserAcceptLanguage, cookieMatchesHost, type BrowserCookie, type BrowserState, type BrowserStorageState, type BrowserContext, type BrowserIndexedDB } from './browser-state.ts';
@@ -254,10 +254,10 @@ export function managedDatabase(url: string, options: { readOnly?: boolean } = {
254
254
  * walk/journey-kit.ts), its statements sent as its client sends them: a text alone as one simple Query (a migration
255
255
  * file, a `BEGIN`), a text with values as one extended-protocol statement, the values bind parameters. A transaction it
256
256
  * opens stays open across its statements until it ends it, as on the application's own connection. Each statement's
257
- * answer is its results in Postgres's text format, untyped. */
258
- export type DatabaseSession = { query(text: string, values?: ReadonlyArray<string | null>): Promise<StatementResult[]>; end(): Promise<void> };
257
+ * answer is its results in Postgres's text format, with native column type OIDs retained beside the values. */
258
+ export type DatabaseSession = { query(text: string, values?: ReadonlyArray<string | null>): Promise<Array<StatementResult & { fieldTypes: number[] }>>; end(): Promise<void> };
259
259
 
260
- type PgResult = { command?: string | null; rowCount?: number | null; fields?: Array<{ name: string }>; rows?: Array<Array<string | null>> };
260
+ type PgResult = { command?: string | null; rowCount?: number | null; fields?: Array<{ name: string; dataTypeID: number }>; rows?: Array<Array<string | null>> };
261
261
 
262
262
  /** The World's Postgres at `url` as one connection held until `end()`. */
263
263
  export async function databaseSession(url: string): Promise<DatabaseSession> {
@@ -277,6 +277,7 @@ export async function databaseSession(url: string): Promise<DatabaseSession> {
277
277
  const answer = (await (client.query({ text, ...(values === undefined ? {} : { values }), rowMode: 'array', types: raw }) as Promise<PgResult | PgResult[]>));
278
278
  return (Array.isArray(answer) ? answer : [answer]).map((r) => ({
279
279
  fields: (r.fields ?? []).map((f) => f.name),
280
+ fieldTypes: (r.fields ?? []).map((f) => f.dataTypeID),
280
281
  rows: r.rows ?? [],
281
282
  command: [r.command ?? '', r.command === 'INSERT' ? '0' : '', r.rowCount ?? ''].filter((part) => part !== '').join(' '),
282
283
  }));
@@ -0,0 +1,108 @@
1
+ // File-format headers only: no decoding, vendor limits, processing or rendition policy.
2
+ type ImageType = 'image/png' | 'image/gif' | 'image/jpeg' | 'image/webp';
3
+ type Dimensions = { width: number; height: number };
4
+ type ImageHeader = Dimensions & { jpeg?: { frameEnd: number; nonMarkerBytes: boolean; standaloneMarkers: boolean; fillBytes: boolean; shortSegments: boolean } };
5
+
6
+ const tag = (bytes: Uint8Array, at: number): string => String.fromCharCode(...bytes.subarray(at, at + 4));
7
+
8
+ /** Dimensions in a caller-declared image format, or undefined for an incomplete header. */
9
+ export function imageDimensions(bytes: Uint8Array, type: ImageType): ImageHeader | undefined {
10
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
11
+ if (type === 'image/png') return bytes.length >= 24 ? { width: view.getUint32(16), height: view.getUint32(20) } : undefined;
12
+ if (type === 'image/gif') return bytes.length >= 10 ? { width: view.getUint16(6, true), height: view.getUint16(8, true) } : undefined;
13
+ if (type === 'image/webp') {
14
+ if (bytes.length < 30) return undefined;
15
+ const chunk = tag(bytes, 12);
16
+ if (chunk === 'VP8 ') return { width: view.getUint16(26, true) & 0x3fff, height: view.getUint16(28, true) & 0x3fff };
17
+ if (chunk === 'VP8L') {
18
+ const bits = view.getUint32(21, true);
19
+ return { width: 1 + (bits & 0x3fff), height: 1 + ((bits >>> 14) & 0x3fff) };
20
+ }
21
+ if (chunk === 'VP8X') {
22
+ const u24 = (at: number): number => bytes[at]! | (bytes[at + 1]! << 8) | (bytes[at + 2]! << 16);
23
+ return { width: 1 + u24(24), height: 1 + u24(27) };
24
+ }
25
+ return undefined;
26
+ }
27
+ let at = 2;
28
+ const jpeg = { frameEnd: 0, nonMarkerBytes: false, standaloneMarkers: false, fillBytes: false, shortSegments: false };
29
+ while (at + 4 <= bytes.length) {
30
+ if (bytes[at] !== 0xff) { jpeg.nonMarkerBytes = true; at += 1; continue; }
31
+ const marker = bytes[at + 1]!;
32
+ if (marker === 0xff) { jpeg.fillBytes = true; at += 1; continue; }
33
+ if (marker === 0xd8 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) { jpeg.standaloneMarkers = true; at += 2; continue; }
34
+ const length = view.getUint16(at + 2);
35
+ if (length < 2) jpeg.shortSegments = true;
36
+ if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
37
+ return at + 9 <= bytes.length ? { width: view.getUint16(at + 7), height: view.getUint16(at + 5), jpeg: { ...jpeg, frameEnd: at + 9 } } : undefined;
38
+ }
39
+ if (marker === 0xda) return undefined;
40
+ at += 2 + length;
41
+ }
42
+ return undefined;
43
+ }
44
+
45
+ /** An image's signature and dimensions, without deciding whether a vendor accepts that format or size. */
46
+ export function readImageHeader(bytes: Uint8Array): (ImageHeader & { type: ImageType }) | undefined {
47
+ let type: ImageType;
48
+ if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) type = 'image/png';
49
+ else if (tag(bytes, 0).startsWith('GIF')) type = 'image/gif';
50
+ else if (bytes[0] === 0xff && bytes[1] === 0xd8) type = 'image/jpeg';
51
+ else if (tag(bytes, 0) === 'RIFF' && tag(bytes, 8) === 'WEBP') type = 'image/webp';
52
+ else return undefined;
53
+ const dimensions = imageDimensions(bytes, type);
54
+ return dimensions ? { type, ...dimensions } : undefined;
55
+ }
56
+
57
+ /** ISO BMFF's bounded top-level boxes and movie headers, with raw duration and 16.16 track dimensions. */
58
+ export function readMp4Header(bytes: Uint8Array): {
59
+ fileType: boolean;
60
+ hasMediaData: boolean;
61
+ movies: Array<{ headers: Array<{ timescale: number; duration: number; payloadSize: number }>; tracks: Dimensions[] }>;
62
+ } {
63
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
64
+ function* boxes(from: number, to: number): Generator<{ type: string; start: number; end: number }> {
65
+ let at = from;
66
+ while (at + 8 <= to) {
67
+ let size = view.getUint32(at);
68
+ const type = tag(bytes, at + 4);
69
+ let header = 8;
70
+ if (size === 1) {
71
+ if (at + 16 > to) return;
72
+ size = Number(view.getBigUint64(at + 8));
73
+ header = 16;
74
+ }
75
+ if (size === 0) size = to - at;
76
+ if (!Number.isSafeInteger(size) || size < header || size > to - at) return;
77
+ yield { type, start: at + header, end: at + size };
78
+ at += size;
79
+ }
80
+ }
81
+ const result: ReturnType<typeof readMp4Header> = { fileType: bytes.length >= 8 && tag(bytes, 4) === 'ftyp', hasMediaData: false, movies: [] };
82
+ for (const box of boxes(0, bytes.length)) {
83
+ if (box.type === 'mdat' && box.end > box.start) result.hasMediaData = true;
84
+ if (box.type !== 'moov') continue;
85
+ const movie: (typeof result.movies)[number] = { headers: [], tracks: [] };
86
+ result.movies.push(movie);
87
+ for (const child of boxes(box.start, box.end)) {
88
+ const { type, start, end } = child;
89
+ if (type === 'mvhd' && start < end) {
90
+ const version = bytes[start];
91
+ const payloadSize = end - start;
92
+ if (version === 0 && payloadSize >= 20) movie.headers.push({ timescale: view.getUint32(start + 12), duration: view.getUint32(start + 16), payloadSize });
93
+ if (version === 1 && payloadSize >= 32) movie.headers.push({ timescale: view.getUint32(start + 20), duration: Number(view.getBigUint64(start + 24)), payloadSize });
94
+ }
95
+ if (type !== 'trak') continue;
96
+ for (const track of boxes(start, end)) {
97
+ if (track.type !== 'tkhd' || track.start >= track.end) continue;
98
+ const version = bytes[track.start];
99
+ const at = version === 1 ? track.start + 88 : version === 0 ? track.start + 76 : track.end;
100
+ if (at + 8 > track.end) continue;
101
+ const width = view.getUint32(at) / 65536;
102
+ const height = view.getUint32(at + 4) / 65536;
103
+ movie.tracks.push({ width, height });
104
+ }
105
+ }
106
+ }
107
+ return result;
108
+ }