@sorisdk/matcher 0.6.1 → 0.6.3

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.
package/README.md CHANGED
@@ -67,6 +67,26 @@ const session = new MatcherSession({
67
67
  - `string`
68
68
  - `URL`
69
69
 
70
+ Pack loading is bounded and transactional. Browser sources are limited to
71
+ 64 MiB by default: a valid oversized `Content-Length` is rejected before the
72
+ body is consumed, and streamed/chunked responses are canceled when their bytes
73
+ cross the same limit. The Rust/WASM parser independently enforces its aggregate
74
+ pack, instruction, entry-buffer, and footer limits. Any failed load preserves
75
+ the session's previous database.
76
+
77
+ Set an intentional browser-source override when constructing the session:
78
+
79
+ ```ts
80
+ const session = new MatcherSession({
81
+ maxPackBytes: 96 * 1024 * 1024
82
+ });
83
+ ```
84
+
85
+ `DEFAULT_MAX_PACK_BYTES` exports the 64 MiB default for applications that need
86
+ to align their own transport controls. The same override is passed to the
87
+ Rust/WASM total-input budget; instruction, cumulative-entry, footer, and
88
+ per-entry limits remain independently enforced.
89
+
70
90
  If you want campaign events and browser recognition flow on top of matching, use `@sorisdk/web-audio`.
71
91
 
72
92
  ## Metadata lookup
@@ -10,6 +10,8 @@ export class WasmMatcherDatabase {
10
10
  getEntryMetadata(name: string): any;
11
11
  loadPackBytes(bytes: Uint8Array): void;
12
12
  loadPackBytesAppend(bytes: Uint8Array): void;
13
+ loadPackBytesAppendWithMaxBytes(bytes: Uint8Array, max_pack_bytes: number): void;
14
+ loadPackBytesWithMaxBytes(bytes: Uint8Array, max_pack_bytes: number): void;
13
15
  constructor();
14
16
  removeEntry(name: string): void;
15
17
  setEntryVariantMetadata(name: string, encoded_metadata: string): void;
@@ -85,6 +85,30 @@ export class WasmMatcherDatabase {
85
85
  throw takeFromExternrefTable0(ret[0]);
86
86
  }
87
87
  }
88
+ /**
89
+ * @param {Uint8Array} bytes
90
+ * @param {number} max_pack_bytes
91
+ */
92
+ loadPackBytesAppendWithMaxBytes(bytes, max_pack_bytes) {
93
+ const ptr0 = passArray8ToWasm0(bytes, wasm.__wbindgen_malloc);
94
+ const len0 = WASM_VECTOR_LEN;
95
+ const ret = wasm.wasmmatcherdatabase_loadPackBytesAppendWithMaxBytes(this.__wbg_ptr, ptr0, len0, max_pack_bytes);
96
+ if (ret[1]) {
97
+ throw takeFromExternrefTable0(ret[0]);
98
+ }
99
+ }
100
+ /**
101
+ * @param {Uint8Array} bytes
102
+ * @param {number} max_pack_bytes
103
+ */
104
+ loadPackBytesWithMaxBytes(bytes, max_pack_bytes) {
105
+ const ptr0 = passArray8ToWasm0(bytes, wasm.__wbindgen_malloc);
106
+ const len0 = WASM_VECTOR_LEN;
107
+ const ret = wasm.wasmmatcherdatabase_loadPackBytesWithMaxBytes(this.__wbg_ptr, ptr0, len0, max_pack_bytes);
108
+ if (ret[1]) {
109
+ throw takeFromExternrefTable0(ret[0]);
110
+ }
111
+ }
88
112
  constructor() {
89
113
  const ret = wasm.wasmmatcherdatabase_new();
90
114
  this.__wbg_ptr = ret >>> 0;
@@ -618,6 +642,10 @@ export function __wbg_new_227d7c05414eb861() {
618
642
  const ret = new Error();
619
643
  return ret;
620
644
  }
645
+ export function __wbg_new_24a82d8799c298c3(arg0, arg1) {
646
+ const ret = new TypeError(getStringFromWasm0(arg0, arg1));
647
+ return ret;
648
+ }
621
649
  export function __wbg_new_34d45cc8e36aaead() {
622
650
  const ret = new Map();
623
651
  return ret;
Binary file
@@ -17,6 +17,8 @@ export const wasmmatcherdatabase_getEntryMetadata: (a: number, b: number, c: num
17
17
  export const wasmmatcherdatabase_length: (a: number) => number;
18
18
  export const wasmmatcherdatabase_loadPackBytes: (a: number, b: number, c: number) => [number, number];
19
19
  export const wasmmatcherdatabase_loadPackBytesAppend: (a: number, b: number, c: number) => [number, number];
20
+ export const wasmmatcherdatabase_loadPackBytesAppendWithMaxBytes: (a: number, b: number, c: number, d: number) => [number, number];
21
+ export const wasmmatcherdatabase_loadPackBytesWithMaxBytes: (a: number, b: number, c: number, d: number) => [number, number];
20
22
  export const wasmmatcherdatabase_new: () => number;
21
23
  export const wasmmatcherdatabase_removeEntry: (a: number, b: number, c: number) => [number, number];
22
24
  export const wasmmatcherdatabase_setEntryVariantMetadata: (a: number, b: number, c: number, d: number, e: number) => [number, number];
package/dist/index.d.ts CHANGED
@@ -139,6 +139,8 @@ type MatcherSessionEventName = keyof MatcherSessionEventMap;
139
139
  type MatcherSessionListener<T extends MatcherSessionEventName> = (payload: MatcherSessionEventMap[T]) => void;
140
140
 
141
141
  type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
142
+ /** Default browser-side ceiling for one AudioPack response or byte source (64 MiB). */
143
+ declare const DEFAULT_MAX_PACK_BYTES: number;
142
144
 
143
145
  interface MatcherEphemeralKeyExchangeOptions {
144
146
  endpoint: string | URL;
@@ -192,22 +194,28 @@ interface InitMatcherOptions {
192
194
  loader?: () => Promise<unknown>;
193
195
  wasmModule?: unknown;
194
196
  wasmInitInput?: unknown;
197
+ embeddedPackLoader?: () => Promise<Uint8Array>;
195
198
  auth?: MatcherEphemeralAuthOptions;
196
199
  }
197
200
  interface MatcherSessionOptions extends InitMatcherOptions {
198
201
  fetch?: FetchLike;
202
+ /** Browser-side maximum for one pack source before it reaches WASM. Defaults to 64 MiB. */
203
+ maxPackBytes?: number;
199
204
  }
200
205
  type InitMatcherResult = MatcherEphemeralAuthResult;
201
206
  declare function initMatcher(options?: InitMatcherOptions): Promise<InitMatcherResult | void>;
202
207
  declare class MatcherSession {
203
208
  private readonly events;
204
209
  private readonly fetcher?;
210
+ private readonly maxPackBytes;
205
211
  private readonly initOptions;
212
+ private readonly embeddedPackLoader?;
206
213
  private readyPromise;
207
214
  private bindings;
208
215
  private database;
209
216
  private matcher;
210
217
  private destroyed;
218
+ private packLoadTail;
211
219
  private readonly distinctMatchDeduper;
212
220
  constructor(options?: MatcherSessionOptions);
213
221
  on<T extends MatcherSessionEventName>(eventName: T, listener: MatcherSessionListener<T>): this;
@@ -215,6 +223,7 @@ declare class MatcherSession {
215
223
  once<T extends MatcherSessionEventName>(eventName: T, listener: MatcherSessionListener<T>): this;
216
224
  ready(): Promise<void>;
217
225
  loadPack(source: PackSource): Promise<void>;
226
+ private loadPackExclusive;
218
227
  addEntry(name: string, afpType: AudioFingerprintType, fingerprint: Uint8Array): Promise<void>;
219
228
  addOrReplace(name: string, afpType: AudioFingerprintType, fingerprint: Uint8Array, metadata?: EntryMetadata | null): Promise<void>;
220
229
  setEntryVariantMetadata(name: string, encodedMetadata: string): Promise<void>;
@@ -235,6 +244,7 @@ declare class MatcherSession {
235
244
  private ensureLiveReady;
236
245
  private ensureNotDestroyed;
237
246
  private matcherOrThrow;
247
+ private bindingsOrThrow;
238
248
  private databaseOrThrow;
239
249
  private emitDistinctMatchEvent;
240
250
  }
@@ -242,4 +252,4 @@ declare function __setMatcherWasmModuleForTests(moduleLike: unknown): void;
242
252
  declare function __setMatcherEmbeddedPackForTests(bytes: Uint8Array): void;
243
253
  declare function __resetMatcherWasmForTests(): void;
244
254
 
245
- export { AudioFingerprintType, type AudioMarkerCode, type AudioMarkerConfig, type AudioMarkerDetection, type EntryMetadata, type InitMatcherOptions, type InitMatcherResult, type MatchConfig, type MatchResult, type MatcherEphemeralAuthOptions, type MatcherEphemeralAuthResult, type MatcherEphemeralKeyExchangeMetadata, type MatcherEphemeralKeyExchangeOptions, MatcherSession, type MatcherSessionEventMap, type MatcherSessionEventName, type MatcherSessionListener, type MatcherSessionOptions, type PackSource, type PitchRatioEstimate, type PitchReferenceEstimate, type VariantDecision, type VariantDecisionStatus, type VariantMatch, type VariantMatchConfig, type VariantMatchQuery, __resetMatcherWasmForTests, __setMatcherEmbeddedPackForTests, __setMatcherWasmModuleForTests, initMatcher };
255
+ export { AudioFingerprintType, type AudioMarkerCode, type AudioMarkerConfig, type AudioMarkerDetection, DEFAULT_MAX_PACK_BYTES, type EntryMetadata, type InitMatcherOptions, type InitMatcherResult, type MatchConfig, type MatchResult, type MatcherEphemeralAuthOptions, type MatcherEphemeralAuthResult, type MatcherEphemeralKeyExchangeMetadata, type MatcherEphemeralKeyExchangeOptions, MatcherSession, type MatcherSessionEventMap, type MatcherSessionEventName, type MatcherSessionListener, type MatcherSessionOptions, type PackSource, type PitchRatioEstimate, type PitchReferenceEstimate, type VariantDecision, type VariantDecisionStatus, type VariantMatch, type VariantMatchConfig, type VariantMatchQuery, __resetMatcherWasmForTests, __setMatcherEmbeddedPackForTests, __setMatcherWasmModuleForTests, initMatcher };
package/dist/index.js CHANGED
@@ -174,6 +174,7 @@ function __resetEmbeddedPackLoaderForTests() {
174
174
  }
175
175
 
176
176
  // src/source.ts
177
+ var DEFAULT_MAX_PACK_BYTES = 64 * 1024 * 1024;
177
178
  function hasCtor(name) {
178
179
  return typeof globalThis[name] !== "undefined";
179
180
  }
@@ -183,6 +184,20 @@ function copyToUint8Array(input) {
183
184
  function toUint8Array(buffer) {
184
185
  return new Uint8Array(buffer.slice(0));
185
186
  }
187
+ function validateMaxPackBytes(maxPackBytes) {
188
+ if (!Number.isSafeInteger(maxPackBytes) || maxPackBytes <= 0 || maxPackBytes > 4294967295) {
189
+ throw new Error("maxPackBytes must be a positive 32-bit unsigned integer");
190
+ }
191
+ return maxPackBytes;
192
+ }
193
+ function packSizeError(maxPackBytes) {
194
+ return new Error(`Pack source exceeds maximum size of ${maxPackBytes} bytes`);
195
+ }
196
+ function assertKnownByteLength(byteLength, maxPackBytes) {
197
+ if (byteLength > maxPackBytes) {
198
+ throw packSizeError(maxPackBytes);
199
+ }
200
+ }
186
201
  function classifyPackSource(source) {
187
202
  if (source instanceof Uint8Array) {
188
203
  return "uint8array";
@@ -216,29 +231,68 @@ function resolveFetch3(fetcher) {
216
231
  }
217
232
  return globalThis.fetch.bind(globalThis);
218
233
  }
219
- async function readResponseAsBytes(response) {
234
+ async function readResponseAsBytes(response, maxPackBytes) {
220
235
  if (!response.ok) {
221
236
  throw new Error(`Failed to load pack: HTTP ${response.status}`);
222
237
  }
223
- return toUint8Array(await response.arrayBuffer());
238
+ const contentLength = response.headers.get("content-length");
239
+ if (contentLength !== null && /^\d+$/.test(contentLength.trim())) {
240
+ const declaredLength = Number(contentLength);
241
+ if (Number.isSafeInteger(declaredLength)) {
242
+ assertKnownByteLength(declaredLength, maxPackBytes);
243
+ }
244
+ }
245
+ if (!response.body) {
246
+ return new Uint8Array();
247
+ }
248
+ const reader = response.body.getReader();
249
+ const chunks = [];
250
+ let byteLength = 0;
251
+ try {
252
+ while (true) {
253
+ const { done, value } = await reader.read();
254
+ if (done) {
255
+ break;
256
+ }
257
+ byteLength += value.byteLength;
258
+ if (byteLength > maxPackBytes) {
259
+ await reader.cancel();
260
+ throw packSizeError(maxPackBytes);
261
+ }
262
+ chunks.push(value);
263
+ }
264
+ } finally {
265
+ reader.releaseLock();
266
+ }
267
+ const bytes = new Uint8Array(byteLength);
268
+ let offset = 0;
269
+ for (const chunk of chunks) {
270
+ bytes.set(chunk, offset);
271
+ offset += chunk.byteLength;
272
+ }
273
+ return bytes;
224
274
  }
225
- async function normalizePackSource(source, fetcher) {
275
+ async function normalizePackSource(source, fetcher, maxPackBytes = DEFAULT_MAX_PACK_BYTES) {
276
+ maxPackBytes = validateMaxPackBytes(maxPackBytes);
226
277
  const sourceType = classifyPackSource(source);
227
278
  if (source instanceof Uint8Array) {
279
+ assertKnownByteLength(source.byteLength, maxPackBytes);
228
280
  return { sourceType, bytes: copyToUint8Array(source) };
229
281
  }
230
282
  if (source instanceof ArrayBuffer) {
283
+ assertKnownByteLength(source.byteLength, maxPackBytes);
231
284
  return { sourceType, bytes: toUint8Array(source) };
232
285
  }
233
286
  if (hasCtor("Blob") && source instanceof Blob) {
287
+ assertKnownByteLength(source.size, maxPackBytes);
234
288
  return { sourceType, bytes: toUint8Array(await source.arrayBuffer()) };
235
289
  }
236
290
  if (hasCtor("Response") && source instanceof Response) {
237
- return { sourceType, bytes: await readResponseAsBytes(source) };
291
+ return { sourceType, bytes: await readResponseAsBytes(source, maxPackBytes) };
238
292
  }
239
293
  if (typeof source === "string" || source instanceof URL || hasCtor("Request") && source instanceof Request) {
240
294
  const response = await resolveFetch3(fetcher)(source);
241
- return { sourceType, bytes: await readResponseAsBytes(response) };
295
+ return { sourceType, bytes: await readResponseAsBytes(response, maxPackBytes) };
242
296
  }
243
297
  throw new Error("Unsupported pack source type");
244
298
  }
@@ -328,7 +382,15 @@ var WrappedDatabase = class {
328
382
  }
329
383
  fn.call(this.inner);
330
384
  }
331
- loadPackBytes(bytes) {
385
+ loadPackBytes(bytes, maxPackBytes) {
386
+ const limitedFn = getCallable(this.inner, [
387
+ "loadPackBytesWithMaxBytes",
388
+ "load_pack_bytes_with_max_bytes"
389
+ ]);
390
+ if (maxPackBytes !== void 0 && limitedFn) {
391
+ limitedFn.call(this.inner, bytes, maxPackBytes);
392
+ return;
393
+ }
332
394
  const fn = getCallable(this.inner, [
333
395
  "loadPackBytes",
334
396
  "loadPack",
@@ -340,7 +402,15 @@ var WrappedDatabase = class {
340
402
  }
341
403
  fn.call(this.inner, bytes);
342
404
  }
343
- loadPackBytesAppend(bytes) {
405
+ loadPackBytesAppend(bytes, maxPackBytes) {
406
+ const limitedFn = getCallable(this.inner, [
407
+ "loadPackBytesAppendWithMaxBytes",
408
+ "load_pack_bytes_append_with_max_bytes"
409
+ ]);
410
+ if (maxPackBytes !== void 0 && limitedFn) {
411
+ limitedFn.call(this.inner, bytes, maxPackBytes);
412
+ return;
413
+ }
344
414
  const appendFn = getCallable(this.inner, ["loadPackBytesAppend", "load_pack_bytes_append"]);
345
415
  if (!appendFn) {
346
416
  throw new Error("WASM database is missing loadPackBytesAppend-compatible method");
@@ -844,19 +914,25 @@ async function initMatcher(options) {
844
914
  var MatcherSession = class {
845
915
  events = new TypedEventEmitter();
846
916
  fetcher;
917
+ maxPackBytes;
847
918
  initOptions;
919
+ embeddedPackLoader;
848
920
  readyPromise;
849
921
  bindings = null;
850
922
  database = null;
851
923
  matcher = null;
852
924
  destroyed = false;
925
+ packLoadTail = Promise.resolve();
853
926
  distinctMatchDeduper = new DistinctMatchDeduper();
854
927
  constructor(options) {
855
928
  this.fetcher = options?.fetch;
929
+ this.maxPackBytes = options?.maxPackBytes ?? DEFAULT_MAX_PACK_BYTES;
930
+ this.embeddedPackLoader = options?.embeddedPackLoader;
856
931
  this.initOptions = {
857
932
  loader: options?.loader,
858
933
  wasmModule: options?.wasmModule,
859
- wasmInitInput: options?.wasmInitInput
934
+ wasmInitInput: options?.wasmInitInput,
935
+ embeddedPackLoader: options?.embeddedPackLoader
860
936
  };
861
937
  this.readyPromise = this.bootstrap();
862
938
  }
@@ -875,17 +951,30 @@ var MatcherSession = class {
875
951
  async ready() {
876
952
  await this.readyPromise;
877
953
  }
878
- async loadPack(source) {
954
+ loadPack(source) {
955
+ const load = this.packLoadTail.then(() => this.loadPackExclusive(source));
956
+ this.packLoadTail = load.catch(() => void 0);
957
+ return load;
958
+ }
959
+ async loadPackExclusive(source) {
879
960
  try {
880
961
  await this.ensureLiveReady();
881
962
  const sourceType = classifyPackSource(source);
882
963
  this.events.emit("packloadstart", { sourceType });
883
- const database = this.databaseOrThrow();
884
- const normalized = await normalizePackSource(source, this.fetcher);
885
- const embeddedPackBytes = await loadEmbeddedPackBytes();
886
- database.clear();
887
- database.loadPackBytes(embeddedPackBytes);
888
- database.loadPackBytesAppend(normalized.bytes);
964
+ const previousDatabase = this.databaseOrThrow();
965
+ const normalized = await normalizePackSource(source, this.fetcher, this.maxPackBytes);
966
+ const embeddedPackBytes = this.embeddedPackLoader ? await this.embeddedPackLoader() : await loadEmbeddedPackBytes();
967
+ const bindings = this.bindingsOrThrow();
968
+ const stagedDatabase = bindings.createDatabase();
969
+ try {
970
+ stagedDatabase.loadPackBytes(embeddedPackBytes);
971
+ stagedDatabase.loadPackBytesAppend(normalized.bytes, this.maxPackBytes);
972
+ } catch (error) {
973
+ stagedDatabase.destroy();
974
+ throw error;
975
+ }
976
+ this.database = stagedDatabase;
977
+ previousDatabase.destroy();
889
978
  this.distinctMatchDeduper.reset();
890
979
  this.matcher?.resetDistinctMatchState?.();
891
980
  this.events.emit("packload", {
@@ -1187,6 +1276,12 @@ var MatcherSession = class {
1187
1276
  }
1188
1277
  return this.matcher;
1189
1278
  }
1279
+ bindingsOrThrow() {
1280
+ if (!this.bindings) {
1281
+ throw new Error("Matcher bindings are not initialized");
1282
+ }
1283
+ return this.bindings;
1284
+ }
1190
1285
  databaseOrThrow() {
1191
1286
  if (!this.database) {
1192
1287
  throw new Error("Database is not initialized");
@@ -1226,6 +1321,7 @@ var AudioFingerprintType = /* @__PURE__ */ ((AudioFingerprintType2) => {
1226
1321
  })(AudioFingerprintType || {});
1227
1322
  export {
1228
1323
  AudioFingerprintType,
1324
+ DEFAULT_MAX_PACK_BYTES,
1229
1325
  MatcherSession,
1230
1326
  __resetMatcherWasmForTests,
1231
1327
  __setMatcherEmbeddedPackForTests,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sorisdk/matcher",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Browser WASM SDK for audio fingerprint matching",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",