@sorisdk/web-audio 0.6.5 → 0.6.7

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
@@ -22,7 +22,7 @@ configuration, or Node-based asset server is required:
22
22
  ```html
23
23
  <script type="module">
24
24
  import { AudioRecognizer } from
25
- "https://cdn.iplateia.com/web/sorisdk/v0.6.5/sori-web-audio.mjs";
25
+ "https://cdn.iplateia.com/web/sorisdk/v0.6.7/sori-web-audio.mjs";
26
26
 
27
27
  const recognizer = new AudioRecognizer({
28
28
  appId: "YOUR_APP_ID",
@@ -49,13 +49,39 @@ configuration, or Node-based asset server is required:
49
49
  ```
50
50
 
51
51
  Static URL imports are valid inside `<script type="module">`. Dynamic
52
- `await import("https://cdn.iplateia.com/web/sorisdk/v0.6.5/sori-web-audio.mjs")`
52
+ `await import("https://cdn.iplateia.com/web/sorisdk/v0.6.7/sori-web-audio.mjs")`
53
53
  is an optional alternative when the SDK should be loaded conditionally.
54
54
 
55
55
  Pin an exact version in production. Do not construct a mutable `latest` URL.
56
56
  The module loads its versioned WASM assets and the shared
57
57
  `https://cdn.iplateia.com/web/sorisdk/model.pack` automatically.
58
58
 
59
+ The fixed model response and caller-supplied AudioPacks use the matcher's same
60
+ 64 MiB browser transport limit by default. A valid oversized `Content-Length`
61
+ is rejected before the body is consumed, and an unknown-length response is
62
+ canceled on the first chunk that crosses the limit. The response is never
63
+ truncated; failures use the deterministic
64
+ `Pack source exceeds maximum size of <limit> bytes` error.
65
+
66
+ Set an intentional shared override through the matcher options when a larger
67
+ pack is required:
68
+
69
+ ```ts
70
+ const recognizer = new AudioRecognizer({
71
+ appId: "YOUR_APP_ID",
72
+ ephemeralKey,
73
+ wasm: {
74
+ matcher: {
75
+ maxPackBytes: 96 * 1024 * 1024
76
+ }
77
+ }
78
+ });
79
+ ```
80
+
81
+ The override applies to both the standalone fixed-model response and ordinary
82
+ pack sources. A caller-supplied `embeddedPackLoader` remains a zero-argument
83
+ callback and is not replaced by the standalone loader.
84
+
59
85
  The standalone module removes the frontend build/server requirement, but it
60
86
  does not remove the trusted server requirement for ephemeral credentials.
61
87
  Provide a backend or serverless endpoint for the `ephemeralKey` callback, and
@@ -93,7 +119,7 @@ An import map can give the standalone URL the npm package name:
93
119
  <script type="importmap">
94
120
  {
95
121
  "imports": {
96
- "@sorisdk/web-audio": "https://cdn.iplateia.com/web/sorisdk/v0.6.5/sori-web-audio.mjs"
122
+ "@sorisdk/web-audio": "https://cdn.iplateia.com/web/sorisdk/v0.6.7/sori-web-audio.mjs"
97
123
  }
98
124
  }
99
125
  </script>
@@ -161,23 +187,61 @@ await recognizer.destroy();
161
187
 
162
188
  ## Activity refinement
163
189
 
164
- The default `AudioRecognizer` activity reporter represents one fingerprint
165
- recognition and its later audio-marker enrichment as one server activity:
166
-
167
- 1. A marker-free match with an active marker-scan request is created with
168
- `POST /api/activity/` and top-level `refinement_expected: true`.
169
- 2. The returned `activity_id` is retained for 30 seconds.
170
- 3. If the same recognition gains `audioMarker.code`, the SDK sends
190
+ The default `AudioRecognizer` activity reporter represents one ordered,
191
+ continuous material segment and its later audio-marker enrichment as one
192
+ server activity:
193
+
194
+ 1. When the coordinator accepts a successful material match, it captures one
195
+ UTC RFC 3339 `recognized_at` value before any asynchronous request mapping,
196
+ header, token, or fetch work.
197
+ 2. The initial `POST /api/activity/` includes that timestamp and the exact
198
+ `DetectedMatch.position` integer in milliseconds. With valid
199
+ `matcherSegments` metadata, the position is local to the matched segment.
200
+ 3. A marker-free match with an active marker-scan request also includes
201
+ top-level `refinement_expected: true`.
202
+ 4. The returned `activity_id` is retained for 30 seconds.
203
+ 5. If the same recognition gains `audioMarker.code`, the SDK sends
171
204
  `PUT /api/activity/{activity_id}` with the same material and
172
205
  `trait.marker`.
173
- 4. The refined `campaign` event keeps the same non-empty `activityId`, so an
206
+ 6. The refined `campaign` event keeps the same non-empty `activityId`, so an
174
207
  application can replace the earlier campaign row deterministically.
175
208
 
209
+ A consecutive match for the same material and fingerprint type renews a local
210
+ 30-second continuity lease without another POST or campaign event. That local
211
+ lease can keep one segment alive beyond the initial server refinement window,
212
+ but it never extends the original PUT deadline. A marker confirmed anywhere
213
+ inside the segment is sticky: the first confirmation can issue at most one PUT,
214
+ and later marker misses or repeated marker evidence issue no mutation or
215
+ downgrade.
216
+
176
217
  A marker present on the first match is included in the POST and does not cause
177
- a PUT or include `refinement_expected`. Recognition without an active marker
178
- scan, plus legacy/custom POST-only reporters, also omit the opt-in flag.
179
- Marker-state `audiomarker` events remain independently observable; they do not
180
- mean that server activity refinement completed.
218
+ a PUT or include `refinement_expected`; that marker-first segment is still
219
+ retained for continuity. Recognition without an active marker scan, plus
220
+ legacy/custom POST-only reporters, also omit the opt-in flag. Custom reporters
221
+ enable segment coalescing only when `refinement` is enabled, preserving legacy
222
+ POST-only behavior otherwise.
223
+
224
+ A different material, an emitted `nomatch`, a gap beyond the continuity lease,
225
+ or stop, clear, destroy, authentication/capture replacement, or reset seals the
226
+ segment. Returning to A after B always creates a new A activity and can never
227
+ refine the earlier A. Marker evidence arriving after a segment is sealed is
228
+ ignored for that segment; create or PUT transport that already started may
229
+ finish under the existing capture, deadline, and cancellation guards.
230
+
231
+ Ordinary fingerprint `match` and marker-state `audiomarker` events remain
232
+ independently observable; coalescing only changes activity mutation and
233
+ campaign delivery. An `audiomarker` event alone does not mean that server
234
+ activity refinement completed.
235
+
236
+ Recognition timing belongs only to the segment's first material POST. Later
237
+ same-material observations update only the internal `lastObservedAt` lease;
238
+ they never replace the first `recognized_at` or `position`. A later marker PUT
239
+ does not include either field, and marker-only events do not create timing
240
+ metadata. `recognized_at` identifies first SDK match acceptance rather than
241
+ HTTP receipt, marker confirmation, refinement, or webhook delivery time. Do not
242
+ derive playback start as `recognized_at - position`: the match is accepted
243
+ after its query window was collected, so that calculation lacks the query
244
+ window's wall-clock anchor.
181
245
 
182
246
  Network and 5xx PUT failures are retried at most twice within the original
183
247
  30-second deadline. Server responses 404, 409, and 410 are terminal. An
@@ -211,6 +275,31 @@ mapping, endpoint, headers, fetcher, and retry policy. The stable terminal
211
275
  statuses 404, 409, and 410 are never retried even if a custom policy returns
212
276
  `true`.
213
277
 
278
+ `recognized_at` and `position` are optional public request properties for
279
+ source compatibility, but they are SDK-owned transport values. After a custom
280
+ POST mapper returns an eligible material payload, the SDK overwrites any
281
+ conflicting values with the captured recognition timestamp and exact matcher
282
+ position. The SDK removes both properties from all refinement PUT payloads,
283
+ including custom refinement mappings. Existing one-argument POST mappers and
284
+ POST-only transports require no signature changes. Activity responses and
285
+ `campaign` events do not gain timing properties from this request contract.
286
+
287
+ ## API endpoint normalization
288
+
289
+ `apiEndpoint` continues to accept a `string` or `URL`. When the SDK derives the
290
+ default authentication and activity URLs, it removes all trailing ASCII `/`
291
+ characters and appends `/auth` or `/activity/` with the existing URL shapes.
292
+
293
+ The endpoint is limited to `MAX_API_ENDPOINT_LENGTH` (32,768 UTF-16 code units).
294
+ The first code unit over that limit throws
295
+ `AudioRecognizer apiEndpoint exceeds maximum length of 32768 UTF-16 code units`
296
+ before trailing-slash scanning or endpoint joining. The SDK rejects the input;
297
+ it never truncates it. An explicitly supplied `apiEndpoint` is length-validated
298
+ during construction even when custom `authEndpoint` and `activityEndpoint`
299
+ values mean its prefix is not used for either join. This resource bound limits
300
+ construction work only. It is not an authentication, authorization, quota, or
301
+ abuse-prevention control.
302
+
214
303
  ## Session identifiers
215
304
 
216
305
  `AudioRecognizer` creates one pseudonymous session identifier per application
@@ -296,6 +385,21 @@ const recognizer = new AudioRecognizer({
296
385
  });
297
386
  ```
298
387
 
388
+ Advanced callers that import `@sorisdk/web-audio/generated/window.js`
389
+ directly still use `appendFingerprint(bytes: Uint8Array): void`. Each generated
390
+ window exposes `maxAppendFingerprintByteLength()`, which is the byte length of
391
+ one complete query for that instance. An append exactly at that limit is
392
+ accepted. A larger append throws a `RangeError` before the JavaScript array is
393
+ copied into WASM, with this deterministic message:
394
+
395
+ ```text
396
+ Fingerprint append byteLength <actual> exceeds the per-instance limit of <limit> bytes
397
+ ```
398
+
399
+ The high-level SDK enforces the same boundary before it invokes the generated
400
+ class. Oversized input is rejected, never truncated or split; normal microphone
401
+ fingerprint chunks and the existing match-window cadence are unchanged.
402
+
299
403
  ## Integration flow
300
404
 
301
405
  1. Prepare your `appId` and ephemeral key flow.
@@ -6,6 +6,7 @@ export class WasmFingerprintMatchWindow {
6
6
  [Symbol.dispose](): void;
7
7
  appendFingerprint(bytes: Uint8Array): void;
8
8
  hasReadyQuery(): boolean;
9
+ maxAppendFingerprintByteLength(): number;
9
10
  constructor(sample_rate: number, afpgen_config: any | null | undefined, match_window_ms: number, match_stride_ms: number);
10
11
  reset(): void;
11
12
  takeReadyQuery(): any;
@@ -13,9 +13,10 @@ export class WasmFingerprintMatchWindow {
13
13
  * @param {Uint8Array} bytes
14
14
  */
15
15
  appendFingerprint(bytes) {
16
- const ptr0 = passArray8ToWasm0(bytes, wasm.__wbindgen_malloc);
17
- const len0 = WASM_VECTOR_LEN;
18
- wasm.wasmfingerprintmatchwindow_appendFingerprint(this.__wbg_ptr, ptr0, len0);
16
+ const ret = wasm.wasmfingerprintmatchwindow_appendFingerprint(this.__wbg_ptr, bytes);
17
+ if (ret[1]) {
18
+ throw takeFromExternrefTable0(ret[0]);
19
+ }
19
20
  }
20
21
  /**
21
22
  * @returns {boolean}
@@ -24,6 +25,13 @@ export class WasmFingerprintMatchWindow {
24
25
  const ret = wasm.wasmfingerprintmatchwindow_hasReadyQuery(this.__wbg_ptr);
25
26
  return ret !== 0;
26
27
  }
28
+ /**
29
+ * @returns {number}
30
+ */
31
+ maxAppendFingerprintByteLength() {
32
+ const ret = wasm.wasmfingerprintmatchwindow_maxAppendFingerprintByteLength(this.__wbg_ptr);
33
+ return ret >>> 0;
34
+ }
27
35
  /**
28
36
  * @param {number} sample_rate
29
37
  * @param {any | null | undefined} afpgen_config
@@ -47,7 +55,10 @@ export class WasmFingerprintMatchWindow {
47
55
  */
48
56
  takeReadyQuery() {
49
57
  const ret = wasm.wasmfingerprintmatchwindow_takeReadyQuery(this.__wbg_ptr);
50
- return ret;
58
+ if (ret[2]) {
59
+ throw takeFromExternrefTable0(ret[1]);
60
+ }
61
+ return takeFromExternrefTable0(ret[0]);
51
62
  }
52
63
  }
53
64
  if (Symbol.dispose) WasmFingerprintMatchWindow.prototype[Symbol.dispose] = WasmFingerprintMatchWindow.prototype.free;
@@ -82,6 +93,10 @@ export function __wbg___wbindgen_in_a5d8b22e52b24dd1(arg0, arg1) {
82
93
  const ret = arg0 in arg1;
83
94
  return ret;
84
95
  }
96
+ export function __wbg___wbindgen_is_function_3baa9db1a987f47d(arg0) {
97
+ const ret = typeof(arg0) === 'function';
98
+ return ret;
99
+ }
85
100
  export function __wbg___wbindgen_is_null_52ff4ec04186736f(arg0) {
86
101
  const ret = arg0 === null;
87
102
  return ret;
@@ -116,6 +131,26 @@ export function __wbg___wbindgen_string_get_7ed5322991caaec5(arg0, arg1) {
116
131
  export function __wbg___wbindgen_throw_6b64449b9b9ed33c(arg0, arg1) {
117
132
  throw new Error(getStringFromWasm0(arg0, arg1));
118
133
  }
134
+ export function __wbg_call_14b169f759b26747() { return handleError(function (arg0, arg1) {
135
+ const ret = arg0.call(arg1);
136
+ return ret;
137
+ }, arguments); }
138
+ export function __wbg_call_a24592a6f349a97e() { return handleError(function (arg0, arg1, arg2) {
139
+ const ret = arg0.call(arg1, arg2);
140
+ return ret;
141
+ }, arguments); }
142
+ export function __wbg_getOwnPropertyDescriptor_131bd582a45a6f5d(arg0, arg1) {
143
+ const ret = Object.getOwnPropertyDescriptor(arg0, arg1);
144
+ return ret;
145
+ }
146
+ export function __wbg_getPrototypeOf_a56da9261bbd1e5b(arg0) {
147
+ const ret = Object.getPrototypeOf(arg0);
148
+ return ret;
149
+ }
150
+ export function __wbg_get_6011fa3a58f61074() { return handleError(function (arg0, arg1) {
151
+ const ret = Reflect.get(arg0, arg1);
152
+ return ret;
153
+ }, arguments); }
119
154
  export function __wbg_get_with_ref_key_6412cf3094599694(arg0, arg1) {
120
155
  const ret = arg0[arg1];
121
156
  return ret;
@@ -152,6 +187,10 @@ export function __wbg_new_0c7403db6e782f19(arg0) {
152
187
  const ret = new Uint8Array(arg0);
153
188
  return ret;
154
189
  }
190
+ export function __wbg_new_191521fecb171639(arg0, arg1) {
191
+ const ret = new RangeError(getStringFromWasm0(arg0, arg1));
192
+ return ret;
193
+ }
155
194
  export function __wbg_new_from_slice_b5ea43e23f6008c0(arg0, arg1) {
156
195
  const ret = new Uint8Array(getArrayU8FromWasm0(arg0, arg1));
157
196
  return ret;
@@ -159,7 +198,28 @@ export function __wbg_new_from_slice_b5ea43e23f6008c0(arg0, arg1) {
159
198
  export function __wbg_prototypesetcall_a6b02eb00b0f4ce2(arg0, arg1, arg2) {
160
199
  Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2);
161
200
  }
201
+ export function __wbg_static_accessor_GLOBAL_8cfadc87a297ca02() {
202
+ const ret = typeof global === 'undefined' ? null : global;
203
+ return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
204
+ }
205
+ export function __wbg_static_accessor_GLOBAL_THIS_602256ae5c8f42cf() {
206
+ const ret = typeof globalThis === 'undefined' ? null : globalThis;
207
+ return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
208
+ }
209
+ export function __wbg_static_accessor_SELF_e445c1c7484aecc3() {
210
+ const ret = typeof self === 'undefined' ? null : self;
211
+ return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
212
+ }
213
+ export function __wbg_static_accessor_WINDOW_f20e8576ef1e0f17() {
214
+ const ret = typeof window === 'undefined' ? null : window;
215
+ return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
216
+ }
162
217
  export function __wbindgen_cast_0000000000000001(arg0, arg1) {
218
+ // Cast intrinsic for `Ref(Slice(U8)) -> NamedExternref("Uint8Array")`.
219
+ const ret = getArrayU8FromWasm0(arg0, arg1);
220
+ return ret;
221
+ }
222
+ export function __wbindgen_cast_0000000000000002(arg0, arg1) {
163
223
  // Cast intrinsic for `Ref(String) -> Externref`.
164
224
  const ret = getStringFromWasm0(arg0, arg1);
165
225
  return ret;
@@ -274,15 +334,17 @@ function getUint8ArrayMemory0() {
274
334
  return cachedUint8ArrayMemory0;
275
335
  }
276
336
 
277
- function isLikeNone(x) {
278
- return x === undefined || x === null;
337
+ function handleError(f, args) {
338
+ try {
339
+ return f.apply(this, args);
340
+ } catch (e) {
341
+ const idx = addToExternrefTable0(e);
342
+ wasm.__wbindgen_exn_store(idx);
343
+ }
279
344
  }
280
345
 
281
- function passArray8ToWasm0(arg, malloc) {
282
- const ptr = malloc(arg.length * 1, 1) >>> 0;
283
- getUint8ArrayMemory0().set(arg, ptr / 1);
284
- WASM_VECTOR_LEN = arg.length;
285
- return ptr;
346
+ function isLikeNone(x) {
347
+ return x === undefined || x === null;
286
348
  }
287
349
 
288
350
  function passStringToWasm0(arg, malloc, realloc) {
Binary file
@@ -2,14 +2,16 @@
2
2
  /* eslint-disable */
3
3
  export const memory: WebAssembly.Memory;
4
4
  export const __wbg_wasmfingerprintmatchwindow_free: (a: number, b: number) => void;
5
- export const wasmfingerprintmatchwindow_appendFingerprint: (a: number, b: number, c: number) => void;
5
+ export const wasmfingerprintmatchwindow_appendFingerprint: (a: number, b: any) => [number, number];
6
6
  export const wasmfingerprintmatchwindow_hasReadyQuery: (a: number) => number;
7
+ export const wasmfingerprintmatchwindow_maxAppendFingerprintByteLength: (a: number) => number;
7
8
  export const wasmfingerprintmatchwindow_new: (a: number, b: number, c: number, d: number) => [number, number, number];
8
9
  export const wasmfingerprintmatchwindow_reset: (a: number) => void;
9
- export const wasmfingerprintmatchwindow_takeReadyQuery: (a: number) => any;
10
+ export const wasmfingerprintmatchwindow_takeReadyQuery: (a: number) => [number, number, number];
10
11
  export const __wbindgen_malloc: (a: number, b: number) => number;
11
12
  export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
12
- export const __wbindgen_externrefs: WebAssembly.Table;
13
+ export const __wbindgen_exn_store: (a: number) => void;
13
14
  export const __externref_table_alloc: () => number;
15
+ export const __wbindgen_externrefs: WebAssembly.Table;
14
16
  export const __externref_table_dealloc: (a: number) => void;
15
17
  export const __wbindgen_start: () => void;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { MatcherEphemeralAuthOptions, InitMatcherOptions, MatcherEphemeralAuthResult, AudioMarkerDetection, PackSource, MatchResult, AudioFingerprintType } from '@sorisdk/matcher';
1
+ import { MatcherEphemeralAuthOptions, InitMatcherOptions, MatcherEphemeralAuthResult, AudioMarkerDetection, PackSource, MatchResult, MatcherSessionOptions, AudioFingerprintType } from '@sorisdk/matcher';
2
2
  export { AudioFingerprintType, AudioMarkerConfig, AudioMarkerDetection, PackSource } from '@sorisdk/matcher';
3
3
  import { InitAfpgenOptions } from '@sorisdk/afpgen';
4
4
 
@@ -62,6 +62,10 @@ interface AudioMarkerEvent {
62
62
  interface MaterialActivityRequestPayload {
63
63
  type: "material";
64
64
  material_id: string;
65
+ /** SDK-owned UTC recognition timestamp. Custom mapper values are overwritten on POST. */
66
+ recognized_at?: string;
67
+ /** SDK-owned matcher position in milliseconds. Custom mapper values are overwritten on POST. */
68
+ position?: number;
65
69
  refinement_expected?: true;
66
70
  trait?: {
67
71
  marker?: string;
@@ -107,7 +111,7 @@ interface MicrophoneMatcherActivityReporterOptions {
107
111
  }
108
112
  interface CreateMicrophoneMatcherWasmOptions {
109
113
  afpgen?: InitAfpgenOptions;
110
- matcher?: Omit<InitMatcherOptions, "auth">;
114
+ matcher?: Omit<MatcherSessionOptions, "auth">;
111
115
  matchWindow?: InitMatchWindowWasmOptions;
112
116
  }
113
117
  interface CreateMicrophoneMatcherOptions {
@@ -147,6 +151,7 @@ type MicrophoneMatcherListener<T extends MicrophoneMatcherEventName> = (payload:
147
151
  type Awaitable<T> = T | Promise<T>;
148
152
  type AudioRecognizerMatcherOptions = Omit<CreateMicrophoneMatcherOptions, "activityReporter" | "packSource">;
149
153
  declare const DEFAULT_SORI_API_ENDPOINT = "https://console.soriapi.com/api";
154
+ declare const MAX_API_ENDPOINT_LENGTH: number;
150
155
  interface AudioRecognizerAuthOptions {
151
156
  endpoint: string | URL;
152
157
  appId: string;
@@ -291,4 +296,4 @@ declare function requestMicrophoneStream(mediaDevices: MediaDevices, mediaConstr
291
296
  preferredSampleRate?: number;
292
297
  }): Promise<MediaStream>;
293
298
 
294
- export { ACTIVITY_REFINEMENT_WINDOW_MS, type ActivityRefinementFailureContext, type ActivityRefinementRequestContext, type AudioMarkerEvent, AudioRecognizer, type AudioRecognizerAuthOptions, type AudioRecognizerOptions, type AuthenticateAndLoadAudioPackOptions, type AuthenticateAndLoadAudioPackResult, type BrowserAudioPackLoader, type BrowserAudioPackState, type BrowserAudioPackStateStore, type BrowserSessionManager, type CreateLocalStorageAudioPackStoreOptions, type CreateLocalStorageSessionManagerOptions, type CreateMicrophoneMatcherOptions, type CreateMicrophoneMatcherWasmOptions, DEFAULT_BROWSER_AUDIOPACK_VERSION, DEFAULT_SORI_API_ENDPOINT, type DetectedMatch, type MaterialActivityRequestPayload, MicrophoneMatcher, type MicrophoneMatcherActivityRefinementOptions, type MicrophoneMatcherActivityReporterOptions, type MicrophoneMatcherCampaignEvent, type MicrophoneMatcherEventMap, type MicrophoneMatcherEventName, type MicrophoneMatcherListener, authenticateAndLoadAudioPack, createLocalStorageAudioPackStore, createLocalStorageSessionManager, createMicrophoneMatcher, requestMicrophoneStream };
299
+ export { ACTIVITY_REFINEMENT_WINDOW_MS, type ActivityRefinementFailureContext, type ActivityRefinementRequestContext, type AudioMarkerEvent, AudioRecognizer, type AudioRecognizerAuthOptions, type AudioRecognizerOptions, type AuthenticateAndLoadAudioPackOptions, type AuthenticateAndLoadAudioPackResult, type BrowserAudioPackLoader, type BrowserAudioPackState, type BrowserAudioPackStateStore, type BrowserSessionManager, type CreateLocalStorageAudioPackStoreOptions, type CreateLocalStorageSessionManagerOptions, type CreateMicrophoneMatcherOptions, type CreateMicrophoneMatcherWasmOptions, DEFAULT_BROWSER_AUDIOPACK_VERSION, DEFAULT_SORI_API_ENDPOINT, type DetectedMatch, MAX_API_ENDPOINT_LENGTH, type MaterialActivityRequestPayload, MicrophoneMatcher, type MicrophoneMatcherActivityRefinementOptions, type MicrophoneMatcherActivityReporterOptions, type MicrophoneMatcherCampaignEvent, type MicrophoneMatcherEventMap, type MicrophoneMatcherEventName, type MicrophoneMatcherListener, authenticateAndLoadAudioPack, createLocalStorageAudioPackStore, createLocalStorageSessionManager, createMicrophoneMatcher, requestMicrophoneStream };
package/dist/index.js CHANGED
@@ -181,6 +181,18 @@ function getBrowserMediaDevices() {
181
181
  }
182
182
 
183
183
  // src/match-window.ts
184
+ var FINGERPRINT_LEVEL_BYTE_LENGTH = 8;
185
+ function getTypedArrayByteLengthGetter() {
186
+ const getter = Object.getOwnPropertyDescriptor(
187
+ Object.getPrototypeOf(Uint8Array.prototype),
188
+ "byteLength"
189
+ )?.get;
190
+ if (!getter) {
191
+ throw new Error("Uint8Array byteLength intrinsic is unavailable");
192
+ }
193
+ return getter;
194
+ }
195
+ var typedArrayByteLengthGetter = getTypedArrayByteLengthGetter();
184
196
  var cachedBindings = null;
185
197
  var initPromise = null;
186
198
  var testInjectedWasmModule = null;
@@ -197,18 +209,46 @@ var defaultLoader = async () => {
197
209
  const loaderModule = await import("./default-loader.js");
198
210
  return loaderModule.default();
199
211
  };
212
+ function calculateMaxAppendFingerprintByteLength(options) {
213
+ const hopSize = options.afpgenConfig.hopSize ?? 1e3;
214
+ const levelCount = options.afpgenConfig.levelCount ?? 2;
215
+ const hopDurationMs = hopSize / options.sampleRate * 1e3;
216
+ const hopsInWindow = Math.ceil(options.matchWindowMs / hopDurationMs);
217
+ return Math.max(1, hopsInWindow) * levelCount * FINGERPRINT_LEVEL_BYTE_LENGTH;
218
+ }
219
+ function appendLimitError(byteLength, maxByteLength) {
220
+ return new RangeError(
221
+ `Fingerprint append byteLength ${byteLength} exceeds the per-instance limit of ${maxByteLength} bytes`
222
+ );
223
+ }
224
+ function intrinsicUint8ArrayByteLength(bytes) {
225
+ return typedArrayByteLengthGetter.call(bytes);
226
+ }
200
227
  var WrappedFingerprintMatchWindow = class {
201
- constructor(inner) {
228
+ constructor(inner, options) {
202
229
  this.inner = inner;
230
+ const maxLengthFn = getCallable(this.inner, [
231
+ "maxAppendFingerprintByteLength",
232
+ "max_append_fingerprint_byte_length"
233
+ ]);
234
+ this.maxAppendByteLength = maxLengthFn ? Number(maxLengthFn.call(this.inner)) : calculateMaxAppendFingerprintByteLength(options);
203
235
  }
204
236
  inner;
237
+ maxAppendByteLength;
205
238
  appendFingerprint(bytes) {
239
+ const byteLength = intrinsicUint8ArrayByteLength(bytes);
240
+ if (byteLength > this.maxAppendByteLength) {
241
+ throw appendLimitError(byteLength, this.maxAppendByteLength);
242
+ }
206
243
  const fn = getCallable(this.inner, ["appendFingerprint", "append_fingerprint"]);
207
244
  if (!fn) {
208
245
  throw new Error("web-audio wasm window is missing appendFingerprint");
209
246
  }
210
247
  fn.call(this.inner, bytes);
211
248
  }
249
+ maxAppendFingerprintByteLength() {
250
+ return this.maxAppendByteLength;
251
+ }
212
252
  hasReadyQuery() {
213
253
  const fn = getCallable(this.inner, ["hasReadyQuery", "has_ready_query"]);
214
254
  if (!fn) {
@@ -261,7 +301,8 @@ function createBindingsFromModule(moduleLike) {
261
301
  options.afpgenConfig,
262
302
  options.matchWindowMs,
263
303
  options.matchStrideMs
264
- )
304
+ ),
305
+ options
265
306
  );
266
307
  }
267
308
  };
@@ -357,6 +398,14 @@ function defaultRequestPayload(match) {
357
398
  ...typeof marker === "string" && marker.length > 0 ? { trait: { marker } } : {}
358
399
  };
359
400
  }
401
+ function captureActivityRecognitionTiming(match) {
402
+ const recognizedAtMillis = Date.now();
403
+ const position = match.position;
404
+ return Object.freeze({
405
+ recognizedAt: new Date(recognizedAtMillis).toISOString(),
406
+ position
407
+ });
408
+ }
360
409
  function validateRequestPayload(payload) {
361
410
  if (payload === null) {
362
411
  return null;
@@ -373,7 +422,12 @@ async function resolveCreateRequestPayload(reporter, match) {
373
422
  if (!payload) {
374
423
  return null;
375
424
  }
376
- const { refinement_expected: _ignored, ...requestPayload } = payload;
425
+ const {
426
+ position: _ignoredPosition,
427
+ recognized_at: _ignoredRecognizedAt,
428
+ refinement_expected: _ignoredRefinementExpected,
429
+ ...requestPayload
430
+ } = payload;
377
431
  return requestPayload;
378
432
  }
379
433
  async function resolveRefineRequestPayload(reporter, match, context) {
@@ -387,7 +441,12 @@ async function resolveRefineRequestPayload(reporter, match, context) {
387
441
  if (!payload) {
388
442
  return null;
389
443
  }
390
- const { refinement_expected: _ignored, ...requestPayload } = payload;
444
+ const {
445
+ position: _ignoredPosition,
446
+ recognized_at: _ignoredRecognizedAt,
447
+ refinement_expected: _ignoredRefinementExpected,
448
+ ...requestPayload
449
+ } = payload;
391
450
  return requestPayload;
392
451
  }
393
452
  function defaultCampaignMapper(payload, match) {
@@ -457,10 +516,20 @@ function responseActivityId(data, campaign) {
457
516
  }
458
517
  async function mapCreateResponse(reporter, data, match, refinementExpected) {
459
518
  if (!data) {
460
- return { activityId: null, campaign: null, refinementExpected };
519
+ return {
520
+ activityId: null,
521
+ campaign: null,
522
+ creationAttempted: true,
523
+ refinementExpected
524
+ };
461
525
  }
462
526
  const campaign = reporter.mapResponseToCampaign ? await reporter.mapResponseToCampaign(data, match) : defaultCampaignMapper(data, match);
463
- return { activityId: responseActivityId(data, campaign), campaign, refinementExpected };
527
+ return {
528
+ activityId: responseActivityId(data, campaign),
529
+ campaign,
530
+ creationAttempted: true,
531
+ refinementExpected
532
+ };
464
533
  }
465
534
  async function resolveRefinementEndpoint(reporter, activityId) {
466
535
  const configured = reporter.refinement && reporter.refinement.endpoint;
@@ -470,10 +539,15 @@ async function resolveRefinementEndpoint(reporter, activityId) {
470
539
  const prefix = String(configured ?? reporter.endpoint);
471
540
  return `${prefix.endsWith("/") ? prefix : `${prefix}/`}${encodeURIComponent(activityId)}`;
472
541
  }
473
- async function createMatchedMaterialActivity(reporter, match, refinementExpected = false) {
542
+ async function createMatchedMaterialActivity(reporter, match, refinementExpected = false, recognitionTiming = captureActivityRecognitionTiming(match)) {
474
543
  const payload = await resolveCreateRequestPayload(reporter, match);
475
544
  if (!payload) {
476
- return { activityId: null, campaign: null, refinementExpected: false };
545
+ return {
546
+ activityId: null,
547
+ campaign: null,
548
+ creationAttempted: false,
549
+ refinementExpected: false
550
+ };
477
551
  }
478
552
  const headers = await resolveHeaders(reporter);
479
553
  const actualMarker = match.audioMarker?.code;
@@ -481,7 +555,12 @@ async function createMatchedMaterialActivity(reporter, match, refinementExpected
481
555
  const hasMarker = typeof actualMarker === "string" && actualMarker.length > 0 || typeof payloadMarker === "string" && payloadMarker.length > 0;
482
556
  const lifecycleOpen = typeof refinementExpected === "function" ? refinementExpected() : refinementExpected;
483
557
  const requestRefinementExpected = Boolean(reporter.refinement && lifecycleOpen && !hasMarker);
484
- const requestPayload = requestRefinementExpected ? { ...payload, refinement_expected: true } : payload;
558
+ const requestPayload = {
559
+ ...payload,
560
+ recognized_at: recognitionTiming.recognizedAt,
561
+ position: recognitionTiming.position,
562
+ ...requestRefinementExpected ? { refinement_expected: true } : {}
563
+ };
485
564
  const response = await resolveFetch(reporter.fetch)(reporter.endpoint, {
486
565
  method: "POST",
487
566
  headers,
@@ -531,6 +610,7 @@ async function refineMatchedMaterialActivity(reporter, match, context, isActive
531
610
 
532
611
  // src/activity-refinement.ts
533
612
  var ACTIVITY_REFINEMENT_WINDOW_MS = 3e4;
613
+ var ACTIVITY_CONTINUITY_GAP_MS = 3e4;
534
614
  var DEFAULT_MAX_REFINEMENT_ATTEMPTS = 2;
535
615
  var MAX_REFINEMENT_ATTEMPTS = 10;
536
616
  function normalizeError(error) {
@@ -566,9 +646,6 @@ var ActivityRefinementCoordinator = class {
566
646
  }
567
647
  report(recognitionId, match, captureGeneration, requestId = null) {
568
648
  const coordinatorGeneration = this.generation;
569
- if (this.activeLifecycle && this.activeLifecycle.recognitionId !== recognitionId) {
570
- this.deleteLifecycle(this.activeLifecycle);
571
- }
572
649
  const hasMarker = typeof match.audioMarker?.code === "string" && match.audioMarker.code.length > 0;
573
650
  const refinement = this.reporter.refinement;
574
651
  const requestCanRefine = requestId !== null && !this.settledRequests.has(requestId);
@@ -578,39 +655,71 @@ var ActivityRefinementCoordinator = class {
578
655
  this.registeredRequests.delete(requestId);
579
656
  this.settledRequests.delete(requestId);
580
657
  }
658
+ if (refinement && this.activeLifecycle) {
659
+ const now = this.now();
660
+ const activeLifecycle = this.activeLifecycle;
661
+ if (this.isContinuousMatch(activeLifecycle, match, captureGeneration, now)) {
662
+ this.renewContinuity(activeLifecycle, now);
663
+ if (requestId !== null) {
664
+ if (requestCanRefine && this.canAcceptMarker(activeLifecycle)) {
665
+ this.requestLifecycles.set(requestId, activeLifecycle);
666
+ } else {
667
+ this.requestLifecycles.delete(requestId);
668
+ }
669
+ }
670
+ if (pendingMarker) {
671
+ this.claimMarker(activeLifecycle, pendingMarker);
672
+ }
673
+ if (hasMarker && match.audioMarker) {
674
+ this.claimMarker(activeLifecycle, match.audioMarker);
675
+ }
676
+ return;
677
+ }
678
+ this.sealLifecycle(activeLifecycle);
679
+ }
581
680
  const refinementExpected = Boolean(refinement && !hasMarker && requestCanRefine);
582
681
  let lifecycle = null;
583
- if (refinementExpected && refinement) {
584
- const now = refinement.now?.() ?? Date.now();
682
+ if (refinement) {
683
+ const now = this.now();
585
684
  lifecycle = {
586
685
  recognitionId,
587
686
  captureGeneration,
588
687
  match,
589
688
  deadline: now + ACTIVITY_REFINEMENT_WINDOW_MS,
689
+ lastObservedAt: now,
590
690
  activityId: null,
591
- markerMatch: null,
592
- markerCode: null,
593
- timer: null,
691
+ markerMatch: hasMarker ? match : null,
692
+ markerCode: hasMarker ? match.audioMarker?.code ?? null : null,
693
+ refinementTimer: null,
694
+ continuityTimer: null,
594
695
  createSettled: false,
595
- refinementStarted: false
696
+ refinementStarted: false,
697
+ refinementInFlight: false,
698
+ refinementClosed: !refinementExpected,
699
+ sealed: false
596
700
  };
597
701
  const createdLifecycle = lifecycle;
598
- lifecycle.timer = setTimeout(() => {
599
- this.deleteLifecycle(createdLifecycle);
600
- }, ACTIVITY_REFINEMENT_WINDOW_MS);
702
+ if (refinementExpected) {
703
+ lifecycle.refinementTimer = setTimeout(() => {
704
+ this.closeRefinement(createdLifecycle);
705
+ }, ACTIVITY_REFINEMENT_WINDOW_MS);
706
+ }
707
+ this.scheduleContinuityExpiry(lifecycle);
601
708
  this.lifecycles.set(recognitionId, lifecycle);
602
- if (requestId !== null) {
709
+ if (requestId !== null && refinementExpected) {
603
710
  this.requestLifecycles.set(requestId, lifecycle);
604
711
  }
605
712
  this.activeLifecycle = lifecycle;
606
- if (pendingMarker) {
607
- this.refine(requestId ?? recognitionId, pendingMarker, captureGeneration);
713
+ if (pendingMarker && refinementExpected) {
714
+ this.claimMarker(lifecycle, pendingMarker);
608
715
  }
609
716
  }
717
+ const recognitionTiming = captureActivityRecognitionTiming(match);
610
718
  void createMatchedMaterialActivity(
611
719
  this.reporter,
612
720
  match,
613
- () => Boolean(lifecycle && this.isRefinementActive(lifecycle))
721
+ () => Boolean(lifecycle && this.isRefinementActive(lifecycle)),
722
+ recognitionTiming
614
723
  ).then(
615
724
  (result) => {
616
725
  if (coordinatorGeneration === this.generation && result.campaign && this.callbacks.isCurrentCapture(captureGeneration)) {
@@ -619,12 +728,17 @@ var ActivityRefinementCoordinator = class {
619
728
  if (coordinatorGeneration !== this.generation || !lifecycle || this.lifecycles.get(recognitionId) !== lifecycle) {
620
729
  return;
621
730
  }
622
- if (!result.refinementExpected) {
731
+ lifecycle.createSettled = true;
732
+ lifecycle.activityId = result.activityId;
733
+ if (!result.creationAttempted) {
623
734
  this.deleteLifecycle(lifecycle);
624
735
  return;
625
736
  }
626
- lifecycle.createSettled = true;
627
- lifecycle.activityId = result.activityId;
737
+ if (!result.refinementExpected) {
738
+ this.closeRefinement(lifecycle);
739
+ this.maybeDeleteSealed(lifecycle);
740
+ return;
741
+ }
628
742
  if (lifecycle.markerMatch) {
629
743
  if (lifecycle.activityId) {
630
744
  this.startRefinement(lifecycle);
@@ -632,9 +746,11 @@ var ActivityRefinementCoordinator = class {
632
746
  this.failMissingActivityId(lifecycle);
633
747
  }
634
748
  }
749
+ this.maybeDeleteSealed(lifecycle);
635
750
  },
636
751
  (error) => {
637
752
  if (lifecycle) {
753
+ lifecycle.createSettled = true;
638
754
  this.deleteLifecycle(lifecycle);
639
755
  }
640
756
  if (coordinatorGeneration === this.generation && this.callbacks.isCurrentCapture(captureGeneration)) {
@@ -646,14 +762,26 @@ var ActivityRefinementCoordinator = class {
646
762
  associate(requestId, match, captureGeneration) {
647
763
  const lifecycle = this.activeLifecycle;
648
764
  this.registeredRequests.delete(requestId);
649
- if (!lifecycle || !match || lifecycle.captureGeneration !== captureGeneration || lifecycle.match.name !== match.name || lifecycle.match.afpType !== match.afpType || !this.isActive(lifecycle)) {
765
+ if (!lifecycle || !match) {
766
+ return false;
767
+ }
768
+ const now = this.now();
769
+ if (!this.isContinuousMatch(lifecycle, match, captureGeneration, now)) {
770
+ this.sealLifecycle(lifecycle);
650
771
  return false;
651
772
  }
652
- this.requestLifecycles.set(requestId, lifecycle);
773
+ this.renewContinuity(lifecycle, now);
653
774
  const pendingMarker = this.pendingMarkers.get(requestId);
654
- if (pendingMarker) {
775
+ if (this.canAcceptMarker(lifecycle)) {
776
+ this.requestLifecycles.set(requestId, lifecycle);
777
+ } else {
778
+ this.requestLifecycles.delete(requestId);
779
+ }
780
+ if (pendingMarker && this.canAcceptMarker(lifecycle)) {
781
+ this.pendingMarkers.delete(requestId);
782
+ this.claimMarker(lifecycle, pendingMarker);
783
+ } else if (pendingMarker) {
655
784
  this.pendingMarkers.delete(requestId);
656
- this.refine(requestId, pendingMarker, captureGeneration);
657
785
  }
658
786
  return true;
659
787
  }
@@ -666,18 +794,14 @@ var ActivityRefinementCoordinator = class {
666
794
  settleRequest(requestId) {
667
795
  const awaitingAssociation = this.registeredRequests.delete(requestId);
668
796
  this.pendingMarkers.delete(requestId);
669
- const lifecycle = this.requestLifecycles.get(requestId);
670
- if (lifecycle) {
671
- this.deleteLifecycle(lifecycle);
672
- return;
673
- }
797
+ this.requestLifecycles.delete(requestId);
674
798
  if (awaitingAssociation) {
675
799
  this.settledRequests.add(requestId);
676
800
  }
677
801
  }
678
802
  clearActive(captureGeneration) {
679
803
  if (this.activeLifecycle?.captureGeneration === captureGeneration) {
680
- this.deleteLifecycle(this.activeLifecycle);
804
+ this.sealLifecycle(this.activeLifecycle);
681
805
  }
682
806
  }
683
807
  refine(recognitionId, marker, captureGeneration) {
@@ -696,24 +820,19 @@ var ActivityRefinementCoordinator = class {
696
820
  }
697
821
  return;
698
822
  }
699
- const now = this.reporter.refinement?.now?.() ?? Date.now();
700
- if (now > lifecycle.deadline) {
701
- this.deleteLifecycle(lifecycle);
823
+ if (!this.canAcceptMarker(lifecycle)) {
702
824
  return;
703
825
  }
704
- lifecycle.markerCode = markerCode;
705
- lifecycle.markerMatch = { ...lifecycle.match, audioMarker: marker };
706
- if (lifecycle.activityId) {
707
- this.startRefinement(lifecycle);
708
- } else if (lifecycle.createSettled) {
709
- this.failMissingActivityId(lifecycle);
710
- }
826
+ this.claimMarker(lifecycle, marker);
711
827
  }
712
828
  reset() {
713
829
  this.generation += 1;
714
830
  for (const lifecycle of this.lifecycles.values()) {
715
- if (lifecycle.timer) {
716
- clearTimeout(lifecycle.timer);
831
+ if (lifecycle.refinementTimer) {
832
+ clearTimeout(lifecycle.refinementTimer);
833
+ }
834
+ if (lifecycle.continuityTimer) {
835
+ clearTimeout(lifecycle.continuityTimer);
717
836
  }
718
837
  }
719
838
  this.lifecycles.clear();
@@ -724,17 +843,21 @@ var ActivityRefinementCoordinator = class {
724
843
  this.activeLifecycle = null;
725
844
  }
726
845
  startRefinement(lifecycle) {
727
- if (lifecycle.refinementStarted || !lifecycle.activityId || !lifecycle.markerMatch) {
846
+ if (lifecycle.refinementStarted || lifecycle.refinementClosed || !lifecycle.activityId || !lifecycle.markerMatch) {
728
847
  return;
729
848
  }
730
849
  lifecycle.refinementStarted = true;
731
- void this.runRefinement(lifecycle);
850
+ lifecycle.refinementInFlight = true;
851
+ void this.runRefinement(lifecycle).finally(() => {
852
+ lifecycle.refinementInFlight = false;
853
+ this.maybeDeleteSealed(lifecycle);
854
+ });
732
855
  }
733
856
  failMissingActivityId(lifecycle) {
734
857
  if (!this.isRefinementActive(lifecycle)) {
735
858
  return;
736
859
  }
737
- this.deleteLifecycle(lifecycle);
860
+ this.closeRefinement(lifecycle);
738
861
  this.callbacks.emitError(
739
862
  "activity-refinement",
740
863
  new Error("Activity refinement requires a non-empty activity_id from the create response")
@@ -743,7 +866,7 @@ var ActivityRefinementCoordinator = class {
743
866
  async runRefinement(lifecycle) {
744
867
  const refinement = this.reporter.refinement;
745
868
  if (!refinement || !lifecycle.activityId || !lifecycle.markerMatch) {
746
- this.deleteLifecycle(lifecycle);
869
+ this.closeRefinement(lifecycle);
747
870
  return;
748
871
  }
749
872
  const maxAttempts = Math.min(
@@ -754,12 +877,12 @@ var ActivityRefinementCoordinator = class {
754
877
  )
755
878
  );
756
879
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
757
- if (!this.isActive(lifecycle)) {
880
+ if (!this.isRefinementActive(lifecycle)) {
758
881
  return;
759
882
  }
760
- const now = refinement.now?.() ?? Date.now();
883
+ const now = this.now();
761
884
  if (now > lifecycle.deadline) {
762
- this.deleteLifecycle(lifecycle);
885
+ this.closeRefinement(lifecycle);
763
886
  return;
764
887
  }
765
888
  try {
@@ -777,7 +900,7 @@ var ActivityRefinementCoordinator = class {
777
900
  if (!this.isRefinementActive(lifecycle)) {
778
901
  return;
779
902
  }
780
- this.deleteLifecycle(lifecycle);
903
+ this.closeRefinement(lifecycle);
781
904
  if (result.campaign) {
782
905
  this.callbacks.emitCampaign(result.campaign);
783
906
  }
@@ -807,7 +930,7 @@ var ActivityRefinementCoordinator = class {
807
930
  if (!this.isRefinementActive(lifecycle)) {
808
931
  return;
809
932
  }
810
- this.deleteLifecycle(lifecycle);
933
+ this.closeRefinement(lifecycle);
811
934
  this.callbacks.emitError("activity-refinement", policyError);
812
935
  return;
813
936
  }
@@ -817,35 +940,95 @@ var ActivityRefinementCoordinator = class {
817
940
  if (retry) {
818
941
  continue;
819
942
  }
820
- this.deleteLifecycle(lifecycle);
943
+ this.closeRefinement(lifecycle);
821
944
  this.callbacks.emitError("activity-refinement", error);
822
945
  return;
823
946
  }
824
947
  }
825
948
  }
826
- isActive(lifecycle) {
949
+ isTransportActive(lifecycle) {
827
950
  return this.lifecycles.get(lifecycle.recognitionId) === lifecycle && this.callbacks.isCurrentCapture(lifecycle.captureGeneration);
828
951
  }
829
952
  isRefinementActive(lifecycle) {
830
- if (!this.isActive(lifecycle)) {
953
+ if (!this.isTransportActive(lifecycle) || lifecycle.refinementClosed) {
831
954
  return false;
832
955
  }
833
- const refinement = this.reporter.refinement;
834
- const now = refinement && refinement.now ? refinement.now() : Date.now();
956
+ const now = this.now();
835
957
  if (now > lifecycle.deadline) {
836
- this.deleteLifecycle(lifecycle);
958
+ this.closeRefinement(lifecycle);
837
959
  return false;
838
960
  }
839
961
  return true;
840
962
  }
841
- deleteLifecycle(lifecycle) {
842
- if (lifecycle.timer) {
843
- clearTimeout(lifecycle.timer);
844
- lifecycle.timer = null;
963
+ canAcceptMarker(lifecycle) {
964
+ return lifecycle.markerCode === null && this.isRefinementActive(lifecycle);
965
+ }
966
+ claimMarker(lifecycle, marker) {
967
+ const markerCode = marker.code;
968
+ if (typeof markerCode !== "string" || markerCode.length === 0 || !this.canAcceptMarker(lifecycle)) {
969
+ return;
845
970
  }
846
- if (this.lifecycles.get(lifecycle.recognitionId) === lifecycle) {
847
- this.lifecycles.delete(lifecycle.recognitionId);
971
+ lifecycle.markerCode = markerCode;
972
+ lifecycle.markerMatch = { ...lifecycle.match, audioMarker: marker };
973
+ if (lifecycle.activityId) {
974
+ this.startRefinement(lifecycle);
975
+ } else if (lifecycle.createSettled) {
976
+ this.failMissingActivityId(lifecycle);
977
+ }
978
+ }
979
+ isContinuousMatch(lifecycle, match, captureGeneration, now) {
980
+ return !lifecycle.sealed && this.activeLifecycle === lifecycle && lifecycle.captureGeneration === captureGeneration && lifecycle.match.name === match.name && lifecycle.match.afpType === match.afpType && this.isTransportActive(lifecycle) && now - lifecycle.lastObservedAt <= ACTIVITY_CONTINUITY_GAP_MS;
981
+ }
982
+ renewContinuity(lifecycle, now) {
983
+ lifecycle.lastObservedAt = now;
984
+ this.scheduleContinuityExpiry(lifecycle);
985
+ }
986
+ scheduleContinuityExpiry(lifecycle) {
987
+ if (lifecycle.continuityTimer) {
988
+ clearTimeout(lifecycle.continuityTimer);
989
+ }
990
+ const remainingMs = lifecycle.lastObservedAt + ACTIVITY_CONTINUITY_GAP_MS - this.now();
991
+ lifecycle.continuityTimer = setTimeout(() => {
992
+ lifecycle.continuityTimer = null;
993
+ if (this.activeLifecycle !== lifecycle || lifecycle.sealed) {
994
+ return;
995
+ }
996
+ const now = this.now();
997
+ if (now - lifecycle.lastObservedAt <= ACTIVITY_CONTINUITY_GAP_MS) {
998
+ this.scheduleContinuityExpiry(lifecycle);
999
+ return;
1000
+ }
1001
+ this.sealLifecycle(lifecycle);
1002
+ }, Math.max(1, remainingMs + 1));
1003
+ }
1004
+ sealLifecycle(lifecycle) {
1005
+ if (lifecycle.sealed) {
1006
+ return;
848
1007
  }
1008
+ lifecycle.sealed = true;
1009
+ if (lifecycle.continuityTimer) {
1010
+ clearTimeout(lifecycle.continuityTimer);
1011
+ lifecycle.continuityTimer = null;
1012
+ }
1013
+ if (this.activeLifecycle === lifecycle) {
1014
+ this.activeLifecycle = null;
1015
+ }
1016
+ this.removeRequestAssociations(lifecycle);
1017
+ this.maybeDeleteSealed(lifecycle);
1018
+ }
1019
+ closeRefinement(lifecycle) {
1020
+ if (lifecycle.refinementClosed) {
1021
+ return;
1022
+ }
1023
+ lifecycle.refinementClosed = true;
1024
+ if (lifecycle.refinementTimer) {
1025
+ clearTimeout(lifecycle.refinementTimer);
1026
+ lifecycle.refinementTimer = null;
1027
+ }
1028
+ this.removeRequestAssociations(lifecycle);
1029
+ this.maybeDeleteSealed(lifecycle);
1030
+ }
1031
+ removeRequestAssociations(lifecycle) {
849
1032
  for (const [requestId, requestLifecycle] of this.requestLifecycles) {
850
1033
  if (requestLifecycle === lifecycle) {
851
1034
  this.requestLifecycles.delete(requestId);
@@ -853,6 +1036,28 @@ var ActivityRefinementCoordinator = class {
853
1036
  this.registeredRequests.delete(requestId);
854
1037
  }
855
1038
  }
1039
+ }
1040
+ maybeDeleteSealed(lifecycle) {
1041
+ if (lifecycle.sealed && lifecycle.createSettled && !lifecycle.refinementInFlight) {
1042
+ this.deleteLifecycle(lifecycle);
1043
+ }
1044
+ }
1045
+ now() {
1046
+ return this.reporter.refinement && this.reporter.refinement.now ? this.reporter.refinement.now() : Date.now();
1047
+ }
1048
+ deleteLifecycle(lifecycle) {
1049
+ if (lifecycle.refinementTimer) {
1050
+ clearTimeout(lifecycle.refinementTimer);
1051
+ lifecycle.refinementTimer = null;
1052
+ }
1053
+ if (lifecycle.continuityTimer) {
1054
+ clearTimeout(lifecycle.continuityTimer);
1055
+ lifecycle.continuityTimer = null;
1056
+ }
1057
+ if (this.lifecycles.get(lifecycle.recognitionId) === lifecycle) {
1058
+ this.lifecycles.delete(lifecycle.recognitionId);
1059
+ }
1060
+ this.removeRequestAssociations(lifecycle);
856
1061
  if (this.activeLifecycle === lifecycle) {
857
1062
  this.activeLifecycle = null;
858
1063
  }
@@ -1576,6 +1781,8 @@ function createLocalStorageSessionManager(options) {
1576
1781
 
1577
1782
  // src/audio-recognizer.ts
1578
1783
  var DEFAULT_SORI_API_ENDPOINT = "https://console.soriapi.com/api";
1784
+ var MAX_API_ENDPOINT_LENGTH = 32 * 1024;
1785
+ var ASCII_SLASH_CODE_UNIT = 47;
1579
1786
  var FORWARDED_EVENTS = [
1580
1787
  "ready",
1581
1788
  "microphoneready",
@@ -1603,12 +1810,24 @@ function ensureNonEmptyString(name, value) {
1603
1810
  }
1604
1811
  return value;
1605
1812
  }
1606
- function normalizeEndpointPrefix(endpoint) {
1607
- const value = String(endpoint).replace(/\/+$/, "");
1813
+ function stringifyAndValidateApiEndpoint(endpoint) {
1814
+ const value = String(endpoint);
1815
+ if (value.length > MAX_API_ENDPOINT_LENGTH) {
1816
+ throw new Error(
1817
+ `AudioRecognizer apiEndpoint exceeds maximum length of ${MAX_API_ENDPOINT_LENGTH} UTF-16 code units`
1818
+ );
1819
+ }
1608
1820
  return value;
1609
1821
  }
1822
+ function normalizeValidatedEndpointPrefix(value) {
1823
+ let end = value.length;
1824
+ while (end > 0 && value.charCodeAt(end - 1) === ASCII_SLASH_CODE_UNIT) {
1825
+ end -= 1;
1826
+ }
1827
+ return end === value.length ? value : value.slice(0, end);
1828
+ }
1610
1829
  function joinEndpoint(prefix, path) {
1611
- return `${normalizeEndpointPrefix(prefix)}${path.startsWith("/") ? path : `/${path}`}`;
1830
+ return `${prefix}${path.startsWith("/") ? path : `/${path}`}`;
1612
1831
  }
1613
1832
  function createEphemeralKeyResolver(endpoint, fetcher, requestInit) {
1614
1833
  return async () => {
@@ -1636,8 +1855,15 @@ function resolveRecognizerOptions(options) {
1636
1855
  const activityReportingDisabled = options.activityReporter === false;
1637
1856
  const customActivityReporter = activityReportingDisabled ? void 0 : options.activityReporter;
1638
1857
  const appId = legacyAuth?.appId ?? options.appId;
1639
- const authEndpoint = legacyAuth?.endpoint ?? options.authEndpoint ?? joinEndpoint(apiEndpoint, "/auth");
1640
- const activityEndpoint = activityReportingDisabled ? void 0 : customActivityReporter?.endpoint ?? options.activityEndpoint ?? joinEndpoint(apiEndpoint, "/activity/");
1858
+ const configuredAuthEndpoint = legacyAuth?.endpoint ?? options.authEndpoint;
1859
+ const configuredActivityEndpoint = activityReportingDisabled ? void 0 : customActivityReporter?.endpoint ?? options.activityEndpoint;
1860
+ const derivesAuthEndpoint = configuredAuthEndpoint == null;
1861
+ const derivesActivityEndpoint = !activityReportingDisabled && configuredActivityEndpoint == null;
1862
+ const derivesApiEndpoint = derivesAuthEndpoint || derivesActivityEndpoint;
1863
+ const validatedApiEndpoint = options.apiEndpoint !== void 0 || derivesApiEndpoint ? stringifyAndValidateApiEndpoint(apiEndpoint) : void 0;
1864
+ const normalizedApiEndpoint = derivesApiEndpoint ? normalizeValidatedEndpointPrefix(validatedApiEndpoint) : void 0;
1865
+ const authEndpoint = configuredAuthEndpoint ?? joinEndpoint(normalizedApiEndpoint, "/auth");
1866
+ const activityEndpoint = activityReportingDisabled ? void 0 : configuredActivityEndpoint ?? joinEndpoint(normalizedApiEndpoint, "/activity/");
1641
1867
  const ephemeralKey = legacyAuth?.ephemeralKey ?? options.ephemeralKey ?? options.ephemeraKey ?? (legacyAuth?.ephemeralKeyEndpoint ?? options.ephemeralKeyEndpoint ? createEphemeralKeyResolver(
1642
1868
  legacyAuth?.ephemeralKeyEndpoint ?? options.ephemeralKeyEndpoint,
1643
1869
  legacyAuth?.ephemeralKeyFetch ?? options.ephemeralKeyFetch ?? legacyAuth?.fetch ?? sharedFetch,
@@ -1825,6 +2051,7 @@ export {
1825
2051
  AudioRecognizer,
1826
2052
  DEFAULT_BROWSER_AUDIOPACK_VERSION,
1827
2053
  DEFAULT_SORI_API_ENDPOINT,
2054
+ MAX_API_ENDPOINT_LENGTH,
1828
2055
  MicrophoneMatcher,
1829
2056
  authenticateAndLoadAudioPack,
1830
2057
  createLocalStorageAudioPackStore,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sorisdk/web-audio",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
4
4
  "description": "Web SDK for browser-based audio recognition with SORI API",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -34,8 +34,8 @@
34
34
  "license": "SEE LICENSE IN LICENSE.md",
35
35
  "homepage": "https://docs.soriapi.com/ko/integration/web",
36
36
  "dependencies": {
37
- "@sorisdk/afpgen": "0.6.5",
38
- "@sorisdk/matcher": "0.6.5"
37
+ "@sorisdk/matcher": "0.6.7",
38
+ "@sorisdk/afpgen": "0.6.7"
39
39
  },
40
40
  "devDependencies": {
41
41
  "tsup": "^8.5.1",