@marianmeres/ecsuite 1.3.2 → 2.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.
@@ -3,23 +3,28 @@
3
3
  *
4
4
  * Product domain manager with in-memory caching.
5
5
  *
6
- * Read-only domain for fetching product data with caching support.
7
- * Does not use state machine - just a simple cache layer.
6
+ * Read-only domain for fetching product data with TTL caching. Extends
7
+ * `BaseDomainManager` for unified observability (subscribe, domain:error,
8
+ * domain:state:changed) but the per-product cache itself is held in a
9
+ * private Map — the store's `data` is null because there is no single
10
+ * aggregate state worth subscribing to (use `getCacheSize()` /
11
+ * `isCached()` for cache introspection).
8
12
  */
9
- import { createClog } from "@marianmeres/clog";
10
- import { createPubSub } from "@marianmeres/pubsub";
13
+ import { BaseDomainManager } from "./base.js";
11
14
  /**
12
- * Product manager with in-memory caching.
15
+ * Product manager with in-memory TTL caching.
13
16
  *
14
- * Unlike other domain managers, ProductManager uses a simple cache layer
15
- * instead of a full state machine. Products are fetched on-demand and cached
16
- * with a configurable TTL.
17
+ * Extends `BaseDomainManager` for unified observability (Svelte-compatible
18
+ * `subscribe`, `domain:state:changed`, `domain:error` events) but skips the
19
+ * per-product state machine — fetching a single product never transitions
20
+ * the whole domain into "syncing" because that would be misleading.
17
21
  *
18
22
  * Features:
19
- * - In-memory cache with TTL expiration
23
+ * - In-memory cache with per-entry TTL expiration
20
24
  * - Single and batch product fetching
25
+ * - In-flight request dedup (cache stampede prevention)
21
26
  * - Prefetching support for UI optimization
22
- * - Event emission on fetch
27
+ * - `domain:error` event emission on adapter failures
23
28
  *
24
29
  * @example
25
30
  * ```typescript
@@ -27,88 +32,79 @@ import { createPubSub } from "@marianmeres/pubsub";
27
32
  * adapter: myProductAdapter,
28
33
  * cacheTtl: 10 * 60 * 1000, // 10 minutes
29
34
  * });
35
+ * await products.initialize();
30
36
  *
31
37
  * const product = await products.getById("prod-123");
32
38
  * const many = await products.getByIds(["prod-1", "prod-2"]);
33
39
  * ```
34
40
  */
35
- export class ProductManager {
36
- #clog = createClog("ecsuite:product", { color: "auto" });
37
- #pubsub;
38
- #adapter = null;
39
- #context = {};
41
+ export class ProductManager extends BaseDomainManager {
40
42
  #cache = new Map();
41
43
  #cacheTtl;
44
+ /** Pending fetches by id, used to dedup concurrent callers (D8). */
45
+ #inflight = new Map();
42
46
  constructor(options = {}) {
43
- this.#adapter = options.adapter ?? null;
44
- this.#context = options.context ?? {};
45
- this.#pubsub = options.pubsub ?? createPubSub();
46
- this.#cacheTtl = options.cacheTtl ?? 5 * 60 * 1000; // 5 minutes default
47
- this.#clog.debug("initialized", { cacheTtl: this.#cacheTtl });
48
- }
49
- /**
50
- * Set the product adapter for server communication.
51
- *
52
- * @param adapter - The ProductAdapter implementation
53
- */
54
- setAdapter(adapter) {
55
- this.#adapter = adapter;
56
- this.#clog.debug("adapter set");
57
- }
58
- /**
59
- * Get the current adapter.
60
- *
61
- * @returns The adapter or null if not set
62
- */
63
- getAdapter() {
64
- return this.#adapter;
65
- }
66
- /**
67
- * Update context (customerId, sessionId).
68
- *
69
- * @param context - Context to merge with existing context
70
- */
71
- setContext(context) {
72
- this.#context = { ...this.#context, ...context };
47
+ super("product", {
48
+ ...options,
49
+ // Cache lives in a private Map; the store's `data` carries no
50
+ // aggregate so persistence is meaningless here.
51
+ storageType: null,
52
+ });
53
+ if (options.adapter) {
54
+ this.adapter = options.adapter;
55
+ }
56
+ this.#cacheTtl = options.cacheTtl ?? 5 * 60 * 1000;
73
57
  }
74
58
  /**
75
- * Get the current context.
76
- *
77
- * @returns Copy of the current context
59
+ * Initialize. Lazy domain — there's nothing to fetch eagerly. Just
60
+ * transitions to "ready" so consumers can rely on the same readiness
61
+ * contract as other domains.
78
62
  */
79
- getContext() {
80
- return { ...this.#context };
63
+ initialize() {
64
+ this.clog.debug("initialize");
65
+ this.setState("ready");
66
+ return Promise.resolve();
81
67
  }
82
68
  /**
83
69
  * Get a single product by ID.
84
- * Returns from cache if valid, otherwise fetches from server.
70
+ * Returns from cache if valid, otherwise fetches from server. Concurrent
71
+ * callers for the same id share a single in-flight request.
85
72
  *
86
73
  * @param productId - The product ID to fetch
87
- * @returns The product data or null if not found/error
74
+ * @returns The product data or null if not found / no adapter
88
75
  * @emits product:fetched - On successful server fetch
76
+ * @emits domain:error - On adapter failure
89
77
  */
90
78
  async getById(productId) {
91
- // Check cache first
92
79
  const cached = this.#getFromCache(productId);
93
- if (cached) {
80
+ if (cached)
94
81
  return cached;
95
- }
96
- // Fetch from server
97
- if (!this.#adapter) {
98
- this.#clog.debug("getById: no adapter", { productId });
99
- return null;
100
- }
101
- this.#clog.debug("getById: fetching", { productId });
102
- try {
103
- const data = await this.#adapter.fetchOne(productId, this.#context);
104
- this.#setCache(productId, data);
105
- this.#emitFetched(productId);
106
- return data;
107
- }
108
- catch (e) {
109
- this.#clog.error("getById failed", { productId, error: e });
82
+ if (!this.adapter) {
83
+ this.clog.debug("getById: no adapter", { productId });
110
84
  return null;
111
85
  }
86
+ // Dedup concurrent callers (cache stampede prevention)
87
+ const pending = this.#inflight.get(productId);
88
+ if (pending)
89
+ return pending;
90
+ const promise = (async () => {
91
+ this.clog.debug("getById: fetching", { productId });
92
+ try {
93
+ const data = await this.adapter.fetchOne(productId, this.context);
94
+ this.#setCache(productId, data);
95
+ this.#emitFetched(productId);
96
+ return data;
97
+ }
98
+ catch (e) {
99
+ this.#emitError("getById", e, { productId });
100
+ return null;
101
+ }
102
+ finally {
103
+ this.#inflight.delete(productId);
104
+ }
105
+ })();
106
+ this.#inflight.set(productId, promise);
107
+ return promise;
112
108
  }
113
109
  /**
114
110
  * Get multiple products by IDs.
@@ -117,11 +113,11 @@ export class ProductManager {
117
113
  * @param productIds - Array of product IDs to fetch
118
114
  * @returns Map of productId to ProductData
119
115
  * @emits product:fetched - For each product fetched from server
116
+ * @emits domain:error - On adapter failure
120
117
  */
121
118
  async getByIds(productIds) {
122
119
  const result = new Map();
123
120
  const missingIds = [];
124
- // Check cache for each product
125
121
  for (const id of productIds) {
126
122
  const cached = this.#getFromCache(id);
127
123
  if (cached) {
@@ -131,17 +127,15 @@ export class ProductManager {
131
127
  missingIds.push(id);
132
128
  }
133
129
  }
134
- // Fetch missing from server
135
- if (missingIds.length > 0 && this.#adapter) {
136
- this.#clog.debug("getByIds: fetching missing", {
130
+ if (missingIds.length > 0 && this.adapter) {
131
+ this.clog.debug("getByIds: fetching missing", {
137
132
  total: productIds.length,
138
133
  cached: result.size,
139
134
  missing: missingIds.length,
140
135
  });
141
136
  try {
142
- const fetchedData = await this.#adapter.fetchMany(missingIds, this.#context);
137
+ const fetchedData = await this.adapter.fetchMany(missingIds, this.context);
143
138
  for (const product of fetchedData) {
144
- // Products from collection-types have model_id
145
139
  const productId = product.model_id;
146
140
  if (productId) {
147
141
  this.#setCache(productId, product);
@@ -151,7 +145,7 @@ export class ProductManager {
151
145
  }
152
146
  }
153
147
  catch (e) {
154
- this.#clog.error("getByIds failed", { missingIds, error: e });
148
+ this.#emitError("getByIds", e, { missingIds });
155
149
  }
156
150
  }
157
151
  return result;
@@ -166,7 +160,7 @@ export class ProductManager {
166
160
  const missingIds = productIds.filter((id) => !this.isCached(id));
167
161
  if (missingIds.length === 0)
168
162
  return;
169
- this.#clog.debug("prefetch", { count: missingIds.length });
163
+ this.clog.debug("prefetch", { count: missingIds.length });
170
164
  await this.getByIds(missingIds);
171
165
  }
172
166
  /**
@@ -177,11 +171,11 @@ export class ProductManager {
177
171
  clearCache(productId) {
178
172
  if (productId) {
179
173
  this.#cache.delete(productId);
180
- this.#clog.debug("cache cleared for product", { productId });
174
+ this.clog.debug("cache cleared for product", { productId });
181
175
  }
182
176
  else {
183
177
  this.#cache.clear();
184
- this.#clog.debug("cache cleared entirely");
178
+ this.clog.debug("cache cleared entirely");
185
179
  }
186
180
  }
187
181
  /**
@@ -204,13 +198,18 @@ export class ProductManager {
204
198
  getCacheSize() {
205
199
  return this.#cache.size;
206
200
  }
201
+ /** Reset clears the cache too (overrides base reset). */
202
+ reset() {
203
+ this.#cache.clear();
204
+ this.#inflight.clear();
205
+ super.reset();
206
+ }
207
207
  /** Get product from cache if valid */
208
208
  #getFromCache(productId) {
209
209
  const entry = this.#cache.get(productId);
210
210
  if (!entry)
211
211
  return null;
212
212
  if (Date.now() >= entry.expiresAt) {
213
- // Expired, remove from cache
214
213
  this.#cache.delete(productId);
215
214
  return null;
216
215
  }
@@ -231,6 +230,25 @@ export class ProductManager {
231
230
  timestamp: Date.now(),
232
231
  productId,
233
232
  };
234
- this.#pubsub.publish(event.type, event);
233
+ this.pubsub.publish(event.type, event);
234
+ }
235
+ /**
236
+ * Emit `domain:error` without changing state. A single failed product
237
+ * fetch should not blanket the whole domain in "error" — other cached
238
+ * lookups remain valid.
239
+ */
240
+ #emitError(operation, e, context) {
241
+ this.clog.error(`${operation} failed`, { ...context, error: e });
242
+ this.emit({
243
+ type: "domain:error",
244
+ domain: "product",
245
+ timestamp: Date.now(),
246
+ error: {
247
+ code: "FETCH_FAILED",
248
+ message: e instanceof Error ? e.message : "Failed to fetch",
249
+ operation,
250
+ originalError: e,
251
+ },
252
+ });
235
253
  }
236
254
  }
@@ -80,8 +80,7 @@ export class WishlistManager extends BaseDomainManager {
80
80
  async addItem(productId) {
81
81
  this.clog.debug("addItem", { productId });
82
82
  // Check if already in wishlist
83
- const current = this.store.get().data ?? { items: [] };
84
- if (current.items.some((i) => i.product_id === productId)) {
83
+ if (this.hasProduct(productId)) {
85
84
  return; // Already in wishlist, no-op
86
85
  }
87
86
  const newItem = {
@@ -89,6 +88,9 @@ export class WishlistManager extends BaseDomainManager {
89
88
  added_at: Date.now(),
90
89
  };
91
90
  await this.withOptimisticUpdate("addItem", () => {
91
+ // Re-read inside callback so we operate on the latest state
92
+ // even if a prior in-flight mutation has just settled.
93
+ const current = this.store.get().data ?? { items: [] };
92
94
  const items = [...current.items, newItem];
93
95
  this.setData({ items }, false);
94
96
  }, async () => {
package/dist/suite.d.ts CHANGED
@@ -43,6 +43,13 @@ export interface ECSuiteConfig {
43
43
  autoInitialize?: boolean;
44
44
  /** Domains to initialize (default: all). Used by both autoInitialize and manual initialize(). */
45
45
  initializeDomains?: InitializableDomainName[];
46
+ /**
47
+ * Reset and re-initialize all domains automatically when `setContext()`
48
+ * changes `customerId` (or transitions between guest and authenticated).
49
+ * Default: `true`. Set to `false` to manage identity transitions yourself
50
+ * via `switchIdentity()` or explicit `reset()` + `initialize()`.
51
+ */
52
+ autoResetOnIdentityChange?: boolean;
46
53
  }
47
54
  /**
48
55
  * Main ECSuite class - orchestrates all e-commerce domain managers.
@@ -63,6 +70,19 @@ export interface ECSuiteConfig {
63
70
  */
64
71
  export declare class ECSuite {
65
72
  #private;
73
+ /**
74
+ * Resolves when the most recent `initialize()` (auto or manual, including
75
+ * the one triggered by an identity switch) has settled. Always available;
76
+ * defaults to `Promise.resolve()` if `autoInitialize: false`.
77
+ *
78
+ * @example
79
+ * ```typescript
80
+ * const suite = createECSuite({ adapters: { cart } });
81
+ * await suite.ready; // wait for initial fetches
82
+ * await suite.cart.addItem(...);
83
+ * ```
84
+ */
85
+ ready: Promise<void>;
66
86
  /** Cart domain manager */
67
87
  readonly cart: CartManager;
68
88
  /** Wishlist domain manager */
@@ -94,8 +114,25 @@ export declare class ECSuite {
94
114
  * ```
95
115
  */
96
116
  initialize(domains?: InitializableDomainName[]): Promise<void>;
97
- /** Update context across all domains */
117
+ /**
118
+ * Update context across all domains.
119
+ *
120
+ * If `customerId` transitions (including to/from undefined) and
121
+ * `autoResetOnIdentityChange` is enabled (default), this also resets
122
+ * all domains and re-initializes them — assigning the new in-flight
123
+ * promise to `ready`. Callers that need to wait for the new identity's
124
+ * data should `await suite.ready` afterwards (or use `switchIdentity`).
125
+ */
98
126
  setContext(context: DomainContext): void;
127
+ /**
128
+ * Atomically switch to a new identity: merge context, reset all domains,
129
+ * and re-initialize. Returns a promise that settles when the re-init
130
+ * completes. Use this instead of `setContext` when you need to await
131
+ * the identity switch (also updates `suite.ready`).
132
+ *
133
+ * Works even when `autoResetOnIdentityChange: false`.
134
+ */
135
+ switchIdentity(context: DomainContext): Promise<void>;
99
136
  /** Get the current context */
100
137
  getContext(): DomainContext;
101
138
  /** Subscribe to specific event type */
@@ -132,6 +169,17 @@ export declare class ECSuite {
132
169
  }) => void): Unsubscriber;
133
170
  /** Reset all domains to initial state */
134
171
  reset(): void;
172
+ /**
173
+ * Tear down the suite: unsubscribe all listeners on the internal pubsub
174
+ * and clear the product cache. Use when the consumer (e.g., a SPA
175
+ * lifecycle hook) is done with the suite — long-lived apps that recreate
176
+ * suites otherwise leak subscribers across instances.
177
+ *
178
+ * Persisted storage is intentionally NOT cleared (cart/wishlist data
179
+ * should survive page reloads); call `reset()` first if you also want
180
+ * to wipe in-memory state.
181
+ */
182
+ destroy(): void;
135
183
  }
136
184
  /**
137
185
  * Factory function to create an ECSuite instance.
package/dist/suite.js CHANGED
@@ -12,6 +12,15 @@ import { OrderManager } from "./domains/order.js";
12
12
  import { CustomerManager } from "./domains/customer.js";
13
13
  import { PaymentManager } from "./domains/payment.js";
14
14
  import { ProductManager } from "./domains/product.js";
15
+ /** Combine multiple unsubscribers into one (also `Symbol.dispose`-compatible). */
16
+ function combineUnsubscribers(...unsubs) {
17
+ const fn = () => {
18
+ for (const u of unsubs)
19
+ u();
20
+ };
21
+ fn[Symbol.dispose] = fn;
22
+ return fn;
23
+ }
15
24
  /**
16
25
  * Main ECSuite class - orchestrates all e-commerce domain managers.
17
26
  *
@@ -33,6 +42,21 @@ export class ECSuite {
33
42
  #clog = createClog("ecsuite", { color: "auto" });
34
43
  #pubsub;
35
44
  #context;
45
+ #initDomains;
46
+ #autoResetOnIdentityChange;
47
+ /**
48
+ * Resolves when the most recent `initialize()` (auto or manual, including
49
+ * the one triggered by an identity switch) has settled. Always available;
50
+ * defaults to `Promise.resolve()` if `autoInitialize: false`.
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * const suite = createECSuite({ adapters: { cart } });
55
+ * await suite.ready; // wait for initial fetches
56
+ * await suite.cart.addItem(...);
57
+ * ```
58
+ */
59
+ ready = Promise.resolve();
36
60
  /** Cart domain manager */
37
61
  cart;
38
62
  /** Wishlist domain manager */
@@ -52,6 +76,8 @@ export class ECSuite {
52
76
  });
53
77
  this.#pubsub = createPubSub();
54
78
  this.#context = config.context ?? {};
79
+ this.#initDomains = config.initializeDomains;
80
+ this.#autoResetOnIdentityChange = config.autoResetOnIdentityChange !== false;
55
81
  const storageType = config.storage?.type ?? "local";
56
82
  // Initialize domain managers with shared pubsub
57
83
  this.cart = new CartManager({
@@ -89,9 +115,10 @@ export class ECSuite {
89
115
  pubsub: this.#pubsub,
90
116
  cacheTtl: config.productCacheTtl,
91
117
  });
92
- // Auto-initialize if configured
118
+ // Auto-initialize if configured. `ready` exposes the in-flight promise
119
+ // so callers don't race the constructor.
93
120
  if (config.autoInitialize !== false) {
94
- this.initialize(config.initializeDomains);
121
+ this.ready = this.initialize(this.#initDomains);
95
122
  }
96
123
  }
97
124
  /** Initialize domains. When called without arguments, initializes all domains.
@@ -118,15 +145,55 @@ export class ECSuite {
118
145
  "order",
119
146
  "customer",
120
147
  "payment",
148
+ "product",
121
149
  ];
122
- const toInit = domains ?? all;
150
+ const toInit = domains ?? this.#initDomains ?? all;
151
+ // Remember the most recently initialized set so identity switches
152
+ // (and other re-inits) re-fetch the same domains.
153
+ this.#initDomains = toInit;
123
154
  this.#clog.debug("initializing domains", toInit);
124
155
  await Promise.all(toInit.map((name) => this[name].initialize()));
125
156
  this.#clog.debug("domains initialized", toInit);
126
157
  }
127
- /** Update context across all domains */
158
+ /**
159
+ * Update context across all domains.
160
+ *
161
+ * If `customerId` transitions (including to/from undefined) and
162
+ * `autoResetOnIdentityChange` is enabled (default), this also resets
163
+ * all domains and re-initializes them — assigning the new in-flight
164
+ * promise to `ready`. Callers that need to wait for the new identity's
165
+ * data should `await suite.ready` afterwards (or use `switchIdentity`).
166
+ */
128
167
  setContext(context) {
129
168
  this.#clog.debug("setContext", context);
169
+ const prevCustomerId = this.#context.customerId;
170
+ this.#context = { ...this.#context, ...context };
171
+ this.cart.setContext(context);
172
+ this.wishlist.setContext(context);
173
+ this.order.setContext(context);
174
+ this.customer.setContext(context);
175
+ this.payment.setContext(context);
176
+ this.product.setContext(context);
177
+ const newCustomerId = this.#context.customerId;
178
+ if (this.#autoResetOnIdentityChange && prevCustomerId !== newCustomerId) {
179
+ this.#clog.debug("identity changed; resetting domains", {
180
+ from: prevCustomerId,
181
+ to: newCustomerId,
182
+ });
183
+ this.reset();
184
+ this.ready = this.initialize(this.#initDomains);
185
+ }
186
+ }
187
+ /**
188
+ * Atomically switch to a new identity: merge context, reset all domains,
189
+ * and re-initialize. Returns a promise that settles when the re-init
190
+ * completes. Use this instead of `setContext` when you need to await
191
+ * the identity switch (also updates `suite.ready`).
192
+ *
193
+ * Works even when `autoResetOnIdentityChange: false`.
194
+ */
195
+ async switchIdentity(context) {
196
+ this.#clog.debug("switchIdentity", context);
130
197
  this.#context = { ...this.#context, ...context };
131
198
  this.cart.setContext(context);
132
199
  this.wishlist.setContext(context);
@@ -134,6 +201,9 @@ export class ECSuite {
134
201
  this.customer.setContext(context);
135
202
  this.payment.setContext(context);
136
203
  this.product.setContext(context);
204
+ this.reset();
205
+ this.ready = this.initialize(this.#initDomains);
206
+ await this.ready;
137
207
  }
138
208
  /** Get the current context */
139
209
  getContext() {
@@ -186,10 +256,7 @@ export class ECSuite {
186
256
  error: event.error,
187
257
  });
188
258
  });
189
- return () => {
190
- unsub1();
191
- unsub2();
192
- };
259
+ return combineUnsubscribers(unsub1, unsub2);
193
260
  }
194
261
  /** Reset all domains to initial state */
195
262
  reset() {
@@ -201,6 +268,21 @@ export class ECSuite {
201
268
  this.payment.reset();
202
269
  this.product.clearCache();
203
270
  }
271
+ /**
272
+ * Tear down the suite: unsubscribe all listeners on the internal pubsub
273
+ * and clear the product cache. Use when the consumer (e.g., a SPA
274
+ * lifecycle hook) is done with the suite — long-lived apps that recreate
275
+ * suites otherwise leak subscribers across instances.
276
+ *
277
+ * Persisted storage is intentionally NOT cleared (cart/wishlist data
278
+ * should survive page reloads); call `reset()` first if you also want
279
+ * to wipe in-memory state.
280
+ */
281
+ destroy() {
282
+ this.#clog.debug("destroy");
283
+ this.product.clearCache();
284
+ this.#pubsub.unsubscribeAll();
285
+ }
204
286
  }
205
287
  /**
206
288
  * Factory function to create an ECSuite instance.
@@ -18,8 +18,6 @@ export interface CartAdapter {
18
18
  removeItem(productId: UUID, ctx: DomainContext): Promise<CartData>;
19
19
  /** Clear all items */
20
20
  clear(ctx: DomainContext): Promise<CartData>;
21
- /** Sync full cart state (for optimistic update reconciliation) */
22
- sync(cart: CartData, ctx: DomainContext): Promise<CartData>;
23
21
  }
24
22
  /** Wishlist adapter interface */
25
23
  export interface WishlistAdapter {
@@ -31,18 +29,23 @@ export interface WishlistAdapter {
31
29
  removeItem(productId: UUID, ctx: DomainContext): Promise<WishlistData>;
32
30
  /** Clear all items */
33
31
  clear(ctx: DomainContext): Promise<WishlistData>;
34
- /** Sync full wishlist state */
35
- sync(wishlist: WishlistData, ctx: DomainContext): Promise<WishlistData>;
36
32
  }
37
33
  /** Order create payload (status is set by server) */
38
34
  export type OrderCreatePayload = Omit<OrderData, "status">;
39
35
  export type { OrderCreateResult } from "@marianmeres/collection-types";
40
- /** Order adapter interface (read + create) */
36
+ /**
37
+ * Order adapter interface (read + create).
38
+ *
39
+ * All read/create methods return `OrderCreateResult` (`{ model_id, data }`)
40
+ * so the manager can identify orders by their server-assigned `model_id`.
41
+ * Returning bare `OrderData` previously made dedup/update impossible because
42
+ * `OrderData` has no `model_id` field — only an open index signature.
43
+ */
41
44
  export interface OrderAdapter {
42
45
  /** Fetch all orders for customer */
43
- fetchAll(ctx: DomainContext): Promise<OrderData[]>;
46
+ fetchAll(ctx: DomainContext): Promise<OrderCreateResult[]>;
44
47
  /** Fetch single order by ID */
45
- fetchOne(orderId: UUID, ctx: DomainContext): Promise<OrderData>;
48
+ fetchOne(orderId: UUID, ctx: DomainContext): Promise<OrderCreateResult>;
46
49
  /** Create new order — returns data + model_id */
47
50
  create(order: OrderCreatePayload, ctx: DomainContext): Promise<OrderCreateResult>;
48
51
  }
@@ -8,8 +8,12 @@ import type { UUID } from "@marianmeres/collection-types";
8
8
  import type { DomainError, DomainState } from "./state.js";
9
9
  /** Domain identifiers */
10
10
  export type DomainName = "cart" | "wishlist" | "order" | "customer" | "payment" | "product";
11
- /** Domain names that support initialize() (excludes product which is fully lazy) */
12
- export type InitializableDomainName = Exclude<DomainName, "product">;
11
+ /**
12
+ * Domain names that support `initialize()`. As of the unified ProductManager,
13
+ * every domain implements initialize() — for product it is a no-op that
14
+ * transitions to "ready" so consumers can rely on the same readiness contract.
15
+ */
16
+ export type InitializableDomainName = DomainName;
13
17
  /** Event types emitted by the suite */
14
18
  export type ECSuiteEventType = "domain:state:changed" | "domain:error" | "domain:synced" | "cart:item:added" | "cart:item:updated" | "cart:item:removed" | "cart:cleared" | "wishlist:item:added" | "wishlist:item:removed" | "wishlist:cleared" | "order:created" | "order:fetched" | "customer:updated" | "customer:fetched" | "payment:fetched" | "payment:initiated" | "payment:captured" | "product:fetched";
15
19
  /** Base event data */