@likyly/sdk 0.0.0-stage → 1.1.1

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.
Files changed (59) hide show
  1. package/README.md +286 -2
  2. package/dist/cjs/client.d.ts +70 -0
  3. package/dist/cjs/client.js +85 -0
  4. package/dist/cjs/encoding.d.ts +11 -0
  5. package/dist/cjs/encoding.js +37 -0
  6. package/dist/cjs/errors.d.ts +52 -0
  7. package/dist/cjs/errors.js +83 -0
  8. package/dist/cjs/http.d.ts +49 -0
  9. package/dist/cjs/http.js +155 -0
  10. package/dist/cjs/index.d.ts +8 -0
  11. package/dist/cjs/index.js +20 -0
  12. package/dist/cjs/package.json +1 -0
  13. package/dist/cjs/recommend-cache.d.ts +42 -0
  14. package/dist/cjs/recommend-cache.js +114 -0
  15. package/dist/cjs/resources/events.d.ts +45 -0
  16. package/dist/cjs/resources/events.js +134 -0
  17. package/dist/cjs/resources/items.d.ts +43 -0
  18. package/dist/cjs/resources/items.js +97 -0
  19. package/dist/cjs/resources/placements.d.ts +24 -0
  20. package/dist/cjs/resources/placements.js +97 -0
  21. package/dist/cjs/resources/recommendations.d.ts +54 -0
  22. package/dist/cjs/resources/recommendations.js +130 -0
  23. package/dist/cjs/resources/users.d.ts +19 -0
  24. package/dist/cjs/resources/users.js +64 -0
  25. package/dist/cjs/session.d.ts +32 -0
  26. package/dist/cjs/session.js +76 -0
  27. package/dist/cjs/types.d.ts +226 -0
  28. package/dist/cjs/types.js +2 -0
  29. package/dist/cjs/version.d.ts +3 -0
  30. package/dist/cjs/version.js +6 -0
  31. package/dist/esm/client.d.ts +70 -0
  32. package/dist/esm/client.js +81 -0
  33. package/dist/esm/encoding.d.ts +11 -0
  34. package/dist/esm/encoding.js +31 -0
  35. package/dist/esm/errors.d.ts +52 -0
  36. package/dist/esm/errors.js +69 -0
  37. package/dist/esm/http.d.ts +49 -0
  38. package/dist/esm/http.js +149 -0
  39. package/dist/esm/index.d.ts +8 -0
  40. package/dist/esm/index.js +4 -0
  41. package/dist/esm/recommend-cache.d.ts +42 -0
  42. package/dist/esm/recommend-cache.js +110 -0
  43. package/dist/esm/resources/events.d.ts +45 -0
  44. package/dist/esm/resources/events.js +130 -0
  45. package/dist/esm/resources/items.d.ts +43 -0
  46. package/dist/esm/resources/items.js +91 -0
  47. package/dist/esm/resources/placements.d.ts +24 -0
  48. package/dist/esm/resources/placements.js +93 -0
  49. package/dist/esm/resources/recommendations.d.ts +54 -0
  50. package/dist/esm/resources/recommendations.js +124 -0
  51. package/dist/esm/resources/users.d.ts +19 -0
  52. package/dist/esm/resources/users.js +60 -0
  53. package/dist/esm/session.d.ts +32 -0
  54. package/dist/esm/session.js +72 -0
  55. package/dist/esm/types.d.ts +226 -0
  56. package/dist/esm/types.js +1 -0
  57. package/dist/esm/version.d.ts +3 -0
  58. package/dist/esm/version.js +3 -0
  59. package/package.json +60 -4
@@ -0,0 +1,49 @@
1
+ import type { RequestOptions } from "./types.js";
2
+ export interface HttpConfig {
3
+ apiKey: string;
4
+ baseUrl: string;
5
+ catalog?: string;
6
+ timeout: number;
7
+ maxRetries: number;
8
+ userAgent: string;
9
+ fetch: typeof fetch;
10
+ /** Test hooks - not part of the public API. */
11
+ sleep: (ms: number) => Promise<void>;
12
+ random: () => number;
13
+ }
14
+ export interface HttpRequest {
15
+ method: "GET" | "POST" | "PUT" | "DELETE";
16
+ /** Path already percent-encoded, starting with "/". */
17
+ path: string;
18
+ query?: Record<string, string | number | boolean | undefined>;
19
+ body?: unknown;
20
+ /**
21
+ * Safe to send again if the outcome is unknown (a timeout, a 5xx, a dropped connection)? True for
22
+ * reads, PUT/DELETE and the upserts; false for events without an `eventId`, where a blind retry
23
+ * could record the event twice.
24
+ */
25
+ idempotent: boolean;
26
+ options?: RequestOptions;
27
+ }
28
+ export interface HttpResponse<T> {
29
+ data: T;
30
+ status: number;
31
+ headers: Headers;
32
+ requestId?: string;
33
+ }
34
+ /**
35
+ * The only place that talks HTTP. Every resource goes through `request()`: auth header, catalog
36
+ * parameter, timeout, retries with exponential backoff + jitter, and the error mapping.
37
+ */
38
+ export declare class HttpClient {
39
+ private readonly config;
40
+ constructor(config: HttpConfig);
41
+ request<T>(req: HttpRequest): Promise<HttpResponse<T>>;
42
+ /** Milliseconds to wait before the next attempt, or undefined if this failure must not be retried. */
43
+ private retryDelay;
44
+ private buildUrl;
45
+ private attempt;
46
+ }
47
+ /** Retry-After is either a number of seconds or an HTTP date. */
48
+ export declare function parseRetryAfter(value: string | null): number | undefined;
49
+ export declare function assertNonEmptyObject(value: unknown, name: string): asserts value is Record<string, unknown>;
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HttpClient = void 0;
4
+ exports.parseRetryAfter = parseRetryAfter;
5
+ exports.assertNonEmptyObject = assertNonEmptyObject;
6
+ const errors_js_1 = require("./errors.js");
7
+ const RETRY_BASE_MS = 500;
8
+ const RETRY_CAP_MS = 8_000;
9
+ /** A Retry-After longer than this is not waited for: the RateLimitError is thrown instead. */
10
+ const MAX_RETRY_AFTER_MS = 60_000;
11
+ /**
12
+ * The only place that talks HTTP. Every resource goes through `request()`: auth header, catalog
13
+ * parameter, timeout, retries with exponential backoff + jitter, and the error mapping.
14
+ */
15
+ class HttpClient {
16
+ config;
17
+ constructor(config) {
18
+ this.config = config;
19
+ }
20
+ async request(req) {
21
+ const maxRetries = req.options?.maxRetries ?? this.config.maxRetries;
22
+ const url = this.buildUrl(req);
23
+ let attempt = 0;
24
+ for (;;) {
25
+ let failure;
26
+ try {
27
+ return await this.attempt(req, url);
28
+ }
29
+ catch (error) {
30
+ if (!(error instanceof errors_js_1.LikylyError))
31
+ throw error; // e.g. the caller's own AbortError
32
+ failure = error;
33
+ }
34
+ const delay = this.retryDelay(failure, attempt, req.idempotent);
35
+ if (delay === undefined || attempt >= maxRetries)
36
+ throw failure;
37
+ attempt += 1;
38
+ await this.config.sleep(delay);
39
+ }
40
+ }
41
+ /** Milliseconds to wait before the next attempt, or undefined if this failure must not be retried. */
42
+ retryDelay(error, attempt, idempotent) {
43
+ const backoff = () => this.config.random() * Math.min(RETRY_CAP_MS, RETRY_BASE_MS * 2 ** attempt); // full jitter
44
+ if (error.statusCode === 429) {
45
+ // Rejected by the rate limiter before reaching the application: nothing was processed, so
46
+ // retrying is safe for every request.
47
+ if (error.retryAfter !== undefined) {
48
+ const wait = error.retryAfter * 1000;
49
+ return wait <= MAX_RETRY_AFTER_MS ? wait : undefined;
50
+ }
51
+ return backoff();
52
+ }
53
+ if (!idempotent)
54
+ return undefined; // the outcome is unknown - never risk a duplicate
55
+ if (error.statusCode === 502 || error.statusCode === 503 || error.statusCode === 504) {
56
+ if (error.retryAfter !== undefined) {
57
+ const wait = error.retryAfter * 1000;
58
+ return wait <= MAX_RETRY_AFTER_MS ? wait : undefined;
59
+ }
60
+ return backoff();
61
+ }
62
+ if (error instanceof errors_js_1.NetworkError || error instanceof errors_js_1.TimeoutError)
63
+ return backoff();
64
+ return undefined;
65
+ }
66
+ buildUrl(req) {
67
+ const params = new URLSearchParams();
68
+ for (const [key, value] of Object.entries(req.query ?? {})) {
69
+ if (value !== undefined)
70
+ params.set(key, String(value));
71
+ }
72
+ if (this.config.catalog)
73
+ params.set("data_product_type", this.config.catalog);
74
+ const qs = params.toString();
75
+ return this.config.baseUrl + req.path + (qs ? `?${qs}` : "");
76
+ }
77
+ async attempt(req, url) {
78
+ const timeoutMs = req.options?.timeout ?? this.config.timeout;
79
+ const controller = new AbortController();
80
+ const userSignal = req.options?.signal;
81
+ let timedOut = false;
82
+ const timer = setTimeout(() => {
83
+ timedOut = true;
84
+ controller.abort();
85
+ }, timeoutMs);
86
+ const onUserAbort = () => controller.abort();
87
+ if (userSignal) {
88
+ if (userSignal.aborted)
89
+ controller.abort();
90
+ else
91
+ userSignal.addEventListener("abort", onUserAbort, { once: true });
92
+ }
93
+ const headers = {
94
+ "X-API-Key": this.config.apiKey,
95
+ Accept: "application/json",
96
+ "User-Agent": this.config.userAgent,
97
+ };
98
+ if (req.body !== undefined)
99
+ headers["Content-Type"] = "application/json";
100
+ let response;
101
+ try {
102
+ response = await this.config.fetch(url, {
103
+ method: req.method,
104
+ headers,
105
+ body: req.body !== undefined ? JSON.stringify(req.body) : undefined,
106
+ signal: controller.signal,
107
+ });
108
+ }
109
+ catch (cause) {
110
+ if (timedOut)
111
+ throw new errors_js_1.TimeoutError(`LIKYLY request timed out after ${timeoutMs} ms`, { cause });
112
+ if (userSignal?.aborted)
113
+ throw cause; // the caller cancelled: surface their AbortError untouched
114
+ throw new errors_js_1.NetworkError(`Could not reach the LIKYLY API: ${cause?.message ?? cause}`, { cause });
115
+ }
116
+ finally {
117
+ clearTimeout(timer);
118
+ userSignal?.removeEventListener("abort", onUserAbort);
119
+ }
120
+ const text = await response.text().catch(() => "");
121
+ let data = undefined;
122
+ if (text) {
123
+ try {
124
+ data = JSON.parse(text);
125
+ }
126
+ catch {
127
+ data = text; // e.g. an HTML 502 page from a proxy
128
+ }
129
+ }
130
+ const requestId = response.headers.get("x-request-id") ??
131
+ (data && typeof data === "object" && "request_id" in data ? String(data.request_id) : undefined);
132
+ if (!response.ok) {
133
+ throw (0, errors_js_1.errorFromResponse)(response.status, data, requestId ?? undefined, parseRetryAfter(response.headers.get("retry-after")));
134
+ }
135
+ return { data: data, status: response.status, headers: response.headers, requestId: requestId ?? undefined };
136
+ }
137
+ }
138
+ exports.HttpClient = HttpClient;
139
+ /** Retry-After is either a number of seconds or an HTTP date. */
140
+ function parseRetryAfter(value) {
141
+ if (!value)
142
+ return undefined;
143
+ const seconds = Number(value);
144
+ if (Number.isFinite(seconds) && seconds >= 0)
145
+ return seconds;
146
+ const date = Date.parse(value);
147
+ if (!Number.isNaN(date))
148
+ return Math.max(0, Math.ceil((date - Date.now()) / 1000));
149
+ return undefined;
150
+ }
151
+ function assertNonEmptyObject(value, name) {
152
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
153
+ throw new errors_js_1.ValidationError(`${name} must be an object`);
154
+ }
155
+ }
@@ -0,0 +1,8 @@
1
+ export { Likyly } from "./client.js";
2
+ export type { LikylyOptions } from "./client.js";
3
+ export { LikylySession } from "./session.js";
4
+ export type { LikylySessionOptions } from "./session.js";
5
+ export { LikylyError, ApiError, AuthenticationError, PermissionDeniedError, NotFoundError, RateLimitError, ValidationError, NetworkError, TimeoutError, PausedError, } from "./errors.js";
6
+ export type { RecommendCacheOptions } from "./recommend-cache.js";
7
+ export { SDK_VERSION } from "./version.js";
8
+ export type * from "./types.js";
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SDK_VERSION = exports.PausedError = exports.TimeoutError = exports.NetworkError = exports.ValidationError = exports.RateLimitError = exports.NotFoundError = exports.PermissionDeniedError = exports.AuthenticationError = exports.ApiError = exports.LikylyError = exports.LikylySession = exports.Likyly = void 0;
4
+ var client_js_1 = require("./client.js");
5
+ Object.defineProperty(exports, "Likyly", { enumerable: true, get: function () { return client_js_1.Likyly; } });
6
+ var session_js_1 = require("./session.js");
7
+ Object.defineProperty(exports, "LikylySession", { enumerable: true, get: function () { return session_js_1.LikylySession; } });
8
+ var errors_js_1 = require("./errors.js");
9
+ Object.defineProperty(exports, "LikylyError", { enumerable: true, get: function () { return errors_js_1.LikylyError; } });
10
+ Object.defineProperty(exports, "ApiError", { enumerable: true, get: function () { return errors_js_1.ApiError; } });
11
+ Object.defineProperty(exports, "AuthenticationError", { enumerable: true, get: function () { return errors_js_1.AuthenticationError; } });
12
+ Object.defineProperty(exports, "PermissionDeniedError", { enumerable: true, get: function () { return errors_js_1.PermissionDeniedError; } });
13
+ Object.defineProperty(exports, "NotFoundError", { enumerable: true, get: function () { return errors_js_1.NotFoundError; } });
14
+ Object.defineProperty(exports, "RateLimitError", { enumerable: true, get: function () { return errors_js_1.RateLimitError; } });
15
+ Object.defineProperty(exports, "ValidationError", { enumerable: true, get: function () { return errors_js_1.ValidationError; } });
16
+ Object.defineProperty(exports, "NetworkError", { enumerable: true, get: function () { return errors_js_1.NetworkError; } });
17
+ Object.defineProperty(exports, "TimeoutError", { enumerable: true, get: function () { return errors_js_1.TimeoutError; } });
18
+ Object.defineProperty(exports, "PausedError", { enumerable: true, get: function () { return errors_js_1.PausedError; } });
19
+ var version_js_1 = require("./version.js");
20
+ Object.defineProperty(exports, "SDK_VERSION", { enumerable: true, get: function () { return version_js_1.SDK_VERSION; } });
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Client-side protection for `placements.recommend`, so a page does not call LIKYLY on every
3
+ * view and does not keep hammering it when it is down:
4
+ *
5
+ * - a cache: an answer is reused for `ttlSeconds` (default one hour) for the same placement,
6
+ * item and visitor. In a browser it lives in `sessionStorage` (one visitor's tab session:
7
+ * back/forward and revisits cost nothing); elsewhere (SSR, Node) in memory. A reused answer
8
+ * keeps its `recommendationId`, so impressions and clicks stay attributed.
9
+ * - a circuit breaker: after a network error, a timeout or a 5xx, calls fail at once for
10
+ * `pauseSeconds` (default 30) instead of waiting on LIKYLY, so the page shows its own
11
+ * fallback straight away. The next call after the pause tries again.
12
+ */
13
+ import type { PlacementRecommendResponse } from "./types.js";
14
+ export interface RecommendCacheOptions {
15
+ /** How long an answer is reused, in seconds. Default 3600. */
16
+ ttlSeconds?: number;
17
+ /** After LIKYLY fails, how long to skip calls, in seconds. Default 30. Set 0 to always call. */
18
+ pauseSeconds?: number;
19
+ /** Where answers are kept. Default: "session" in a browser (sessionStorage), "memory" elsewhere. */
20
+ storage?: "session" | "memory";
21
+ }
22
+ export declare class RecommendCache {
23
+ private readonly now;
24
+ private readonly ttlMs;
25
+ private readonly pauseMs;
26
+ private readonly storage;
27
+ private readonly memory;
28
+ private pausedUntil;
29
+ constructor(options?: RecommendCacheOptions, now?: () => number);
30
+ get(key: string): PlacementRecommendResponse | null;
31
+ set(key: string, value: PlacementRecommendResponse): void;
32
+ /** Drop every cached answer of this client (e.g. after a purge in the LIKYLY console). */
33
+ clear(): void;
34
+ /** True while calls are paused after a failure. */
35
+ isPaused(): boolean;
36
+ /** Seconds left in the pause (0 when not paused). */
37
+ pauseRemaining(): number;
38
+ recordFailure(): void;
39
+ recordSuccess(): void;
40
+ private read;
41
+ private remove;
42
+ }
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RecommendCache = void 0;
4
+ const PREFIX = "likyly:rec:";
5
+ const MAX_MEMORY_ENTRIES = 500;
6
+ function browserStorage() {
7
+ try {
8
+ const storage = globalThis.sessionStorage;
9
+ if (!storage)
10
+ return null;
11
+ storage.getItem(PREFIX); // throws where storage is blocked (privacy settings, sandboxed frames)
12
+ return storage;
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ class RecommendCache {
19
+ now;
20
+ ttlMs;
21
+ pauseMs;
22
+ storage;
23
+ memory = new Map();
24
+ pausedUntil = 0;
25
+ constructor(options = {}, now = Date.now) {
26
+ this.now = now;
27
+ this.ttlMs = Math.max(0, options.ttlSeconds ?? 3600) * 1000;
28
+ this.pauseMs = Math.max(0, options.pauseSeconds ?? 30) * 1000;
29
+ this.storage = options.storage === "memory" ? null : browserStorage();
30
+ }
31
+ get(key) {
32
+ if (this.ttlMs === 0)
33
+ return null;
34
+ const entry = this.read(key);
35
+ if (!entry)
36
+ return null;
37
+ if (entry.expires <= this.now()) {
38
+ this.remove(key);
39
+ return null;
40
+ }
41
+ return entry.value;
42
+ }
43
+ set(key, value) {
44
+ if (this.ttlMs === 0)
45
+ return;
46
+ const entry = { expires: this.now() + this.ttlMs, value };
47
+ if (this.storage) {
48
+ try {
49
+ this.storage.setItem(PREFIX + key, JSON.stringify(entry));
50
+ return;
51
+ }
52
+ catch {
53
+ // Storage full or blocked: fall back to memory for this one.
54
+ }
55
+ }
56
+ this.memory.set(key, entry);
57
+ if (this.memory.size > MAX_MEMORY_ENTRIES)
58
+ this.memory.delete(this.memory.keys().next().value);
59
+ }
60
+ /** Drop every cached answer of this client (e.g. after a purge in the LIKYLY console). */
61
+ clear() {
62
+ this.memory.clear();
63
+ if (!this.storage)
64
+ return;
65
+ try {
66
+ for (let i = this.storage.length - 1; i >= 0; i--) {
67
+ const k = this.storage.key(i);
68
+ if (k?.startsWith(PREFIX))
69
+ this.storage.removeItem(k);
70
+ }
71
+ }
72
+ catch {
73
+ // Blocked storage: nothing was written there either.
74
+ }
75
+ }
76
+ /** True while calls are paused after a failure. */
77
+ isPaused() {
78
+ return this.now() < this.pausedUntil;
79
+ }
80
+ /** Seconds left in the pause (0 when not paused). */
81
+ pauseRemaining() {
82
+ return Math.max(0, Math.ceil((this.pausedUntil - this.now()) / 1000));
83
+ }
84
+ recordFailure() {
85
+ if (this.pauseMs > 0)
86
+ this.pausedUntil = this.now() + this.pauseMs;
87
+ }
88
+ recordSuccess() {
89
+ this.pausedUntil = 0;
90
+ }
91
+ read(key) {
92
+ if (this.storage) {
93
+ try {
94
+ const raw = this.storage.getItem(PREFIX + key);
95
+ if (raw)
96
+ return JSON.parse(raw);
97
+ }
98
+ catch {
99
+ // Unreadable entry or storage: treat as a miss.
100
+ }
101
+ }
102
+ return this.memory.get(key) ?? null;
103
+ }
104
+ remove(key) {
105
+ this.memory.delete(key);
106
+ try {
107
+ this.storage?.removeItem(PREFIX + key);
108
+ }
109
+ catch {
110
+ // ignore
111
+ }
112
+ }
113
+ }
114
+ exports.RecommendCache = RecommendCache;
@@ -0,0 +1,45 @@
1
+ import type { HttpClient } from "../http.js";
2
+ import type { EventBatchResult, EventInput, EventResult, IdentifyInput, IdentifyResult, RequestOptions, TypedEventInput } from "../types.js";
3
+ /**
4
+ * `likyly.events` - what your visitors do. Works with the **public** key from a browser, or the
5
+ * secret key from your backend. `track()` is the one mechanism; `view()`, `click()`, ... are
6
+ * shortcuts that call it with the matching event type.
7
+ */
8
+ export declare class EventsResource {
9
+ private readonly http;
10
+ constructor(http: HttpClient);
11
+ /**
12
+ * Records one event of any type: `view`, `click`, ... or your own (`favorite`, `share`, ...).
13
+ * Event types are open strings - the six helpers below are conveniences, not a closed list.
14
+ *
15
+ * Retries: a failed call is only retried automatically when it carries an `eventId` (then a
16
+ * replay is harmless); without one, an ambiguous failure is thrown rather than risking a duplicate.
17
+ */
18
+ track(type: string, event: EventInput, options?: RequestOptions): Promise<EventResult>;
19
+ /** Up to 1000 events in one call, each with its own `type`. All-or-nothing validation. */
20
+ trackMany(events: TypedEventInput[], options?: RequestOptions): Promise<EventBatchResult>;
21
+ /** The item was shown to the visitor (send the `recommendationId` it came with). */
22
+ impression(event: EventInput, options?: RequestOptions): Promise<EventResult>;
23
+ /** The visitor looked at the item (a product page, an article). */
24
+ view(event: EventInput, options?: RequestOptions): Promise<EventResult>;
25
+ /** The visitor clicked the item. */
26
+ click(event: EventInput, options?: RequestOptions): Promise<EventResult>;
27
+ addToCart(event: EventInput, options?: RequestOptions): Promise<EventResult>;
28
+ removeFromCart(event: EventInput, options?: RequestOptions): Promise<EventResult>;
29
+ /** Set `eventId` (e.g. `purchase_<orderId>_<itemId>`) so a retry can never count the purchase twice. */
30
+ purchase(event: EventInput, options?: RequestOptions): Promise<EventResult>;
31
+ /** Alias of {@link impression} - the item was shown to the visitor in a recommendation slot. */
32
+ recommendationImpression(event: EventInput, options?: RequestOptions): Promise<EventResult>;
33
+ /** Alias of {@link click} - the visitor clicked a recommended item. */
34
+ recommendationClick(event: EventInput, options?: RequestOptions): Promise<EventResult>;
35
+ /** Alias of {@link view} - the visitor looked at a product/article page. */
36
+ productView(event: EventInput, options?: RequestOptions): Promise<EventResult>;
37
+ /**
38
+ * Call right after login/signup, once you know `userId`, for the `sessionId` the visitor
39
+ * was anonymous under: every past interaction of that session not already attributed to a
40
+ * user is reassigned to `userId`, so it counts as that user's history from then on. Safe to
41
+ * call more than once. See `LikylySession.identify` for the client-side counterpart that
42
+ * also remembers the link locally.
43
+ */
44
+ identify(input: IdentifyInput, options?: RequestOptions): Promise<IdentifyResult>;
45
+ }
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EventsResource = void 0;
4
+ const encoding_js_1 = require("../encoding.js");
5
+ const errors_js_1 = require("../errors.js");
6
+ /**
7
+ * `likyly.events` - what your visitors do. Works with the **public** key from a browser, or the
8
+ * secret key from your backend. `track()` is the one mechanism; `view()`, `click()`, ... are
9
+ * shortcuts that call it with the matching event type.
10
+ */
11
+ class EventsResource {
12
+ http;
13
+ constructor(http) {
14
+ this.http = http;
15
+ }
16
+ /**
17
+ * Records one event of any type: `view`, `click`, ... or your own (`favorite`, `share`, ...).
18
+ * Event types are open strings - the six helpers below are conveniences, not a closed list.
19
+ *
20
+ * Retries: a failed call is only retried automatically when it carries an `eventId` (then a
21
+ * replay is harmless); without one, an ambiguous failure is thrown rather than risking a duplicate.
22
+ */
23
+ async track(type, event, options) {
24
+ const res = await this.http.request({
25
+ method: "POST",
26
+ path: `/events/${(0, encoding_js_1.encodeSegment)(requireEventType(type))}`,
27
+ body: eventBody(event),
28
+ idempotent: event.eventId !== undefined,
29
+ options,
30
+ });
31
+ return { message: res.data.message, eventId: res.data.event_id ?? undefined, duplicate: res.data.duplicate === true };
32
+ }
33
+ /** Up to 1000 events in one call, each with its own `type`. All-or-nothing validation. */
34
+ async trackMany(events, options) {
35
+ if (!Array.isArray(events) || events.length === 0)
36
+ throw new errors_js_1.ValidationError("events must be a non-empty array");
37
+ const res = await this.http.request({
38
+ method: "POST",
39
+ path: "/events/batch",
40
+ body: { events: events.map((e) => ({ event_type: requireEventType(e.type), ...eventBody(e) })) },
41
+ idempotent: events.every((e) => e.eventId !== undefined),
42
+ options,
43
+ });
44
+ return { received: res.data.received, accepted: res.data.accepted, duplicates: res.data.duplicates };
45
+ }
46
+ /** The item was shown to the visitor (send the `recommendationId` it came with). */
47
+ impression(event, options) {
48
+ return this.track("impression", event, options);
49
+ }
50
+ /** The visitor looked at the item (a product page, an article). */
51
+ view(event, options) {
52
+ return this.track("view", event, options);
53
+ }
54
+ /** The visitor clicked the item. */
55
+ click(event, options) {
56
+ return this.track("click", event, options);
57
+ }
58
+ addToCart(event, options) {
59
+ return this.track("add_to_cart", event, options);
60
+ }
61
+ removeFromCart(event, options) {
62
+ return this.track("remove_from_cart", event, options);
63
+ }
64
+ /** Set `eventId` (e.g. `purchase_<orderId>_<itemId>`) so a retry can never count the purchase twice. */
65
+ purchase(event, options) {
66
+ return this.track("purchase", event, options);
67
+ }
68
+ // ---- Clearer aliases for the recommendation-tracking events ---------------------------
69
+ // Same wire call as impression/view/click above (mapped server-side onto the same
70
+ // already-weighted event type - see db.EVENT_TYPE_ALIASES) - purely a naming preference,
71
+ // e.g. for code a coding agent generates from the LIKYLY MCP tools' own vocabulary.
72
+ /** Alias of {@link impression} - the item was shown to the visitor in a recommendation slot. */
73
+ recommendationImpression(event, options) {
74
+ return this.track("recommendation_impression", event, options);
75
+ }
76
+ /** Alias of {@link click} - the visitor clicked a recommended item. */
77
+ recommendationClick(event, options) {
78
+ return this.track("recommendation_click", event, options);
79
+ }
80
+ /** Alias of {@link view} - the visitor looked at a product/article page. */
81
+ productView(event, options) {
82
+ return this.track("product_view", event, options);
83
+ }
84
+ /**
85
+ * Call right after login/signup, once you know `userId`, for the `sessionId` the visitor
86
+ * was anonymous under: every past interaction of that session not already attributed to a
87
+ * user is reassigned to `userId`, so it counts as that user's history from then on. Safe to
88
+ * call more than once. See `LikylySession.identify` for the client-side counterpart that
89
+ * also remembers the link locally.
90
+ */
91
+ async identify(input, options) {
92
+ const userId = (0, encoding_js_1.requireId)(input.userId, "userId");
93
+ const sessionId = (0, encoding_js_1.requireId)(input.sessionId, "sessionId");
94
+ const res = await this.http.request({
95
+ method: "POST",
96
+ path: "/events/identify",
97
+ body: { user_id: userId, session_id: sessionId },
98
+ idempotent: true, // only ever reassigns not-yet-attributed rows - replaying is harmless
99
+ options,
100
+ });
101
+ return { linkedInteractions: res.data.linked_interactions };
102
+ }
103
+ }
104
+ exports.EventsResource = EventsResource;
105
+ function requireEventType(type) {
106
+ if (typeof type !== "string" || !/^[A-Za-z0-9_-]{1,64}$/.test(type)) {
107
+ throw new errors_js_1.ValidationError('event type must be 1-64 characters of letters, digits, "_" or "-" (e.g. "view", "add_to_cart", "favorite")');
108
+ }
109
+ return type;
110
+ }
111
+ /** `properties` is passed through untouched: its keys are yours. */
112
+ function eventBody(event) {
113
+ if (event === null || typeof event !== "object")
114
+ throw new errors_js_1.ValidationError("event must be an object");
115
+ const itemId = (0, encoding_js_1.requireId)(event.itemId, "itemId");
116
+ if (event.userId === undefined && event.sessionId === undefined) {
117
+ throw new errors_js_1.ValidationError("an event needs a userId or a sessionId (or both)");
118
+ }
119
+ if (event.userId !== undefined)
120
+ (0, encoding_js_1.requireId)(event.userId, "userId");
121
+ if (event.sessionId !== undefined)
122
+ (0, encoding_js_1.requireId)(event.sessionId, "sessionId");
123
+ return (0, encoding_js_1.compact)({
124
+ event_id: event.eventId,
125
+ user_id: event.userId,
126
+ session_id: event.sessionId,
127
+ item_id: itemId,
128
+ recommendation_id: event.recommendationId,
129
+ placement: event.placement,
130
+ quantity: event.quantity,
131
+ occurred_at: event.occurredAt instanceof Date ? event.occurredAt.toISOString() : event.occurredAt,
132
+ properties: event.properties,
133
+ });
134
+ }
@@ -0,0 +1,43 @@
1
+ import type { HttpClient } from "../http.js";
2
+ import type { BatchResult, Item, ItemImport, ItemInput, ItemList, ListOptions, RequestOptions } from "../types.js";
3
+ /** `likyly.items` - your catalog. Needs the **secret** API key. */
4
+ export declare class ItemsResource {
5
+ private readonly http;
6
+ constructor(http: HttpClient);
7
+ /** One item by your own id. */
8
+ get(itemId: string, options?: RequestOptions): Promise<Item>;
9
+ /** One page of the catalog. Pagination is `limit` + `offset` (the API's default is 100 per page); `total` is the whole catalog's size. */
10
+ list(listOptions?: ListOptions, options?: RequestOptions): Promise<ItemList>;
11
+ /**
12
+ * Creates the item, or replaces it if it exists - idempotent, safe to call as often as you like.
13
+ * The body is the whole item: fields you leave out are cleared.
14
+ */
15
+ upsert(itemId: string, item: ItemInput, options?: RequestOptions): Promise<Item>;
16
+ /** Removes the item from the catalog. Events already recorded for it are kept. */
17
+ delete(itemId: string, options?: RequestOptions): Promise<void>;
18
+ /** Batch upsert (1-1000 items) - each entry behaves like `upsert`. A failing entry is reported in `errors`. */
19
+ upsertMany(items: ItemImport[], options?: RequestOptions): Promise<BatchResult>;
20
+ /** Alias of {@link upsertMany}: the API's `POST /items/import` is a JSON batch upsert. (A CSV import exists only in the LIKYLY dashboard.) */
21
+ import(items: ItemImport[], options?: RequestOptions): Promise<BatchResult>;
22
+ /** Batch delete (1-1000 ids). Ids that don't exist are reported in `errors`. */
23
+ deleteMany(itemIds: string[], options?: RequestOptions): Promise<BatchResult>;
24
+ }
25
+ interface WireItem {
26
+ item_id: string;
27
+ title: string;
28
+ description?: string | null;
29
+ properties?: Record<string, unknown>;
30
+ }
31
+ export interface WireBatch {
32
+ received: number;
33
+ succeeded: number;
34
+ failed: number;
35
+ errors?: {
36
+ index: number;
37
+ id?: string | null;
38
+ message: string;
39
+ }[];
40
+ }
41
+ export declare function toItem(w: WireItem): Item;
42
+ export declare function toBatch(w: WireBatch): BatchResult;
43
+ export {};