@avsbhq/node 1.0.0 → 1.5.0

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/dist/index.js CHANGED
@@ -13,12 +13,266 @@ import {
13
13
  noopLogger
14
14
  } from "@avsbhq/core";
15
15
 
16
+ // src/datasets.ts
17
+ var DEFAULT_REQUEST_TIMEOUT = 1e4;
18
+ function miss(slug, version) {
19
+ return { found: false, meta: { slug, version } };
20
+ }
21
+ function createDatasetsClient(options) {
22
+ const { sdkKey, getDatafile, logger } = options;
23
+ const requestTimeout = options.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
24
+ async function get(slug, key) {
25
+ if (!slug || !key) {
26
+ logger.warn("datasets.get called with an empty slug or key \u2014 returning not found");
27
+ return miss(slug, null);
28
+ }
29
+ const datafile = getDatafile();
30
+ if (!datafile) {
31
+ logger.debug(`SDK not ready, returning not found for dataset "${slug}"`);
32
+ return miss(slug, null);
33
+ }
34
+ const dataset = datafile.datasets?.find((d) => d.slug === slug);
35
+ if (!dataset) {
36
+ logger.debug(`Dataset "${slug}" not present in datafile \u2014 returning not found`);
37
+ return miss(slug, null);
38
+ }
39
+ if (!dataset.liveVersion) {
40
+ logger.debug(`Dataset "${slug}" has no live version \u2014 returning not found`);
41
+ return miss(slug, null);
42
+ }
43
+ const base = datafile.collectEndpoint.replace(/\/+$/, "");
44
+ const url = `${base}/v1/ds/${encodeURIComponent(sdkKey)}/${encodeURIComponent(slug)}/${dataset.liveVersion}/${encodeURIComponent(key)}`;
45
+ try {
46
+ const res = await fetch(url, { signal: AbortSignal.timeout(requestTimeout) });
47
+ if (!res.ok) {
48
+ logger.warn("Dataset lookup failed", { slug, key, status: res.status });
49
+ return miss(slug, dataset.liveVersion);
50
+ }
51
+ const body = await res.json();
52
+ if (!body || body.found !== true) {
53
+ return miss(slug, dataset.liveVersion);
54
+ }
55
+ return {
56
+ found: true,
57
+ value: body.value,
58
+ meta: { slug, version: dataset.liveVersion }
59
+ };
60
+ } catch (e) {
61
+ logger.warn("Dataset lookup error", { slug, key, error: String(e) });
62
+ return miss(slug, dataset.liveVersion);
63
+ }
64
+ }
65
+ return { get };
66
+ }
67
+
68
+ // src/recs.ts
69
+ var DEFAULT_REQUEST_TIMEOUT2 = 1e4;
70
+ var MAX_SEED_CHARS = 256;
71
+ var MAX_SEEDS = 3;
72
+ var MAX_ITEMS_CAP = 24;
73
+ var DEFAULT_MAX_ITEMS = 8;
74
+ var MAX_ID_CHARS = 64;
75
+ var MAX_ATTR_VALUE_CHARS = 512;
76
+ var GLOBAL_SEED = "_global";
77
+ function clampMaxItems(value) {
78
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 1) return DEFAULT_MAX_ITEMS;
79
+ return Math.min(Math.floor(value), MAX_ITEMS_CAP);
80
+ }
81
+ function asText(value) {
82
+ if (typeof value === "string") return value;
83
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
84
+ return "";
85
+ }
86
+ function joinIds(ids) {
87
+ let out = "";
88
+ for (const id of ids) {
89
+ const next = out === "" ? id : `${out},${id}`;
90
+ if (next.length > MAX_ATTR_VALUE_CHARS) break;
91
+ out = next;
92
+ }
93
+ return out;
94
+ }
95
+ function toRecProducts(items, cap) {
96
+ const out = [];
97
+ for (const item of items) {
98
+ if (out.length >= cap) break;
99
+ if (item === null || typeof item !== "object") continue;
100
+ const fields = item;
101
+ const id = (asText(fields.id) || asText(fields.sku)).slice(0, MAX_ID_CHARS);
102
+ if (id === "") continue;
103
+ const product = { id };
104
+ const title = asText(fields.title);
105
+ if (title) product.title = title;
106
+ const image = asText(fields.image);
107
+ if (image) product.image = image;
108
+ const href = asText(fields.href) || asText(fields.url);
109
+ if (href && !/^\s*javascript:/i.test(href)) product.href = href;
110
+ const category = asText(fields.category);
111
+ if (category) product.category = category;
112
+ if (typeof fields.price === "number" && Number.isFinite(fields.price)) {
113
+ product.price = Math.round(fields.price);
114
+ }
115
+ if (typeof fields.compareAtPrice === "number" && Number.isFinite(fields.compareAtPrice)) {
116
+ product.compareAtPrice = Math.round(fields.compareAtPrice);
117
+ }
118
+ if (typeof fields.currency === "string" && fields.currency !== "") {
119
+ product.currency = fields.currency;
120
+ }
121
+ const availability = fields.availability;
122
+ if (availability === "in_stock" || availability === "out_of_stock" || availability === "removed") {
123
+ product.availability = availability;
124
+ }
125
+ out.push(product);
126
+ }
127
+ return out;
128
+ }
129
+ function miss2(recipe, degraded) {
130
+ return { items: [], served: false, recipe, source: null, fallbackStep: null, degraded };
131
+ }
132
+ function createRecsClient(options) {
133
+ const { sdkKey, getDatafile, getTracker, logger } = options;
134
+ const requestTimeout = options.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT2;
135
+ function resolveSeeds(context) {
136
+ const clip = (s) => s.slice(0, MAX_SEED_CHARS);
137
+ if (Array.isArray(context?.seeds)) {
138
+ const seeds = context.seeds.filter((s) => typeof s === "string" && s !== "").slice(0, MAX_SEEDS).map(clip);
139
+ if (seeds.length > 0) return seeds;
140
+ }
141
+ if (typeof context?.seed === "string" && context.seed !== "") return [clip(context.seed)];
142
+ return [GLOBAL_SEED];
143
+ }
144
+ async function get(req) {
145
+ try {
146
+ const recipeHandle = typeof req?.recipe === "string" ? req.recipe : "";
147
+ if (recipeHandle === "") {
148
+ logger.warn("recs.get called with an empty recipe \u2014 returning served:false");
149
+ return miss2("", false);
150
+ }
151
+ const datafile = getDatafile();
152
+ if (!datafile) {
153
+ logger.debug(`SDK not ready, returning served:false for recipe "${recipeHandle}"`);
154
+ return miss2(recipeHandle, false);
155
+ }
156
+ const recipes = datafile.recipes;
157
+ const entry = recipes?.find((r) => r.recipe === recipeHandle) ?? null;
158
+ if (!entry) {
159
+ logger.debug(`Recipe "${recipeHandle}" not present in datafile \u2014 returning served:false`);
160
+ return miss2(recipeHandle, false);
161
+ }
162
+ const seeds = resolveSeeds(req.context);
163
+ const maxItems = clampMaxItems(req.maxItems);
164
+ const excludeOutOfStock = typeof req.excludeOutOfStock === "boolean" ? req.excludeOutOfStock : entry.excludeOutOfStock;
165
+ const body = {
166
+ snippetKey: sdkKey,
167
+ assigned: 0,
168
+ sources: [{ chain: entry.chain }],
169
+ maxItems,
170
+ excludeOutOfStock,
171
+ merch: entry.merch ?? [],
172
+ sessionAffinity: entry.sessionAffinity
173
+ };
174
+ if (seeds.length === 1) body.seedKey = seeds[0];
175
+ else body.seedKeys = seeds;
176
+ const base = datafile.collectEndpoint.replace(/\/+$/, "").replace("/v1/collect/batch", "");
177
+ const res = await fetch(`${base}/v1/recs/resolve`, {
178
+ method: "POST",
179
+ headers: { "Content-Type": "application/json" },
180
+ body: JSON.stringify(body),
181
+ signal: AbortSignal.timeout(requestTimeout)
182
+ });
183
+ if (!res.ok) {
184
+ logger.warn("Recs resolve failed", { recipe: recipeHandle, status: res.status });
185
+ return miss2(recipeHandle, true);
186
+ }
187
+ const parsed = await res.json();
188
+ const data = parsed?.data;
189
+ if (!data || typeof data !== "object") return miss2(recipeHandle, true);
190
+ const items = toRecProducts(Array.isArray(data.items) ? data.items : [], maxItems);
191
+ const servedStep = data.served ?? null;
192
+ const served = servedStep !== null && items.length > 0;
193
+ if (req.attribution) {
194
+ const tracker = getTracker();
195
+ if (tracker) {
196
+ tracker.queue({
197
+ visitorId: req.attribution.visitorId,
198
+ experimentId: req.attribution.experimentId,
199
+ variationId: req.attribution.variationId,
200
+ eventName: "rec:impression",
201
+ revenue: null,
202
+ timestamp: Date.now(),
203
+ pageUrl: "",
204
+ attributes: {
205
+ recipe: entry.recipe,
206
+ seed: joinIds(seeds),
207
+ slug: servedStep?.slug ?? "",
208
+ version: servedStep ? String(servedStep.version) : "",
209
+ step: servedStep ? String(servedStep.step) : "",
210
+ served: served ? "true" : "false",
211
+ items: joinIds(items.map((i) => i.id))
212
+ }
213
+ });
214
+ } else {
215
+ logger.warn("recs.get attribution supplied before tracker is ready; impression dropped");
216
+ }
217
+ }
218
+ return {
219
+ items,
220
+ served,
221
+ recipe: entry.recipe,
222
+ source: servedStep?.slug ?? null,
223
+ fallbackStep: servedStep?.step ?? null,
224
+ degraded: false
225
+ };
226
+ } catch (e) {
227
+ logger.warn("Recs resolve error", {
228
+ recipe: typeof req?.recipe === "string" ? req.recipe : "",
229
+ error: String(e)
230
+ });
231
+ return miss2(typeof req?.recipe === "string" ? req.recipe : "", true);
232
+ }
233
+ }
234
+ function trackClick(productId, opts) {
235
+ if (typeof productId !== "string" || productId === "") {
236
+ logger.warn("recs.trackClick called with an empty productId \u2014 dropped");
237
+ return;
238
+ }
239
+ if (!opts || !opts.visitorId || !opts.experimentId || !opts.variationId) {
240
+ logger.warn("recs.trackClick requires visitorId/experimentId/variationId \u2014 dropped");
241
+ return;
242
+ }
243
+ const tracker = getTracker();
244
+ if (!tracker) {
245
+ logger.warn("recs.trackClick called before tracker is ready; click dropped");
246
+ return;
247
+ }
248
+ const position = typeof opts.position === "number" && Number.isInteger(opts.position) && opts.position >= 1 ? String(opts.position) : "";
249
+ tracker.queue({
250
+ visitorId: opts.visitorId,
251
+ experimentId: opts.experimentId,
252
+ variationId: opts.variationId,
253
+ eventName: "rec:click",
254
+ revenue: null,
255
+ timestamp: Date.now(),
256
+ pageUrl: "",
257
+ attributes: {
258
+ recipe: typeof opts.recipe === "string" ? opts.recipe : "",
259
+ item: productId.slice(0, MAX_ID_CHARS),
260
+ position
261
+ }
262
+ });
263
+ }
264
+ return { get, trackClick };
265
+ }
266
+
16
267
  // src/decisionLog.ts
17
268
  function createDecisionLog() {
18
269
  const entries = [];
19
270
  return { entries };
20
271
  }
21
272
 
273
+ // src/serverTracker.ts
274
+ import { randomUUID } from "crypto";
275
+
22
276
  // src/reconnectBackoff.ts
23
277
  var BASE_DELAY_MS = 1e3;
24
278
  var MAX_DELAY_MS = 6e4;
@@ -47,6 +301,12 @@ function parseRetryAfter(headerValue) {
47
301
  }
48
302
 
49
303
  // src/serverTracker.ts
304
+ function stampExposure(event) {
305
+ return event.eventId ? event : { ...event, eventId: randomUUID() };
306
+ }
307
+ function stampMetric(event) {
308
+ return event.eventId ? event : { ...event, eventId: randomUUID() };
309
+ }
50
310
  function createServerTracker(options) {
51
311
  const {
52
312
  sdkKey,
@@ -59,6 +319,7 @@ function createServerTracker(options) {
59
319
  } = options;
60
320
  const batchEndpoint = collectEndpoint.replace(/\/?$/, "/v1/collect/batch");
61
321
  const metricEndpoint = collectEndpoint.replace(/\/?$/, "/v1/collect/metric-batch");
322
+ const ordersEndpoint = collectEndpoint.replace(/\/?$/, "/v1/collect/orders");
62
323
  let exposureQueue = [];
63
324
  let metricQueue = [];
64
325
  let flushTimer = null;
@@ -70,7 +331,7 @@ function createServerTracker(options) {
70
331
  }, flushInterval);
71
332
  function queue(event) {
72
333
  if (destroyed) return;
73
- exposureQueue.push(event);
334
+ exposureQueue.push(stampExposure(event));
74
335
  if (exposureQueue.length > maxQueueSize) {
75
336
  const dropped = exposureQueue.length - maxQueueSize;
76
337
  exposureQueue = exposureQueue.slice(-maxQueueSize);
@@ -84,7 +345,7 @@ function createServerTracker(options) {
84
345
  }
85
346
  function queueMetric(event) {
86
347
  if (destroyed) return;
87
- metricQueue.push(event);
348
+ metricQueue.push(stampMetric(event));
88
349
  if (metricQueue.length > maxQueueSize) {
89
350
  const dropped = metricQueue.length - maxQueueSize;
90
351
  metricQueue = metricQueue.slice(-maxQueueSize);
@@ -146,6 +407,47 @@ function createServerTracker(options) {
146
407
  }
147
408
  }
148
409
  }
410
+ async function trackPurchase(visitorId, order) {
411
+ if (destroyed) return;
412
+ const payload = {
413
+ ...order,
414
+ eventId: randomUUID(),
415
+ visitorId,
416
+ timestamp: Date.now(),
417
+ pageUrl: ""
418
+ };
419
+ const body = JSON.stringify({ sdkKey, orders: [payload] });
420
+ let backoff = resetBackoff();
421
+ let pendingRetryAfterMs = void 0;
422
+ for (let attempt = 0; attempt <= retryAttempts; attempt++) {
423
+ try {
424
+ const response = await fetch(ordersEndpoint, {
425
+ method: "POST",
426
+ headers: { "Content-Type": "application/json" },
427
+ body,
428
+ signal: AbortSignal.timeout(1e4)
429
+ });
430
+ if (!response.ok) {
431
+ const parsed = parseRetryAfter(response.headers.get("retry-after"));
432
+ if (parsed !== null) pendingRetryAfterMs = parsed;
433
+ throw new Error(`HTTP ${response.status}`);
434
+ }
435
+ logger.debug(`Purchase order ${order.orderId} sent for visitor ${visitorId}`);
436
+ return;
437
+ } catch (e) {
438
+ if (attempt < retryAttempts) {
439
+ backoff = nextBackoff(backoff, pendingRetryAfterMs);
440
+ pendingRetryAfterMs = void 0;
441
+ await new Promise((r) => setTimeout(r, backoff.delayMs));
442
+ } else {
443
+ logger.error(
444
+ `Failed to send purchase order ${order.orderId} after ${retryAttempts} retries`,
445
+ { endpoint: ordersEndpoint, error: String(e) }
446
+ );
447
+ }
448
+ }
449
+ }
450
+ }
149
451
  async function destroy() {
150
452
  destroyed = true;
151
453
  if (flushTimer) {
@@ -154,186 +456,17 @@ function createServerTracker(options) {
154
456
  }
155
457
  await flush();
156
458
  }
157
- return { queue, queueMetric, flush, destroy };
459
+ return { queue, queueMetric, trackPurchase, flush, destroy };
158
460
  }
159
461
 
160
462
  // src/streaming.ts
161
- function createStreamingClient(options) {
162
- const {
163
- endpoint,
164
- sdkKey,
165
- onDatafileUpdated,
166
- logger,
167
- reconnectDelay
168
- } = options;
169
- let connected = false;
170
- let abortController = null;
171
- let reconnectTimer = null;
172
- let backoff = resetBackoff();
173
- let pendingRetryAfterMs = void 0;
174
- function connect() {
175
- if (connected) return;
176
- void startConnection();
177
- }
178
- function disconnect() {
179
- connected = false;
180
- if (abortController) {
181
- abortController.abort();
182
- abortController = null;
183
- }
184
- if (reconnectTimer) {
185
- clearTimeout(reconnectTimer);
186
- reconnectTimer = null;
187
- }
188
- }
189
- function isConnected() {
190
- return connected;
191
- }
192
- async function fetchTicket(signal) {
193
- const ticketUrl = `${endpoint}?sdkKey=${encodeURIComponent(sdkKey)}`;
194
- try {
195
- const res = await fetch(ticketUrl, { signal });
196
- if (!res.ok) {
197
- logger.warn("SSE ticket fetch failed", { status: res.status, url: ticketUrl });
198
- return null;
199
- }
200
- const body = await res.json();
201
- if (!body?.url || !body?.token) {
202
- logger.warn("SSE ticket response missing fields");
203
- return null;
204
- }
205
- return body;
206
- } catch (e) {
207
- if (signal.aborted) return null;
208
- logger.warn("SSE ticket fetch error", { error: String(e) });
209
- return null;
210
- }
211
- }
212
- async function startConnection() {
213
- abortController = new AbortController();
214
- const signal = abortController.signal;
215
- const ticket = await fetchTicket(signal);
216
- if (!ticket || signal.aborted) {
217
- if (!signal.aborted) scheduleReconnect();
218
- return;
219
- }
220
- const streamUrl = `${ticket.url}?sdkKey=${encodeURIComponent(sdkKey)}&token=${encodeURIComponent(ticket.token)}`;
221
- try {
222
- const response = await fetch(streamUrl, {
223
- headers: { Accept: "text/event-stream" },
224
- signal
225
- });
226
- if (response.status === 401) {
227
- logger.warn("SSE stream rejected token; refetching ticket");
228
- const next = await fetchTicket(signal);
229
- if (!next || signal.aborted) {
230
- if (!signal.aborted) scheduleReconnect();
231
- return;
232
- }
233
- const retryUrl = `${next.url}?sdkKey=${encodeURIComponent(sdkKey)}&token=${encodeURIComponent(next.token)}`;
234
- const retry = await fetch(retryUrl, {
235
- headers: { Accept: "text/event-stream" },
236
- signal
237
- });
238
- if (!retry.ok || !retry.body) {
239
- logger.warn("SSE retry failed", { status: retry.status });
240
- recordRetryAfter(retry.headers.get("retry-after"));
241
- scheduleReconnect();
242
- return;
243
- }
244
- await pumpFrames(retry, signal);
245
- return;
246
- }
247
- if (response.status === 429) {
248
- recordRetryAfter(response.headers.get("retry-after"));
249
- logger.warn("SSE rate-limited", { status: 429 });
250
- scheduleReconnect();
251
- return;
252
- }
253
- if (!response.ok) {
254
- logger.warn("SSE connection failed", { status: response.status, url: streamUrl });
255
- recordRetryAfter(response.headers.get("retry-after"));
256
- scheduleReconnect();
257
- return;
258
- }
259
- if (!response.body) {
260
- logger.warn("SSE response has no body");
261
- scheduleReconnect();
262
- return;
263
- }
264
- await pumpFrames(response, signal);
265
- } catch (e) {
266
- if (signal.aborted) return;
267
- logger.warn("SSE connection error", { error: String(e) });
268
- connected = false;
269
- scheduleReconnect();
270
- }
271
- }
272
- function recordRetryAfter(headerValue) {
273
- const parsed = parseRetryAfter(headerValue);
274
- if (parsed !== null) pendingRetryAfterMs = parsed;
275
- }
276
- async function pumpFrames(response, signal) {
277
- connected = true;
278
- backoff = resetBackoff();
279
- pendingRetryAfterMs = void 0;
280
- logger.info(`SSE connected to ${endpoint}`);
281
- const reader = response.body.getReader();
282
- const decoder = new TextDecoder();
283
- let buffer = "";
284
- try {
285
- while (true) {
286
- const { done, value } = await reader.read();
287
- if (done) break;
288
- buffer += decoder.decode(value, { stream: true });
289
- const lines = buffer.split("\n");
290
- buffer = lines.pop() ?? "";
291
- for (const line of lines) {
292
- if (line.startsWith("data: ")) {
293
- const data = line.slice(6).trim();
294
- if (!data) continue;
295
- try {
296
- const parsed = JSON.parse(data);
297
- if (parsed.type === "datafile_updated") {
298
- onDatafileUpdated();
299
- }
300
- } catch {
301
- logger.warn("Failed to parse SSE data", { data });
302
- }
303
- }
304
- }
305
- }
306
- } catch (e) {
307
- if (signal.aborted) return;
308
- logger.warn("SSE stream read error", { error: String(e) });
309
- }
310
- connected = false;
311
- scheduleReconnect();
312
- }
313
- function scheduleReconnect() {
314
- if (reconnectTimer) return;
315
- if (typeof reconnectDelay === "number") {
316
- reconnectTimer = setTimeout(() => {
317
- reconnectTimer = null;
318
- void startConnection();
319
- }, reconnectDelay);
320
- return;
321
- }
322
- backoff = nextBackoff(backoff, pendingRetryAfterMs);
323
- pendingRetryAfterMs = void 0;
324
- logger.warn(
325
- `SSE reconnect attempt ${backoff.attempt} in ${backoff.delayMs}ms`
326
- );
327
- reconnectTimer = setTimeout(() => {
328
- reconnectTimer = null;
329
- void startConnection();
330
- }, backoff.delayMs);
331
- }
332
- return { connect, disconnect, isConnected };
333
- }
463
+ import {
464
+ createStreamingClient,
465
+ createBrowserStreamingClient
466
+ } from "@avsbhq/utils/streaming";
334
467
 
335
468
  // src/version.ts
336
- var SDK_VERSION = "0.3.0";
469
+ var SDK_VERSION = "0.4.0";
337
470
 
338
471
  // src/UserBoundClient.ts
339
472
  function createUserBoundClient(server, context) {
@@ -509,6 +642,42 @@ var AvsbServer = class {
509
642
  }
510
643
  return this._utils;
511
644
  }
645
+ /**
646
+ * Server-side datasets read surface (e-commerce suite Phase 2).
647
+ * Constructed lazily on first access, mirroring the `utils` getter.
648
+ * `client.datasets.get(slug, key)` resolves the live version from the
649
+ * current datafile and never throws — see ./datasets.ts.
650
+ */
651
+ _datasets = null;
652
+ get datasets() {
653
+ if (!this._datasets) {
654
+ this._datasets = createDatasetsClient({
655
+ sdkKey: this.sdkKey,
656
+ getDatafile: () => this.datafile,
657
+ logger: this.logger
658
+ });
659
+ }
660
+ return this._datasets;
661
+ }
662
+ /**
663
+ * Server-side recommendations surface (e-commerce suite overhaul, C3).
664
+ * Constructed lazily on first access, mirroring the `datasets` getter.
665
+ * `client.recs.get({ recipe, … })` resolves the recipe registry from the
666
+ * current datafile and never throws; impressions/clicks ride the server
667
+ * tracker's queue and revenue attaches via the existing `trackPurchase`.
668
+ */
669
+ _recs = null;
670
+ get recs() {
671
+ if (!this._recs) {
672
+ this._recs = createRecsClient({
673
+ sdkKey: this.sdkKey,
674
+ getDatafile: () => this.datafile,
675
+ getTracker: () => this.tracker,
676
+ logger: this.logger
677
+ });
678
+ }
679
+ return this._recs;
680
+ }
512
681
  constructor(options) {
513
682
  this.sdkKey = options.sdkKey;
514
683
  this.pollingInterval = Math.max(options.pollingInterval ?? 6e4, 5e3);
@@ -693,6 +862,16 @@ var AvsbServer = class {
693
862
  return out;
694
863
  }
695
864
  // ---------------------------------------------------------------------------
865
+ // Purchase tracking — fires immediately to /v1/collect/orders
866
+ // ---------------------------------------------------------------------------
867
+ async trackPurchase(visitorId, order) {
868
+ if (!this.tracker) {
869
+ this.logger.warn("trackPurchase called before tracker is ready; order dropped");
870
+ return;
871
+ }
872
+ return this.tracker.trackPurchase(visitorId, order);
873
+ }
874
+ // ---------------------------------------------------------------------------
696
875
  // §5.6 Unified track — context required
697
876
  // ---------------------------------------------------------------------------
698
877
  track(eventKey, payload) {
package/package.json CHANGED
@@ -1,14 +1,19 @@
1
1
  {
2
2
  "name": "@avsbhq/node",
3
- "version": "1.0.0",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "exports": {
9
- ".": { "import": "./dist/index.js", "require": "./dist/index.cjs" }
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "require": "./dist/index.cjs"
12
+ }
10
13
  },
11
- "files": ["dist"],
14
+ "files": [
15
+ "dist"
16
+ ],
12
17
  "engines": {
13
18
  "node": ">=18.0.0"
14
19
  },
@@ -28,8 +33,8 @@
28
33
  "test:watch": "vitest"
29
34
  },
30
35
  "dependencies": {
31
- "@avsbhq/core": "1.0.0",
32
- "@avsbhq/utils": "1.0.0"
36
+ "@avsbhq/core": "1.3.0",
37
+ "@avsbhq/utils": "1.0.1"
33
38
  },
34
39
  "devDependencies": {
35
40
  "tsup": "^8.0.0",
@@ -37,4 +42,4 @@
37
42
  "vitest": "^3.0.0",
38
43
  "@types/node": "^20.0.0"
39
44
  }
40
- }
45
+ }