@sorisdk/matcher 0.6.7 → 0.6.9

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
@@ -55,6 +55,38 @@ const session = new MatcherSession({
55
55
  });
56
56
  ```
57
57
 
58
+ ## Continuous recognition and reset
59
+
60
+ Every query returns its matching result, including repeated material matches.
61
+ `match`, `bestMatch`, and their variant methods share one event observer:
62
+ continuous A → A emits one `match`, while A → B → A emits three. Material
63
+ identity includes the fingerprint type. There is no material cooldown.
64
+
65
+ Every successful observation resets the consecutive-miss counter and renews
66
+ the run, even when its public event is suppressed. Two consecutive no-match
67
+ results end the run and emit one `nomatch`; a single transient miss does not.
68
+ A gap **greater than 30 seconds since the last successful observation** also
69
+ ends continuity, including periods with no queries. This boundary is evaluated
70
+ on the next query: a success emits a new `match` immediately, or a no-match
71
+ emits `nomatch` without waiting for a second miss. The exported
72
+ `MATCH_CONTINUITY_GAP_MS` is shared with Web activity reporting.
73
+
74
+ Call `session.resetRecognitionState()` at an explicit input/capture boundary.
75
+ It synchronously clears event suppression and invalidates pending queries from
76
+ the previous run, retaining the loaded database. Invalidated queries return
77
+ `null` (best-match methods) or `[]` (multi-match methods) without match/no-match
78
+ events. Successful pack replacement and entry mutations also reset recognition
79
+ state; a failed pack load retains the previous state and entries.
80
+
81
+ The low-level generated WASM matcher always returns successful observations;
82
+ event deduplication belongs to `MatcherSession`. Its existing
83
+ `resetDistinctMatchState()` export remains available for compatibility.
84
+
85
+ For source regression tests, build the package first with
86
+ `corepack pnpm --filter @sorisdk/matcher build`, then run
87
+ `corepack pnpm --filter @sorisdk/matcher test`. Tests include production WASM
88
+ instantiated with the TypeScript session, using deterministic fingerprints.
89
+
58
90
  ## Pack loading
59
91
 
60
92
  `loadPack(...)` accepts:
@@ -35,11 +35,21 @@ export class WasmMatcherEngine {
35
35
  matchQuery(database: WasmMatcherDatabase, query: Uint8Array, config?: any | null): any;
36
36
  matchQueryWithVariants(database: WasmMatcherDatabase, query: Uint8Array, pitch_query: any, config?: any | null, variant_config?: any | null): any;
37
37
  constructor();
38
+ /**
39
+ * Compatibility no-op: continuous-run deduplication belongs to MatcherSession.
40
+ * The binding returns every successful observation and retains no run state.
41
+ */
38
42
  resetDistinctMatchState(): void;
39
43
  }
40
44
 
41
45
  export function detectAudioMarker(pcm16_le: Uint8Array, sample_rate: number, config?: any | null): any;
42
46
 
47
+ /**
48
+ * Explicit v1 policy; existing marker exports remain on the legacy policy.
49
+ * Result includes raw detection, normalized diagnostics and shared eligibility.
50
+ */
51
+ export function detectAudioMarkerLevelRobust(pcm16_le: Uint8Array, sample_rate: number, config?: any | null, limits?: any | null): any;
52
+
43
53
  export function detectAudioMarkerWithLimits(pcm16_le: Uint8Array, sample_rate: number, config?: any | null, limits?: any | null): any;
44
54
 
45
55
  export function detectPatternMark(pcm16_le: Uint8Array, sample_rate: number, config?: any | null): any;
@@ -5,5 +5,5 @@ import { __wbg_set_wasm } from "./core_bg.js";
5
5
  __wbg_set_wasm(wasm);
6
6
  wasm.__wbindgen_start();
7
7
  export {
8
- WasmMatcherDatabase, WasmMatcherEngine, detectAudioMarker, detectAudioMarkerWithLimits, detectPatternMark, detectPatternMarkWithLimits, estimatePitchRatio, estimatePitchRatioAgainstReference, estimatePitchRatioAgainstSpectralFeatures, extractPitchReference, extractSpectralShiftReference, init_matcher_wasm
8
+ WasmMatcherDatabase, WasmMatcherEngine, detectAudioMarker, detectAudioMarkerLevelRobust, detectAudioMarkerWithLimits, detectPatternMark, detectPatternMarkWithLimits, estimatePitchRatio, estimatePitchRatioAgainstReference, estimatePitchRatioAgainstSpectralFeatures, extractPitchReference, extractSpectralShiftReference, init_matcher_wasm
9
9
  } from "./core_bg.js";
@@ -359,6 +359,10 @@ export class WasmMatcherEngine {
359
359
  WasmMatcherEngineFinalization.register(this, this.__wbg_ptr, this);
360
360
  return this;
361
361
  }
362
+ /**
363
+ * Compatibility no-op: continuous-run deduplication belongs to MatcherSession.
364
+ * The binding returns every successful observation and retains no run state.
365
+ */
362
366
  resetDistinctMatchState() {
363
367
  wasm.wasmmatcherengine_resetDistinctMatchState(this.__wbg_ptr);
364
368
  }
@@ -379,6 +383,23 @@ export function detectAudioMarker(pcm16_le, sample_rate, config) {
379
383
  return takeFromExternrefTable0(ret[0]);
380
384
  }
381
385
 
386
+ /**
387
+ * Explicit v1 policy; existing marker exports remain on the legacy policy.
388
+ * Result includes raw detection, normalized diagnostics and shared eligibility.
389
+ * @param {Uint8Array} pcm16_le
390
+ * @param {number} sample_rate
391
+ * @param {any | null} [config]
392
+ * @param {any | null} [limits]
393
+ * @returns {any}
394
+ */
395
+ export function detectAudioMarkerLevelRobust(pcm16_le, sample_rate, config, limits) {
396
+ const ret = wasm.detectAudioMarkerLevelRobust(pcm16_le, sample_rate, isLikeNone(config) ? 0 : addToExternrefTable0(config), isLikeNone(limits) ? 0 : addToExternrefTable0(limits));
397
+ if (ret[2]) {
398
+ throw takeFromExternrefTable0(ret[1]);
399
+ }
400
+ return takeFromExternrefTable0(ret[0]);
401
+ }
402
+
382
403
  /**
383
404
  * @param {Uint8Array} pcm16_le
384
405
  * @param {number} sample_rate
@@ -599,6 +620,10 @@ export function __wbg_call_14b169f759b26747() { return handleError(function (arg
599
620
  const ret = arg0.call(arg1);
600
621
  return ret;
601
622
  }, arguments); }
623
+ export function __wbg_call_a24592a6f349a97e() { return handleError(function (arg0, arg1, arg2) {
624
+ const ret = arg0.call(arg1, arg2);
625
+ return ret;
626
+ }, arguments); }
602
627
  export function __wbg_done_9158f7cc8751ba32(arg0) {
603
628
  const ret = arg0.done;
604
629
  return ret;
@@ -618,10 +643,22 @@ export function __wbg_error_a6fa202b58aa1cd3(arg0, arg1) {
618
643
  wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
619
644
  }
620
645
  }
646
+ export function __wbg_getOwnPropertyDescriptor_131bd582a45a6f5d(arg0, arg1) {
647
+ const ret = Object.getOwnPropertyDescriptor(arg0, arg1);
648
+ return ret;
649
+ }
650
+ export function __wbg_getPrototypeOf_a56da9261bbd1e5b(arg0) {
651
+ const ret = Object.getPrototypeOf(arg0);
652
+ return ret;
653
+ }
621
654
  export function __wbg_get_1affdbdd5573b16a() { return handleError(function (arg0, arg1) {
622
655
  const ret = Reflect.get(arg0, arg1);
623
656
  return ret;
624
657
  }, arguments); }
658
+ export function __wbg_get_6011fa3a58f61074() { return handleError(function (arg0, arg1) {
659
+ const ret = Reflect.get(arg0, arg1);
660
+ return ret;
661
+ }, arguments); }
625
662
  export function __wbg_get_8360291721e2339f(arg0, arg1) {
626
663
  const ret = arg0[arg1 >>> 0];
627
664
  return ret;
@@ -716,10 +753,6 @@ export function __wbg_next_7646edaa39458ef7(arg0) {
716
753
  const ret = arg0.next;
717
754
  return ret;
718
755
  }
719
- export function __wbg_now_a9b7df1cbee90986() {
720
- const ret = Date.now();
721
- return ret;
722
- }
723
756
  export function __wbg_prototypesetcall_a6b02eb00b0f4ce2(arg0, arg1, arg2) {
724
757
  Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2);
725
758
  }
@@ -740,6 +773,26 @@ export function __wbg_stack_3b0d974bbf31e44f(arg0, arg1) {
740
773
  getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
741
774
  getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
742
775
  }
776
+ export function __wbg_static_accessor_GLOBAL_8cfadc87a297ca02() {
777
+ const ret = typeof global === 'undefined' ? null : global;
778
+ return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
779
+ }
780
+ export function __wbg_static_accessor_GLOBAL_THIS_602256ae5c8f42cf() {
781
+ const ret = typeof globalThis === 'undefined' ? null : globalThis;
782
+ return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
783
+ }
784
+ export function __wbg_static_accessor_SELF_e445c1c7484aecc3() {
785
+ const ret = typeof self === 'undefined' ? null : self;
786
+ return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
787
+ }
788
+ export function __wbg_static_accessor_WINDOW_f20e8576ef1e0f17() {
789
+ const ret = typeof window === 'undefined' ? null : window;
790
+ return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
791
+ }
792
+ export function __wbg_toStringTag_d6b1716c86d4892d() {
793
+ const ret = Symbol.toStringTag;
794
+ return ret;
795
+ }
743
796
  export function __wbg_value_ee3a06f4579184fa(arg0) {
744
797
  const ret = arg0.value;
745
798
  return ret;
@@ -755,11 +808,16 @@ export function __wbindgen_cast_0000000000000002(arg0) {
755
808
  return ret;
756
809
  }
757
810
  export function __wbindgen_cast_0000000000000003(arg0, arg1) {
811
+ // Cast intrinsic for `Ref(Slice(U8)) -> NamedExternref("Uint8Array")`.
812
+ const ret = getArrayU8FromWasm0(arg0, arg1);
813
+ return ret;
814
+ }
815
+ export function __wbindgen_cast_0000000000000004(arg0, arg1) {
758
816
  // Cast intrinsic for `Ref(String) -> Externref`.
759
817
  const ret = getStringFromWasm0(arg0, arg1);
760
818
  return ret;
761
819
  }
762
- export function __wbindgen_cast_0000000000000004(arg0) {
820
+ export function __wbindgen_cast_0000000000000005(arg0) {
763
821
  // Cast intrinsic for `U64 -> Externref`.
764
822
  const ret = BigInt.asUintN(64, arg0);
765
823
  return ret;
Binary file
@@ -4,6 +4,7 @@ export const memory: WebAssembly.Memory;
4
4
  export const __wbg_wasmmatcherdatabase_free: (a: number, b: number) => void;
5
5
  export const __wbg_wasmmatcherengine_free: (a: number, b: number) => void;
6
6
  export const detectAudioMarker: (a: any, b: number, c: number) => [number, number, number];
7
+ export const detectAudioMarkerLevelRobust: (a: any, b: number, c: number, d: number) => [number, number, number];
7
8
  export const detectAudioMarkerWithLimits: (a: any, b: number, c: number, d: number) => [number, number, number];
8
9
  export const detectPatternMarkWithLimits: (a: any, b: number, c: number, d: number) => [number, number, number];
9
10
  export const estimatePitchRatio: (a: number, b: number, c: number) => [number, number, number];
@@ -27,7 +28,6 @@ export const wasmmatcherengine_bestMatch: (a: number, b: number, c: number, d: n
27
28
  export const wasmmatcherengine_bestMatchWithVariants: (a: number, b: number, c: number, d: number, e: any, f: number, g: number) => [number, number, number];
28
29
  export const wasmmatcherengine_detectAudioMarker: (a: number, b: any, c: number, d: number) => [number, number, number];
29
30
  export const wasmmatcherengine_detectAudioMarkerWithLimits: (a: number, b: any, c: number, d: number, e: number) => [number, number, number];
30
- export const wasmmatcherengine_detectPatternMark: (a: number, b: any, c: number, d: number) => [number, number, number];
31
31
  export const wasmmatcherengine_detectPatternMarkWithLimits: (a: number, b: any, c: number, d: number, e: number) => [number, number, number];
32
32
  export const wasmmatcherengine_estimatePitchRatio: (a: number, b: number, c: number, d: number) => [number, number, number];
33
33
  export const wasmmatcherengine_estimatePitchRatioAgainstReference: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number];
@@ -36,10 +36,11 @@ export const wasmmatcherengine_extractPitchReference: (a: number, b: number, c:
36
36
  export const wasmmatcherengine_extractSpectralShiftReference: (a: number, b: number, c: number, d: number) => [number, number, number];
37
37
  export const wasmmatcherengine_matchQuery: (a: number, b: number, c: number, d: number, e: number) => [number, number, number];
38
38
  export const wasmmatcherengine_matchQueryWithVariants: (a: number, b: number, c: number, d: number, e: any, f: number, g: number) => [number, number, number];
39
- export const wasmmatcherengine_new: () => number;
40
39
  export const wasmmatcherengine_resetDistinctMatchState: (a: number) => void;
40
+ export const wasmmatcherengine_new: () => number;
41
41
  export const init_matcher_wasm: () => void;
42
42
  export const detectPatternMark: (a: any, b: number, c: number) => [number, number, number];
43
+ export const wasmmatcherengine_detectPatternMark: (a: number, b: any, c: number, d: number) => [number, number, number];
43
44
  export const __wbindgen_malloc: (a: number, b: number) => number;
44
45
  export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
45
46
  export const __wbindgen_exn_store: (a: number) => void;
package/dist/index.d.ts CHANGED
@@ -230,6 +230,7 @@ declare class MatcherSession {
230
230
  private matcher;
231
231
  private destroyed;
232
232
  private packLoadTail;
233
+ private recognitionGeneration;
233
234
  private readonly distinctMatchDeduper;
234
235
  constructor(options?: MatcherSessionOptions);
235
236
  on<T extends MatcherSessionEventName>(eventName: T, listener: MatcherSessionListener<T>): this;
@@ -255,6 +256,10 @@ declare class MatcherSession {
255
256
  detectAudioMarkerWithLimits(pcm16Le: Uint8Array, sampleRate: number, config: AudioMarkerConfig | undefined, limits: AudioMarkerLimits): Promise<AudioMarkerDetection>;
256
257
  matchWithVariants(query: Uint8Array, pitchQuery: VariantMatchQuery, config?: MatchConfig, variantConfig?: VariantMatchConfig): Promise<VariantMatch[]>;
257
258
  bestMatchWithVariants(query: Uint8Array, pitchQuery: VariantMatchQuery, config?: MatchConfig, variantConfig?: VariantMatchConfig): Promise<VariantMatch | null>;
259
+ /** End the current recognition run without removing loaded entries.
260
+ * Pending queries from the previous run cannot update event suppression state.
261
+ */
262
+ resetRecognitionState(): void;
258
263
  destroy(): void;
259
264
  private bootstrap;
260
265
  private ensureLiveReady;
@@ -268,4 +273,7 @@ declare function __setMatcherWasmModuleForTests(moduleLike: unknown): void;
268
273
  declare function __setMatcherEmbeddedPackForTests(bytes: Uint8Array): void;
269
274
  declare function __resetMatcherWasmForTests(): void;
270
275
 
271
- export { AUDIO_MARKER_INPUT_BUDGET_EXCEEDED_REASON, AUDIO_MARKER_WORK_BUDGET_EXCEEDED_REASON, AUDIO_MARKER_WORK_OVERFLOW_REASON, AudioFingerprintType, type AudioMarkerCode, type AudioMarkerConfig, type AudioMarkerDetection, type AudioMarkerLimits, DEFAULT_MAX_AUDIO_MARKER_DURATION_MILLIS, DEFAULT_MAX_AUDIO_MARKER_PCM_BYTES, DEFAULT_MAX_AUDIO_MARKER_SAMPLES, DEFAULT_MAX_AUDIO_MARKER_WORK_UNITS, 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 };
276
+ /** Maximum gap between successful observations in one continuous material run. */
277
+ declare const MATCH_CONTINUITY_GAP_MS = 30000;
278
+
279
+ export { AUDIO_MARKER_INPUT_BUDGET_EXCEEDED_REASON, AUDIO_MARKER_WORK_BUDGET_EXCEEDED_REASON, AUDIO_MARKER_WORK_OVERFLOW_REASON, AudioFingerprintType, type AudioMarkerCode, type AudioMarkerConfig, type AudioMarkerDetection, type AudioMarkerLimits, DEFAULT_MAX_AUDIO_MARKER_DURATION_MILLIS, DEFAULT_MAX_AUDIO_MARKER_PCM_BYTES, DEFAULT_MAX_AUDIO_MARKER_SAMPLES, DEFAULT_MAX_AUDIO_MARKER_WORK_UNITS, DEFAULT_MAX_PACK_BYTES, type EntryMetadata, type InitMatcherOptions, type InitMatcherResult, MATCH_CONTINUITY_GAP_MS, 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
@@ -327,28 +327,31 @@ var AUDIO_MARKER_WORK_OVERFLOW_REASON = "detector_work_overflow";
327
327
 
328
328
  // src/match-deduper.ts
329
329
  var DEFAULT_NO_MATCH_CONFIRMATION_COUNT = 2;
330
+ var MATCH_CONTINUITY_GAP_MS = 3e4;
330
331
  function getMatchIdentity(match) {
331
332
  return `${match.name}:${match.afpType}`;
332
333
  }
333
334
  var DistinctMatchDeduper = class {
334
335
  lastIdentity = null;
335
336
  pendingNoMatchCount = 0;
336
- observe(best) {
337
+ lastObservedAt = null;
338
+ observe(best, now = Date.now()) {
339
+ const expired = this.lastObservedAt !== null && now - this.lastObservedAt > MATCH_CONTINUITY_GAP_MS;
337
340
  if (!best) {
338
341
  if (this.lastIdentity === null) {
339
342
  return { kind: "repeat", best: null };
340
343
  }
341
344
  this.pendingNoMatchCount += 1;
342
- if (this.pendingNoMatchCount < DEFAULT_NO_MATCH_CONFIRMATION_COUNT) {
345
+ if (!expired && this.pendingNoMatchCount < DEFAULT_NO_MATCH_CONFIRMATION_COUNT) {
343
346
  return { kind: "repeat", best: null };
344
347
  }
345
- this.lastIdentity = null;
346
- this.pendingNoMatchCount = 0;
348
+ this.reset();
347
349
  return { kind: "nomatch" };
348
350
  }
349
351
  const nextIdentity = getMatchIdentity(best);
350
352
  this.pendingNoMatchCount = 0;
351
- if (this.lastIdentity === nextIdentity) {
353
+ this.lastObservedAt = now;
354
+ if (!expired && this.lastIdentity === nextIdentity) {
352
355
  return { kind: "repeat", best };
353
356
  }
354
357
  this.lastIdentity = nextIdentity;
@@ -357,6 +360,7 @@ var DistinctMatchDeduper = class {
357
360
  reset() {
358
361
  this.lastIdentity = null;
359
362
  this.pendingNoMatchCount = 0;
363
+ this.lastObservedAt = null;
360
364
  }
361
365
  };
362
366
 
@@ -753,11 +757,6 @@ function normalizeResults(rawResults) {
753
757
  function normalizeVariantMatches(rawResults) {
754
758
  return rawResults.filter((item) => typeof item === "object" && item !== null).map((item) => normalizeVariantMatch(item));
755
759
  }
756
- function isInternallyDuplicatedBestMatch(result) {
757
- return Boolean(
758
- result && typeof result === "object" && result[INTERNAL_DUPLICATE_MATCH_FLAG] === true
759
- );
760
- }
761
760
  function resolveFactory(moduleLike, names) {
762
761
  const fn = getCallable(moduleLike, names);
763
762
  return fn ? fn.bind(moduleLike) : null;
@@ -1050,6 +1049,7 @@ var MatcherSession = class {
1050
1049
  matcher = null;
1051
1050
  destroyed = false;
1052
1051
  packLoadTail = Promise.resolve();
1052
+ recognitionGeneration = 0;
1053
1053
  distinctMatchDeduper = new DistinctMatchDeduper();
1054
1054
  constructor(options) {
1055
1055
  this.fetcher = options?.fetch;
@@ -1102,8 +1102,7 @@ var MatcherSession = class {
1102
1102
  }
1103
1103
  this.database = stagedDatabase;
1104
1104
  previousDatabase.destroy();
1105
- this.distinctMatchDeduper.reset();
1106
- this.matcher?.resetDistinctMatchState?.();
1105
+ this.resetRecognitionState();
1107
1106
  this.events.emit("packload", {
1108
1107
  sourceType: normalized.sourceType,
1109
1108
  byteLength: normalized.bytes.byteLength
@@ -1116,8 +1115,7 @@ var MatcherSession = class {
1116
1115
  try {
1117
1116
  await this.ensureLiveReady();
1118
1117
  this.databaseOrThrow().addEntry(name, afpType, toUint8ArrayCopy(fingerprint));
1119
- this.distinctMatchDeduper.reset();
1120
- this.matcher?.resetDistinctMatchState?.();
1118
+ this.resetRecognitionState();
1121
1119
  } catch (error) {
1122
1120
  handleError(this.events, "addEntry", error);
1123
1121
  }
@@ -1131,8 +1129,7 @@ var MatcherSession = class {
1131
1129
  toUint8ArrayCopy(fingerprint),
1132
1130
  metadata
1133
1131
  );
1134
- this.distinctMatchDeduper.reset();
1135
- this.matcher?.resetDistinctMatchState?.();
1132
+ this.resetRecognitionState();
1136
1133
  } catch (error) {
1137
1134
  handleError(this.events, "addOrReplace", error);
1138
1135
  }
@@ -1141,8 +1138,7 @@ var MatcherSession = class {
1141
1138
  try {
1142
1139
  await this.ensureLiveReady();
1143
1140
  this.databaseOrThrow().setEntryVariantMetadata(name, encodedMetadata);
1144
- this.distinctMatchDeduper.reset();
1145
- this.matcher?.resetDistinctMatchState?.();
1141
+ this.resetRecognitionState();
1146
1142
  } catch (error) {
1147
1143
  handleError(this.events, "setEntryVariantMetadata", error);
1148
1144
  }
@@ -1151,8 +1147,7 @@ var MatcherSession = class {
1151
1147
  try {
1152
1148
  await this.ensureLiveReady();
1153
1149
  this.databaseOrThrow().removeEntry(name);
1154
- this.distinctMatchDeduper.reset();
1155
- this.matcher?.resetDistinctMatchState?.();
1150
+ this.resetRecognitionState();
1156
1151
  } catch (error) {
1157
1152
  handleError(this.events, "removeEntry", error);
1158
1153
  }
@@ -1161,8 +1156,7 @@ var MatcherSession = class {
1161
1156
  try {
1162
1157
  await this.ensureLiveReady();
1163
1158
  this.databaseOrThrow().clear();
1164
- this.distinctMatchDeduper.reset();
1165
- this.matcher?.resetDistinctMatchState?.();
1159
+ this.resetRecognitionState();
1166
1160
  } catch (error) {
1167
1161
  handleError(this.events, "clear", error);
1168
1162
  }
@@ -1176,27 +1170,38 @@ var MatcherSession = class {
1176
1170
  }
1177
1171
  }
1178
1172
  async match(query, config) {
1173
+ const generation = this.recognitionGeneration;
1179
1174
  try {
1180
1175
  await this.ensureLiveReady();
1176
+ if (generation !== this.recognitionGeneration) {
1177
+ return [];
1178
+ }
1181
1179
  const safeQuery = toUint8ArrayCopy(query);
1182
1180
  this.events.emit("querystart", { queryByteLength: safeQuery.byteLength });
1181
+ if (generation !== this.recognitionGeneration || this.destroyed) {
1182
+ return [];
1183
+ }
1183
1184
  const results = this.matcherOrThrow().findMatches(this.databaseOrThrow(), safeQuery, config);
1184
- this.emitDistinctMatchEvent(results[0] ?? null, safeQuery.byteLength, results);
1185
+ this.emitDistinctMatchEvent(generation, results[0] ?? null, safeQuery.byteLength, results);
1185
1186
  return results;
1186
1187
  } catch (error) {
1187
1188
  handleError(this.events, "match", error);
1188
1189
  }
1189
1190
  }
1190
1191
  async bestMatch(query, config) {
1192
+ const generation = this.recognitionGeneration;
1191
1193
  try {
1192
1194
  await this.ensureLiveReady();
1195
+ if (generation !== this.recognitionGeneration) {
1196
+ return null;
1197
+ }
1193
1198
  const safeQuery = toUint8ArrayCopy(query);
1194
1199
  this.events.emit("querystart", { queryByteLength: safeQuery.byteLength });
1195
- const best = this.matcherOrThrow().bestMatch(this.databaseOrThrow(), safeQuery, config);
1196
- if (isInternallyDuplicatedBestMatch(best)) {
1197
- return best;
1200
+ if (generation !== this.recognitionGeneration || this.destroyed) {
1201
+ return null;
1198
1202
  }
1199
- this.emitDistinctMatchEvent(best, safeQuery.byteLength, best ? [best] : []);
1203
+ const best = this.matcherOrThrow().bestMatch(this.databaseOrThrow(), safeQuery, config);
1204
+ this.emitDistinctMatchEvent(generation, best, safeQuery.byteLength, best ? [best] : []);
1200
1205
  return best;
1201
1206
  } catch (error) {
1202
1207
  handleError(this.events, "bestMatch", error);
@@ -1346,10 +1351,17 @@ var MatcherSession = class {
1346
1351
  }
1347
1352
  }
1348
1353
  async matchWithVariants(query, pitchQuery, config, variantConfig) {
1354
+ const generation = this.recognitionGeneration;
1349
1355
  try {
1350
1356
  await this.ensureLiveReady();
1357
+ if (generation !== this.recognitionGeneration) {
1358
+ return [];
1359
+ }
1351
1360
  const safeQuery = toUint8ArrayCopy(query);
1352
1361
  this.events.emit("querystart", { queryByteLength: safeQuery.byteLength });
1362
+ if (generation !== this.recognitionGeneration || this.destroyed) {
1363
+ return [];
1364
+ }
1353
1365
  const results = this.matcherOrThrow().findMatchesWithVariants(
1354
1366
  this.databaseOrThrow(),
1355
1367
  safeQuery,
@@ -1358,6 +1370,7 @@ var MatcherSession = class {
1358
1370
  variantConfig
1359
1371
  );
1360
1372
  this.emitDistinctMatchEvent(
1373
+ generation,
1361
1374
  results[0]?.matchResult ?? null,
1362
1375
  safeQuery.byteLength,
1363
1376
  results.map((result) => result.matchResult)
@@ -1368,10 +1381,17 @@ var MatcherSession = class {
1368
1381
  }
1369
1382
  }
1370
1383
  async bestMatchWithVariants(query, pitchQuery, config, variantConfig) {
1384
+ const generation = this.recognitionGeneration;
1371
1385
  try {
1372
1386
  await this.ensureLiveReady();
1387
+ if (generation !== this.recognitionGeneration) {
1388
+ return null;
1389
+ }
1373
1390
  const safeQuery = toUint8ArrayCopy(query);
1374
1391
  this.events.emit("querystart", { queryByteLength: safeQuery.byteLength });
1392
+ if (generation !== this.recognitionGeneration || this.destroyed) {
1393
+ return null;
1394
+ }
1375
1395
  const best = this.matcherOrThrow().bestMatchWithVariants(
1376
1396
  this.databaseOrThrow(),
1377
1397
  safeQuery,
@@ -1380,6 +1400,7 @@ var MatcherSession = class {
1380
1400
  variantConfig
1381
1401
  );
1382
1402
  this.emitDistinctMatchEvent(
1403
+ generation,
1383
1404
  best?.matchResult ?? null,
1384
1405
  safeQuery.byteLength,
1385
1406
  best ? [best.matchResult] : []
@@ -1389,10 +1410,19 @@ var MatcherSession = class {
1389
1410
  handleError(this.events, "bestMatchWithVariants", error);
1390
1411
  }
1391
1412
  }
1413
+ /** End the current recognition run without removing loaded entries.
1414
+ * Pending queries from the previous run cannot update event suppression state.
1415
+ */
1416
+ resetRecognitionState() {
1417
+ this.recognitionGeneration += 1;
1418
+ this.distinctMatchDeduper.reset();
1419
+ this.matcher?.resetDistinctMatchState?.();
1420
+ }
1392
1421
  destroy() {
1393
1422
  if (this.destroyed) {
1394
1423
  return;
1395
1424
  }
1425
+ this.resetRecognitionState();
1396
1426
  this.destroyed = true;
1397
1427
  this.matcher?.destroy();
1398
1428
  this.database?.destroy();
@@ -1456,7 +1486,10 @@ var MatcherSession = class {
1456
1486
  }
1457
1487
  return this.database;
1458
1488
  }
1459
- emitDistinctMatchEvent(best, queryByteLength, results) {
1489
+ emitDistinctMatchEvent(generation, best, queryByteLength, results) {
1490
+ if (generation !== this.recognitionGeneration || this.destroyed) {
1491
+ return;
1492
+ }
1460
1493
  const observation = this.distinctMatchDeduper.observe(best);
1461
1494
  if (observation.kind === "match") {
1462
1495
  this.events.emit("match", { results, best: observation.best });
@@ -1489,6 +1522,7 @@ export {
1489
1522
  DEFAULT_MAX_AUDIO_MARKER_SAMPLES,
1490
1523
  DEFAULT_MAX_AUDIO_MARKER_WORK_UNITS,
1491
1524
  DEFAULT_MAX_PACK_BYTES,
1525
+ MATCH_CONTINUITY_GAP_MS,
1492
1526
  MatcherSession,
1493
1527
  __resetMatcherWasmForTests,
1494
1528
  __setMatcherEmbeddedPackForTests,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sorisdk/matcher",
3
- "version": "0.6.7",
3
+ "version": "0.6.9",
4
4
  "description": "Browser WASM SDK for audio fingerprint matching",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",