@wocart/storefront 0.0.0-stage → 1.0.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.
Files changed (82) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +2 -0
  4. package/README.md +143 -2
  5. package/USAGE.md +1465 -0
  6. package/dist/abandoned-checkout.d.ts +22 -0
  7. package/dist/abandoned-checkout.js +89 -0
  8. package/dist/actions.d.ts +117 -0
  9. package/dist/actions.js +196 -0
  10. package/dist/astro.d.ts +6 -0
  11. package/dist/astro.js +3 -0
  12. package/dist/basket.d.ts +87 -0
  13. package/dist/basket.js +258 -0
  14. package/dist/client/cart.d.ts +30 -0
  15. package/dist/client/cart.js +26 -0
  16. package/dist/client/categories.d.ts +25 -0
  17. package/dist/client/categories.js +37 -0
  18. package/dist/client/config.d.ts +29 -0
  19. package/dist/client/config.js +41 -0
  20. package/dist/client/core.d.ts +86 -0
  21. package/dist/client/core.js +582 -0
  22. package/dist/client/errors.d.ts +19 -0
  23. package/dist/client/errors.js +31 -0
  24. package/dist/client/geo.d.ts +31 -0
  25. package/dist/client/geo.js +39 -0
  26. package/dist/client/index.d.ts +39 -0
  27. package/dist/client/index.js +80 -0
  28. package/dist/client/orders.d.ts +68 -0
  29. package/dist/client/orders.js +76 -0
  30. package/dist/client/page-data.d.ts +31 -0
  31. package/dist/client/page-data.js +37 -0
  32. package/dist/client/pages.d.ts +33 -0
  33. package/dist/client/pages.js +44 -0
  34. package/dist/client/paths.d.ts +8 -0
  35. package/dist/client/paths.js +10 -0
  36. package/dist/client/products.d.ts +56 -0
  37. package/dist/client/products.js +78 -0
  38. package/dist/client/shipping.d.ts +37 -0
  39. package/dist/client/shipping.js +48 -0
  40. package/dist/data/communes.d.ts +25 -0
  41. package/dist/data/communes.js +1690 -0
  42. package/dist/data/wilayas.d.ts +6 -0
  43. package/dist/data/wilayas.js +64 -0
  44. package/dist/domain.d.ts +332 -0
  45. package/dist/domain.js +214 -0
  46. package/dist/endpoints/proxy.d.ts +111 -0
  47. package/dist/endpoints/proxy.js +161 -0
  48. package/dist/index.d.ts +42 -0
  49. package/dist/index.js +30 -0
  50. package/dist/locale-routing.d.ts +59 -0
  51. package/dist/locale-routing.js +62 -0
  52. package/dist/locales.d.ts +15 -0
  53. package/dist/locales.js +8 -0
  54. package/dist/meta-tracking.d.ts +34 -0
  55. package/dist/meta-tracking.js +72 -0
  56. package/dist/middleware.d.ts +100 -0
  57. package/dist/middleware.js +72 -0
  58. package/dist/offer-view.d.ts +10 -0
  59. package/dist/offer-view.js +13 -0
  60. package/dist/otp.d.ts +80 -0
  61. package/dist/otp.js +295 -0
  62. package/dist/phone.d.ts +2 -0
  63. package/dist/phone.js +34 -0
  64. package/dist/product-view.d.ts +12 -0
  65. package/dist/product-view.js +42 -0
  66. package/dist/routes.d.ts +45 -0
  67. package/dist/routes.js +59 -0
  68. package/dist/schemas.d.ts +753 -0
  69. package/dist/schemas.js +377 -0
  70. package/dist/server.d.ts +4 -0
  71. package/dist/server.js +2 -0
  72. package/dist/types.d.ts +418 -0
  73. package/dist/types.js +1 -0
  74. package/dist/utils/image.d.ts +140 -0
  75. package/dist/utils/image.js +131 -0
  76. package/dist/utils/money.d.ts +39 -0
  77. package/dist/utils/money.js +43 -0
  78. package/dist/utils/product-image.d.ts +37 -0
  79. package/dist/utils/product-image.js +38 -0
  80. package/dist/whatsapp.d.ts +55 -0
  81. package/dist/whatsapp.js +89 -0
  82. package/package.json +76 -4
@@ -0,0 +1,582 @@
1
+ /**
2
+ * Deep API Client - Core Infrastructure
3
+ *
4
+ * Hides: caching, request deduplication, retry logic, validation,
5
+ * header management, cancellation, and URL construction.
6
+ *
7
+ * Deep module principle: Maximum leverage for namespace implementations.
8
+ * One infrastructure layer serves all API methods.
9
+ */
10
+ import { StorefrontError, isStorefrontError } from "./errors.js";
11
+ /**
12
+ * Result type for mutations (POST/PATCH).
13
+ *
14
+ * `domainError` is **always present on a failure the SDK produced** — every
15
+ * transport, HTTP, schema and abort path sets it, so a consumer branches on
16
+ * `result.domainError.category` without an optional-chain. It is declared as
17
+ * a required field on purpose: a new failure path that forgets it does not
18
+ * compile.
19
+ *
20
+ * `error` stays as the human-readable message (`domainError.message`), `code`
21
+ * stays an open string because the server's error codes are open (`HTTP_<status>`
22
+ * for gateway failures, the server's own code otherwise — a closed union would
23
+ * reject codes this SDK has not seen yet), and the index signature carries the
24
+ * server's flattened `context` (OTP's `windowSeconds`/`attemptsRemaining`/
25
+ * `terminal`, field issues) so a proxy can forward it without re-declaring each
26
+ * key. Read models do not come back as a `ClientResult` — reads degrade to
27
+ * `null`/`[]`/`{}` instead (see USAGE.md, "Failure and result contracts").
28
+ */
29
+ let cacheGeneration = 0;
30
+ const ABORT_MESSAGE = "The request was cancelled";
31
+ const TIMEOUT_MESSAGE = "The request timed out";
32
+ const MALFORMED_MESSAGE = "The server response could not be read";
33
+ const SCHEMA_MESSAGE = "Schema validation failed";
34
+ /**
35
+ * The server's own error categories, translated to SDK categories. This is the
36
+ * wire vocabulary of the platform's ERROR_CATEGORIES, not a second copy of it:
37
+ * the server always wins when it names a category, and the status-code mapping
38
+ * below only covers failures that arrive without one (gateways, proxies).
39
+ */
40
+ const WIRE_CATEGORIES = {
41
+ VALIDATION: "validation",
42
+ AUTHENTICATION: "authentication",
43
+ BUSINESS_LOGIC: "business",
44
+ SYSTEM: "system",
45
+ };
46
+ function isRecord(value) {
47
+ return typeof value === "object" && value !== null && !Array.isArray(value);
48
+ }
49
+ function readString(source, key) {
50
+ if (!isRecord(source))
51
+ return undefined;
52
+ const value = source[key];
53
+ return typeof value === "string" && value.length > 0 ? value : undefined;
54
+ }
55
+ function readNumber(source, key) {
56
+ if (!isRecord(source))
57
+ return undefined;
58
+ const value = source[key];
59
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
60
+ }
61
+ function categorizeFailure(status, wireCategory) {
62
+ if (typeof wireCategory === "string" && WIRE_CATEGORIES[wireCategory]) {
63
+ return WIRE_CATEGORIES[wireCategory];
64
+ }
65
+ if (status === 401 || status === 403)
66
+ return "authentication";
67
+ if (status === 400 || status === 422)
68
+ return "validation";
69
+ if (status === 404 || status === 409 || status === 429)
70
+ return "business";
71
+ if (status !== undefined && status >= 500)
72
+ return "system";
73
+ return "unknown";
74
+ }
75
+ /**
76
+ * What the platform actually did. A 2xx means it accepted the command, a 4xx is
77
+ * a clean refusal, and everything else — 5xx, dropped connections, timeouts —
78
+ * leaves the outcome unproven. Only a proven outcome may be replayed.
79
+ */
80
+ function determineSideEffectOutcome(status) {
81
+ if (status === undefined)
82
+ return "unknown";
83
+ if (status >= 200 && status < 300)
84
+ return "committed";
85
+ if (status >= 400 && status < 500)
86
+ return "rejected";
87
+ return "unknown";
88
+ }
89
+ /**
90
+ * A write is replayable only when the server refused it *and* said when to come
91
+ * back: retrying a dropped connection is how a shopper ends up with two orders.
92
+ * Reads are safe to repeat unless the request itself was rejected.
93
+ */
94
+ function isReplayable(status, readOnly) {
95
+ if (readOnly)
96
+ return status === 429 || status >= 500;
97
+ return status === 429;
98
+ }
99
+ /** Field-level errors from the server's Zod failures (`context.fields`). */
100
+ function readFieldIssues(context) {
101
+ if (!isRecord(context) || !Array.isArray(context.fields))
102
+ return undefined;
103
+ const issues = context.fields.flatMap((entry) => {
104
+ if (!isRecord(entry))
105
+ return [];
106
+ return [
107
+ {
108
+ field: typeof entry.path === "string" ? entry.path : "",
109
+ code: typeof entry.code === "string" ? entry.code : "INVALID",
110
+ message: typeof entry.message === "string" ? entry.message : "Invalid value",
111
+ },
112
+ ];
113
+ });
114
+ return issues.length > 0 ? issues : undefined;
115
+ }
116
+ function composeSignal(signal, timeoutMs) {
117
+ if (!signal && timeoutMs === undefined) {
118
+ return { signal: undefined, aborted: () => false, timedOut: () => false, cleanup: () => { } };
119
+ }
120
+ if (timeoutMs === undefined) {
121
+ return { signal, aborted: () => signal.aborted, timedOut: () => false, cleanup: () => { } };
122
+ }
123
+ const controller = new AbortController();
124
+ let timedOut = false;
125
+ const timer = setTimeout(() => {
126
+ timedOut = true;
127
+ controller.abort();
128
+ }, timeoutMs);
129
+ const forward = () => controller.abort();
130
+ if (signal) {
131
+ if (signal.aborted)
132
+ forward();
133
+ else
134
+ signal.addEventListener("abort", forward, { once: true });
135
+ }
136
+ return {
137
+ signal: controller.signal,
138
+ aborted: () => signal?.aborted === true,
139
+ timedOut: () => timedOut,
140
+ cleanup: () => {
141
+ clearTimeout(timer);
142
+ signal?.removeEventListener("abort", forward);
143
+ },
144
+ };
145
+ }
146
+ function abortedError(operation) {
147
+ return {
148
+ category: "transport",
149
+ code: "REQUEST_ABORTED",
150
+ message: ABORT_MESSAGE,
151
+ retryable: false,
152
+ operation,
153
+ sideEffect: "not_started",
154
+ };
155
+ }
156
+ /**
157
+ * A failure that happened before the server answered — or after it stopped
158
+ * answering. `readOnly` marks reads: only a read may be replayed on one of
159
+ * these, because a write's outcome is unproven and replaying it is how a
160
+ * shopper ends up with two orders.
161
+ */
162
+ function transportFailure(cause, signal, operation, readOnly) {
163
+ const sideEffect = readOnly ? "not_started" : "unknown";
164
+ if (signal.timedOut()) {
165
+ return {
166
+ category: "transport",
167
+ code: "REQUEST_TIMEOUT",
168
+ message: TIMEOUT_MESSAGE,
169
+ retryable: readOnly,
170
+ operation,
171
+ sideEffect,
172
+ };
173
+ }
174
+ if (signal.aborted()) {
175
+ return { ...abortedError(operation), sideEffect };
176
+ }
177
+ return {
178
+ category: "transport",
179
+ code: "NETWORK_ERROR",
180
+ message: cause instanceof Error ? cause.message : "Network error",
181
+ retryable: readOnly,
182
+ operation,
183
+ sideEffect,
184
+ };
185
+ }
186
+ /**
187
+ * A 2xx response the SDK could not read or validate. The server already
188
+ * committed the command — only the answer went missing — so a write must not be
189
+ * replayed on the strength of it.
190
+ */
191
+ function unreadableSuccess(code, message, operation, readOnly) {
192
+ return {
193
+ category: "system",
194
+ code,
195
+ message,
196
+ retryable: readOnly,
197
+ operation,
198
+ sideEffect: readOnly ? "not_started" : "committed",
199
+ };
200
+ }
201
+ /**
202
+ * A failure the server reported itself: its message, its code, its category, and
203
+ * every diagnostic it chose to publish in `context`.
204
+ */
205
+ function serverFailure(status, body, operation, readOnly) {
206
+ const context = isRecord(body) ? body.context : undefined;
207
+ const message = readString(body, "error") ??
208
+ readString(body, "message") ??
209
+ (status !== undefined ? `HTTP ${status}` : "Request failed");
210
+ const code = readString(body, "code");
211
+ const retryAfterSeconds = readNumber(context, "windowSeconds") ?? readNumber(body, "retryAfterSeconds");
212
+ const fieldIssues = readFieldIssues(context);
213
+ const requestId = readString(context, "requestId");
214
+ const domainError = {
215
+ category: categorizeFailure(status, isRecord(body) ? body.category : undefined),
216
+ code: code ?? (status !== undefined ? `HTTP_${status}` : "HTTP_ERROR"),
217
+ message,
218
+ retryable: isReplayable(status, readOnly),
219
+ operation,
220
+ sideEffect: determineSideEffectOutcome(status),
221
+ ...(fieldIssues !== undefined ? { fieldIssues } : {}),
222
+ ...(requestId !== undefined ? { requestId } : {}),
223
+ ...(retryAfterSeconds !== undefined ? { retryAfterSeconds } : {}),
224
+ };
225
+ return {
226
+ message,
227
+ code,
228
+ flatContext: isRecord(context) ? context : undefined,
229
+ domainError,
230
+ };
231
+ }
232
+ async function readBody(response) {
233
+ try {
234
+ return { ok: true, value: await response.json() };
235
+ }
236
+ catch {
237
+ return { ok: false };
238
+ }
239
+ }
240
+ /**
241
+ * Create client core infrastructure
242
+ *
243
+ * @param config - Client configuration
244
+ * @returns Core infrastructure for namespace implementations
245
+ */
246
+ export function createClientCore(config) {
247
+ const { serverUrl, apiKey } = config;
248
+ const requestCache = new Map();
249
+ const inflightRequests = new Map();
250
+ let observedCacheGeneration = cacheGeneration;
251
+ function resetClearedCaches() {
252
+ if (observedCacheGeneration === cacheGeneration)
253
+ return;
254
+ requestCache.clear();
255
+ inflightRequests.clear();
256
+ observedCacheGeneration = cacheGeneration;
257
+ }
258
+ /**
259
+ * Build full URL from path
260
+ */
261
+ function buildUrl(path) {
262
+ const base = serverUrl.replace(/\/$/, "");
263
+ const cleanPath = path.startsWith("/") ? path : `/${path}`;
264
+ return `${base}${cleanPath}`;
265
+ }
266
+ /**
267
+ * Build request headers
268
+ */
269
+ function buildHeaders(customHeaders) {
270
+ return {
271
+ "X-Store-API-Key": apiKey,
272
+ "Content-Type": "application/json",
273
+ ...customHeaders,
274
+ };
275
+ }
276
+ /**
277
+ * Execute fetch with retry logic.
278
+ *
279
+ * Retries are for reads only — callers pass `retries: 0` for anything that
280
+ * writes. An aborted signal is never retried and never worth waiting on.
281
+ */
282
+ async function fetchWithRetry(url, options, retries, signal) {
283
+ let lastError = null;
284
+ for (let attempt = 0; attempt <= retries; attempt++) {
285
+ if (signal?.aborted) {
286
+ throw new Error(ABORT_MESSAGE);
287
+ }
288
+ try {
289
+ return await fetch(url, options);
290
+ }
291
+ catch (error) {
292
+ lastError = error instanceof Error ? error : new Error("Network error");
293
+ if (signal?.aborted || attempt === retries) {
294
+ break;
295
+ }
296
+ // Exponential backoff: 100ms, 200ms
297
+ const delay = 100 * Math.pow(2, attempt);
298
+ await new Promise((resolve) => setTimeout(resolve, delay));
299
+ }
300
+ }
301
+ throw lastError ?? new Error("Network error");
302
+ }
303
+ /**
304
+ * GET request with in-flight deduplication and opt-in caching.
305
+ *
306
+ * Default behaviour: every call fetches fresh,
307
+ * but parallel identical calls coalesce into one request. Pass
308
+ * `{ cache: true }` for read-mostly data (config, categories, geo).
309
+ *
310
+ * Rejected and nullish results are never cached — a transient failure
311
+ * or an absent record must not be pinned for the isolate's lifetime.
312
+ *
313
+ * Pass `{ envelope: true }` for endpoints that carry metadata outside
314
+ * `data` (e.g. pagination totals) and need the full response body.
315
+ *
316
+ * A failing read never resolves to a wrong value: it throws a
317
+ * `StorefrontError` carrying the same `DomainError` the mutation path
318
+ * returns. A cancelled or timed-out read neither retries nor coalesces —
319
+ * one caller's abort must not cancel another's request, and an abandoned
320
+ * request must not be pinned in the cache.
321
+ */
322
+ async function get(path, schema, opts) {
323
+ resetClearedCaches();
324
+ const url = buildUrl(path);
325
+ const operation = opts?.operation ?? `GET ${path}`;
326
+ const request = composeSignal(opts?.signal, opts?.timeoutMs);
327
+ const shared = request.signal === undefined;
328
+ try {
329
+ // Check cache first (only successful results ever live here)
330
+ if (opts?.cache && shared) {
331
+ const cached = requestCache.get(url);
332
+ if (cached) {
333
+ return (await cached);
334
+ }
335
+ }
336
+ // Check in-flight requests for deduplication
337
+ if (shared) {
338
+ const inflight = inflightRequests.get(url);
339
+ if (inflight) {
340
+ return (await inflight);
341
+ }
342
+ }
343
+ // Create new request
344
+ const requestPromise = (async () => {
345
+ try {
346
+ const response = await fetchWithRetry(url, {
347
+ method: "GET",
348
+ headers: buildHeaders(),
349
+ ...(request.signal ? { signal: request.signal } : {}),
350
+ }, shared ? 2 : 0, request.signal);
351
+ if (!response.ok) {
352
+ const body = await readBody(response);
353
+ throw new StorefrontError(serverFailure(response.status, body.ok ? body.value : undefined, operation, true)
354
+ .domainError);
355
+ }
356
+ const body = await readBody(response);
357
+ if (!body.ok) {
358
+ throw new StorefrontError(unreadableSuccess("MALFORMED_RESPONSE", MALFORMED_MESSAGE, operation, true));
359
+ }
360
+ const json = body.value;
361
+ if (opts?.envelope) {
362
+ if (schema) {
363
+ const parsed = schema.safeParse(json);
364
+ if (!parsed.success) {
365
+ throw new StorefrontError(unreadableSuccess("SCHEMA_VALIDATION_FAILED", SCHEMA_MESSAGE, operation, true));
366
+ }
367
+ return parsed.data;
368
+ }
369
+ return json;
370
+ }
371
+ const data = isRecord(json) ? json.data : undefined;
372
+ // Validate with schema if provided
373
+ if (schema) {
374
+ const parsed = schema.safeParse(data);
375
+ if (!parsed.success) {
376
+ throw new StorefrontError(unreadableSuccess("SCHEMA_VALIDATION_FAILED", SCHEMA_MESSAGE, operation, true));
377
+ }
378
+ return parsed.data;
379
+ }
380
+ return data;
381
+ }
382
+ finally {
383
+ // Remove from inflight after completion
384
+ if (shared) {
385
+ inflightRequests.delete(url);
386
+ }
387
+ }
388
+ })();
389
+ // Store in-flight so parallel callers coalesce (only when no individual signal)
390
+ if (shared) {
391
+ inflightRequests.set(url, requestPromise);
392
+ }
393
+ if (opts?.cache && shared) {
394
+ requestCache.set(url, requestPromise);
395
+ // A failed or empty lookup must not be pinned: evict so the next
396
+ // call retries against the server.
397
+ requestPromise.then((value) => {
398
+ if (value == null)
399
+ requestCache.delete(url);
400
+ }, () => {
401
+ requestCache.delete(url);
402
+ });
403
+ }
404
+ return await requestPromise;
405
+ }
406
+ catch (error) {
407
+ if (error instanceof StorefrontError)
408
+ throw error;
409
+ throw new StorefrontError(transportFailure(error, request, operation, true));
410
+ }
411
+ finally {
412
+ request.cleanup();
413
+ }
414
+ }
415
+ /**
416
+ * POST request with error handling.
417
+ *
418
+ * By default the `{ data: ... }` envelope is unwrapped and `data`
419
+ * returned. Pass `topLevelData: true` for endpoints whose payload sits at
420
+ * the top level (e.g. `POST /store/abandoned` → `{ success, id }`).
421
+ *
422
+ * On failure the error result carries `code`, every field of the server's
423
+ * `context` object (`windowSeconds`, `attemptsRemaining`, `terminal`)
424
+ * flattened into the result — the checkout UI reads these — and the whole
425
+ * failure again as `domainError`, the serializable contract a caller may
426
+ * branch on. A POST never throws: an aborted or dropped request leaves the
427
+ * outcome `unknown`, never a retry of a possible duplicate.
428
+ */
429
+ async function post(path, body, opts) {
430
+ const url = buildUrl(path);
431
+ const operation = opts?.operation ?? `POST ${path}`;
432
+ const request = composeSignal(opts?.signal, opts?.timeoutMs);
433
+ if (request.aborted()) {
434
+ request.cleanup();
435
+ const domainError = abortedError(operation);
436
+ return { success: false, error: domainError.message, domainError };
437
+ }
438
+ try {
439
+ const response = await fetchWithRetry(url, {
440
+ method: "POST",
441
+ headers: buildHeaders(opts?.headers),
442
+ body: JSON.stringify(body),
443
+ ...(request.signal ? { signal: request.signal } : {}),
444
+ }, 0, request.signal);
445
+ const read = await readBody(response);
446
+ if (!response.ok) {
447
+ const failure = serverFailure(response.status, read.ok ? read.value : undefined, operation, false);
448
+ return {
449
+ success: false,
450
+ error: failure.message,
451
+ ...(failure.code !== undefined ? { code: failure.code } : {}),
452
+ domainError: failure.domainError,
453
+ ...failure.flatContext,
454
+ };
455
+ }
456
+ if (!read.ok || !isRecord(read.value)) {
457
+ const domainError = unreadableSuccess("MALFORMED_RESPONSE", MALFORMED_MESSAGE, operation, false);
458
+ return { success: false, error: domainError.message, domainError };
459
+ }
460
+ const payload = read.value;
461
+ const data = opts?.topLevelData ? payload : payload.data;
462
+ // Validate with schema if provided
463
+ if (opts?.schema) {
464
+ const parsed = opts.schema.safeParse(data);
465
+ if (!parsed.success) {
466
+ const domainError = unreadableSuccess("SCHEMA_VALIDATION_FAILED", SCHEMA_MESSAGE, operation, false);
467
+ return { success: false, error: domainError.message, domainError };
468
+ }
469
+ return { success: true, data: parsed.data };
470
+ }
471
+ return { success: true, data: data };
472
+ }
473
+ catch (error) {
474
+ const domainError = transportFailure(error, request, operation, false);
475
+ return { success: false, error: domainError.message, domainError };
476
+ }
477
+ finally {
478
+ request.cleanup();
479
+ }
480
+ }
481
+ /**
482
+ * PATCH request with error handling
483
+ */
484
+ async function patch(path, body, optsOrSchema) {
485
+ const url = buildUrl(path);
486
+ const opts = optsOrSchema && "safeParse" in optsOrSchema
487
+ ? { schema: optsOrSchema }
488
+ : (optsOrSchema ?? {});
489
+ const operation = opts.operation ?? `PATCH ${path}`;
490
+ const request = composeSignal(opts.signal, opts.timeoutMs);
491
+ if (request.aborted()) {
492
+ request.cleanup();
493
+ const domainError = abortedError(operation);
494
+ return { success: false, error: domainError.message, domainError };
495
+ }
496
+ try {
497
+ const response = await fetchWithRetry(url, {
498
+ method: "PATCH",
499
+ headers: buildHeaders(opts.headers),
500
+ body: JSON.stringify(body),
501
+ ...(request.signal ? { signal: request.signal } : {}),
502
+ }, 0, request.signal);
503
+ const read = await readBody(response);
504
+ if (!response.ok) {
505
+ const failure = serverFailure(response.status, read.ok ? read.value : undefined, operation, false);
506
+ return {
507
+ success: false,
508
+ error: failure.message,
509
+ ...(failure.code !== undefined ? { code: failure.code } : {}),
510
+ domainError: failure.domainError,
511
+ ...failure.flatContext,
512
+ };
513
+ }
514
+ if (!read.ok || !isRecord(read.value)) {
515
+ const domainError = unreadableSuccess("MALFORMED_RESPONSE", MALFORMED_MESSAGE, operation, false);
516
+ return { success: false, error: domainError.message, domainError };
517
+ }
518
+ const data = read.value.data;
519
+ // Validate with schema if provided
520
+ if (opts.schema) {
521
+ const parsed = opts.schema.safeParse(data);
522
+ if (!parsed.success) {
523
+ const domainError = unreadableSuccess("SCHEMA_VALIDATION_FAILED", SCHEMA_MESSAGE, operation, false);
524
+ return { success: false, error: domainError.message, domainError };
525
+ }
526
+ return { success: true, data: parsed.data };
527
+ }
528
+ return { success: true, data: data };
529
+ }
530
+ catch (error) {
531
+ const domainError = transportFailure(error, request, operation, false);
532
+ return { success: false, error: domainError.message, domainError };
533
+ }
534
+ finally {
535
+ request.cleanup();
536
+ }
537
+ }
538
+ return {
539
+ config,
540
+ get,
541
+ post,
542
+ patch,
543
+ };
544
+ }
545
+ /**
546
+ * Report a read that a namespace swallowed.
547
+ *
548
+ * Namespaces degrade instead of throwing — that is their contract — so this
549
+ * is the operator's only signal that a call did not complete. A failure the
550
+ * core produced already carries its DomainError; anything else (a bug inside
551
+ * a namespace, a storage exception) is normalised to the same shape, so a
552
+ * consumer's handler branches on one contract. A throwing hook never turns a
553
+ * degraded read into a thrown one.
554
+ */
555
+ export function reportDegradedRead(config, error, operation) {
556
+ const onError = config?.onError;
557
+ if (!onError)
558
+ return;
559
+ const domainError = isStorefrontError(error)
560
+ ? error.domainError
561
+ : {
562
+ category: "transport",
563
+ code: "NETWORK_ERROR",
564
+ message: error instanceof Error ? error.message : "Network error",
565
+ retryable: true,
566
+ operation,
567
+ sideEffect: "not_started",
568
+ };
569
+ try {
570
+ onError(domainError);
571
+ }
572
+ catch {
573
+ // Observability must never escalate a degraded read.
574
+ }
575
+ }
576
+ /**
577
+ * Clear the request cache
578
+ * Useful for testing or forcing fresh data
579
+ */
580
+ export function clearCache() {
581
+ cacheGeneration += 1;
582
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * SDK error type for read failures.
3
+ *
4
+ * Reads resolve their "absent" outcomes (null, [], {}) through the namespaces;
5
+ * everything else is a real failure and arrives as one of these. It stays an
6
+ * `Error` so existing `catch`/`instanceof Error` call sites keep working, and it
7
+ * carries a plain, serializable `DomainError` so a caller can branch on
8
+ * category, code, retryability and side-effect outcome instead of a string.
9
+ */
10
+ import type { DomainError } from "../domain.js";
11
+ export declare class StorefrontError extends Error {
12
+ readonly domainError: DomainError;
13
+ constructor(domainError: DomainError);
14
+ }
15
+ /**
16
+ * Recognises a StorefrontError across a realm boundary (worker, iframe, Astro
17
+ * action), where `instanceof` fails but the serialized shape survives.
18
+ */
19
+ export declare function isStorefrontError(value: unknown): value is StorefrontError;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * SDK error type for read failures.
3
+ *
4
+ * Reads resolve their "absent" outcomes (null, [], {}) through the namespaces;
5
+ * everything else is a real failure and arrives as one of these. It stays an
6
+ * `Error` so existing `catch`/`instanceof Error` call sites keep working, and it
7
+ * carries a plain, serializable `DomainError` so a caller can branch on
8
+ * category, code, retryability and side-effect outcome instead of a string.
9
+ */
10
+ export class StorefrontError extends Error {
11
+ domainError;
12
+ constructor(domainError) {
13
+ super(domainError.message);
14
+ this.name = "StorefrontError";
15
+ this.domainError = domainError;
16
+ }
17
+ }
18
+ /**
19
+ * Recognises a StorefrontError across a realm boundary (worker, iframe, Astro
20
+ * action), where `instanceof` fails but the serialized shape survives.
21
+ */
22
+ export function isStorefrontError(value) {
23
+ if (value instanceof StorefrontError)
24
+ return true;
25
+ if (typeof value !== "object" || value === null)
26
+ return false;
27
+ const candidate = value;
28
+ return (candidate.name === "StorefrontError" &&
29
+ typeof candidate.domainError === "object" &&
30
+ candidate.domainError !== null);
31
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Geo Namespace - Wilayas & Communes
3
+ *
4
+ * Deep module: hides where the wilaya dataset lives and the communes lookup
5
+ * behind two calls.
6
+ *
7
+ * `wilayas()` resolves the bundled static dataset — Algeria's 58 wilayas
8
+ * are stable reference data, so no network round-trip. It stays async so the
9
+ * data can move server-side without touching call sites.
10
+ *
11
+ * `communes()` reads the server and is cached per wilaya (reference data);
12
+ * failures resolve to `[]` — never throw. Wilaya IDs are 1–58; anything
13
+ * else is a 400 upstream, surfaced as `[]` (no communes to show).
14
+ */
15
+ import { type ClientCore } from "./core.js";
16
+ import type { Commune, Wilaya } from "../types.js";
17
+ export interface GeoNamespace {
18
+ /**
19
+ * Algeria's 58 wilayas, with Arabic names for RTL checkout forms.
20
+ */
21
+ wilayas(): Promise<Wilaya[]>;
22
+ /**
23
+ * Communes for one wilaya (1–58), cached. Resolves to `[]` on failure
24
+ * or an unknown wilaya.
25
+ */
26
+ communes(wilayaId: number | string): Promise<Commune[]>;
27
+ }
28
+ /**
29
+ * Create geo namespace
30
+ */
31
+ export declare function createGeoNamespace(core: ClientCore): GeoNamespace;