@marianmeres/ecsuite 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.
- package/AGENTS.md +218 -0
- package/API.md +865 -0
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/dist/adapters/mock/cart.d.ts +20 -0
- package/dist/adapters/mock/cart.js +86 -0
- package/dist/adapters/mock/customer.d.ts +20 -0
- package/dist/adapters/mock/customer.js +58 -0
- package/dist/adapters/mock/mod.d.ts +9 -0
- package/dist/adapters/mock/mod.js +9 -0
- package/dist/adapters/mock/order.d.ts +20 -0
- package/dist/adapters/mock/order.js +66 -0
- package/dist/adapters/mock/payment.d.ts +20 -0
- package/dist/adapters/mock/payment.js +55 -0
- package/dist/adapters/mock/product.d.ts +25 -0
- package/dist/adapters/mock/product.js +60 -0
- package/dist/adapters/mock/wishlist.d.ts +20 -0
- package/dist/adapters/mock/wishlist.js +70 -0
- package/dist/adapters/mod.d.ts +6 -0
- package/dist/adapters/mod.js +6 -0
- package/dist/domains/base.d.ts +83 -0
- package/dist/domains/base.js +187 -0
- package/dist/domains/cart.d.ts +96 -0
- package/dist/domains/cart.js +287 -0
- package/dist/domains/customer.d.ts +74 -0
- package/dist/domains/customer.js +183 -0
- package/dist/domains/mod.d.ts +13 -0
- package/dist/domains/mod.js +13 -0
- package/dist/domains/order.d.ts +83 -0
- package/dist/domains/order.js +233 -0
- package/dist/domains/payment.d.ts +83 -0
- package/dist/domains/payment.js +175 -0
- package/dist/domains/product.d.ts +130 -0
- package/dist/domains/product.js +241 -0
- package/dist/domains/wishlist.d.ts +101 -0
- package/dist/domains/wishlist.js +256 -0
- package/dist/mod.d.ts +28 -0
- package/dist/mod.js +32 -0
- package/dist/suite.d.ts +115 -0
- package/dist/suite.js +168 -0
- package/dist/types/adapter.d.ts +77 -0
- package/dist/types/adapter.js +7 -0
- package/dist/types/events.d.ts +111 -0
- package/dist/types/events.js +7 -0
- package/dist/types/mod.d.ts +9 -0
- package/dist/types/mod.js +9 -0
- package/dist/types/state.d.ts +61 -0
- package/dist/types/state.js +7 -0
- package/package.json +28 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module domains/product
|
|
3
|
+
*
|
|
4
|
+
* Product domain manager with in-memory caching.
|
|
5
|
+
*
|
|
6
|
+
* Read-only domain for fetching product data with caching support.
|
|
7
|
+
* Does not use state machine - just a simple cache layer.
|
|
8
|
+
*/
|
|
9
|
+
import { createClog } from "@marianmeres/clog";
|
|
10
|
+
import { createPubSub } from "@marianmeres/pubsub";
|
|
11
|
+
/**
|
|
12
|
+
* Product manager with in-memory caching.
|
|
13
|
+
*
|
|
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
|
+
*
|
|
18
|
+
* Features:
|
|
19
|
+
* - In-memory cache with TTL expiration
|
|
20
|
+
* - Single and batch product fetching
|
|
21
|
+
* - Prefetching support for UI optimization
|
|
22
|
+
* - Event emission on fetch
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* const products = new ProductManager({
|
|
27
|
+
* adapter: myProductAdapter,
|
|
28
|
+
* cacheTtl: 10 * 60 * 1000, // 10 minutes
|
|
29
|
+
* });
|
|
30
|
+
*
|
|
31
|
+
* const product = await products.getById("prod-123");
|
|
32
|
+
* const many = await products.getByIds(["prod-1", "prod-2"]);
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export class ProductManager {
|
|
36
|
+
_clog = createClog("ecsuite:product", { color: "auto" });
|
|
37
|
+
_pubsub;
|
|
38
|
+
_adapter = null;
|
|
39
|
+
_context = {};
|
|
40
|
+
_cache = new Map();
|
|
41
|
+
_cacheTtl;
|
|
42
|
+
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 };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get the current context.
|
|
76
|
+
*
|
|
77
|
+
* @returns Copy of the current context
|
|
78
|
+
*/
|
|
79
|
+
getContext() {
|
|
80
|
+
return { ...this._context };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get a single product by ID.
|
|
84
|
+
* Returns from cache if valid, otherwise fetches from server.
|
|
85
|
+
*
|
|
86
|
+
* @param productId - The product ID to fetch
|
|
87
|
+
* @returns The product data or null if not found/error
|
|
88
|
+
* @emits product:fetched - On successful server fetch
|
|
89
|
+
*/
|
|
90
|
+
async getById(productId) {
|
|
91
|
+
// Check cache first
|
|
92
|
+
const cached = this._getFromCache(productId);
|
|
93
|
+
if (cached) {
|
|
94
|
+
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 result = await this._adapter.fetchOne(productId, this._context);
|
|
104
|
+
if (result.success && result.data) {
|
|
105
|
+
this._setCache(productId, result.data);
|
|
106
|
+
this._emitFetched(productId);
|
|
107
|
+
return result.data;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
this._clog.error("getById failed", { productId, error: e });
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get multiple products by IDs.
|
|
118
|
+
* Returns from cache when available, fetches missing products in batch.
|
|
119
|
+
*
|
|
120
|
+
* @param productIds - Array of product IDs to fetch
|
|
121
|
+
* @returns Map of productId to ProductData
|
|
122
|
+
* @emits product:fetched - For each product fetched from server
|
|
123
|
+
*/
|
|
124
|
+
async getByIds(productIds) {
|
|
125
|
+
const result = new Map();
|
|
126
|
+
const missingIds = [];
|
|
127
|
+
// Check cache for each product
|
|
128
|
+
for (const id of productIds) {
|
|
129
|
+
const cached = this._getFromCache(id);
|
|
130
|
+
if (cached) {
|
|
131
|
+
result.set(id, cached);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
missingIds.push(id);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Fetch missing from server
|
|
138
|
+
if (missingIds.length > 0 && this._adapter) {
|
|
139
|
+
this._clog.debug("getByIds: fetching missing", {
|
|
140
|
+
total: productIds.length,
|
|
141
|
+
cached: result.size,
|
|
142
|
+
missing: missingIds.length,
|
|
143
|
+
});
|
|
144
|
+
try {
|
|
145
|
+
const fetchResult = await this._adapter.fetchMany(missingIds, this._context);
|
|
146
|
+
if (fetchResult.success && fetchResult.data) {
|
|
147
|
+
for (const product of fetchResult.data) {
|
|
148
|
+
// Products from collection-types have model_id
|
|
149
|
+
const productId = product.model_id;
|
|
150
|
+
if (productId) {
|
|
151
|
+
this._setCache(productId, product);
|
|
152
|
+
result.set(productId, product);
|
|
153
|
+
this._emitFetched(productId);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
this._clog.error("getByIds failed", { missingIds, error: e });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Prefetch products into cache.
|
|
166
|
+
* Useful for preloading product data before rendering.
|
|
167
|
+
*
|
|
168
|
+
* @param productIds - Array of product IDs to prefetch
|
|
169
|
+
*/
|
|
170
|
+
async prefetch(productIds) {
|
|
171
|
+
const missingIds = productIds.filter((id) => !this.isCached(id));
|
|
172
|
+
if (missingIds.length === 0)
|
|
173
|
+
return;
|
|
174
|
+
this._clog.debug("prefetch", { count: missingIds.length });
|
|
175
|
+
await this.getByIds(missingIds);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Clear the product cache entirely or for a specific product.
|
|
179
|
+
*
|
|
180
|
+
* @param productId - Optional product ID to clear (clears all if not provided)
|
|
181
|
+
*/
|
|
182
|
+
clearCache(productId) {
|
|
183
|
+
if (productId) {
|
|
184
|
+
this._cache.delete(productId);
|
|
185
|
+
this._clog.debug("cache cleared for product", { productId });
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
this._cache.clear();
|
|
189
|
+
this._clog.debug("cache cleared entirely");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Check if a product is in the cache and not expired.
|
|
194
|
+
*
|
|
195
|
+
* @param productId - The product ID to check
|
|
196
|
+
* @returns True if the product is cached and valid
|
|
197
|
+
*/
|
|
198
|
+
isCached(productId) {
|
|
199
|
+
const entry = this._cache.get(productId);
|
|
200
|
+
if (!entry)
|
|
201
|
+
return false;
|
|
202
|
+
return Date.now() < entry.expiresAt;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Get the current cache size.
|
|
206
|
+
*
|
|
207
|
+
* @returns Number of cached products (includes expired entries)
|
|
208
|
+
*/
|
|
209
|
+
getCacheSize() {
|
|
210
|
+
return this._cache.size;
|
|
211
|
+
}
|
|
212
|
+
/** Get product from cache if valid */
|
|
213
|
+
_getFromCache(productId) {
|
|
214
|
+
const entry = this._cache.get(productId);
|
|
215
|
+
if (!entry)
|
|
216
|
+
return null;
|
|
217
|
+
if (Date.now() >= entry.expiresAt) {
|
|
218
|
+
// Expired, remove from cache
|
|
219
|
+
this._cache.delete(productId);
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
return entry.data;
|
|
223
|
+
}
|
|
224
|
+
/** Set product in cache */
|
|
225
|
+
_setCache(productId, data) {
|
|
226
|
+
this._cache.set(productId, {
|
|
227
|
+
data,
|
|
228
|
+
expiresAt: Date.now() + this._cacheTtl,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
/** Emit product:fetched event */
|
|
232
|
+
_emitFetched(productId) {
|
|
233
|
+
const event = {
|
|
234
|
+
type: "product:fetched",
|
|
235
|
+
domain: "product",
|
|
236
|
+
timestamp: Date.now(),
|
|
237
|
+
productId,
|
|
238
|
+
};
|
|
239
|
+
this._pubsub.publish(event.type, event);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module domains/wishlist
|
|
3
|
+
*
|
|
4
|
+
* Wishlist domain manager with optimistic updates and localStorage persistence.
|
|
5
|
+
* Manages wishlist state with automatic server synchronization.
|
|
6
|
+
*/
|
|
7
|
+
import type { UUID } from "@marianmeres/collection-types";
|
|
8
|
+
import type { WishlistAdapter } from "../types/adapter.js";
|
|
9
|
+
import type { EnrichedWishlistItem, WishlistData, WishlistItem } from "../types/state.js";
|
|
10
|
+
import { BaseDomainManager, type BaseDomainOptions } from "./base.js";
|
|
11
|
+
import type { ProductManager } from "./product.js";
|
|
12
|
+
export interface WishlistManagerOptions extends BaseDomainOptions {
|
|
13
|
+
/** Wishlist adapter for server communication */
|
|
14
|
+
adapter?: WishlistAdapter;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Wishlist domain manager with optimistic updates and localStorage persistence.
|
|
18
|
+
*
|
|
19
|
+
* Features:
|
|
20
|
+
* - Automatic localStorage persistence (configurable)
|
|
21
|
+
* - Optimistic updates with automatic rollback on server error
|
|
22
|
+
* - Server synchronization via WishlistAdapter
|
|
23
|
+
* - Toggle functionality for easy add/remove
|
|
24
|
+
* - Enriched items with product data support
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const wishlist = new WishlistManager({ adapter: myWishlistAdapter });
|
|
29
|
+
* await wishlist.initialize();
|
|
30
|
+
*
|
|
31
|
+
* await wishlist.toggleItem("prod-1"); // Adds item
|
|
32
|
+
* await wishlist.toggleItem("prod-1"); // Removes item
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare class WishlistManager extends BaseDomainManager<WishlistData, WishlistAdapter> {
|
|
36
|
+
constructor(options?: WishlistManagerOptions);
|
|
37
|
+
/** Initialize wishlist (load from storage, then sync with server) */
|
|
38
|
+
initialize(): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Add a product to the wishlist.
|
|
41
|
+
* No-op if the product is already in the wishlist.
|
|
42
|
+
*
|
|
43
|
+
* @param productId - The product ID to add
|
|
44
|
+
* @emits wishlist:item:added - On successful addition
|
|
45
|
+
*/
|
|
46
|
+
addItem(productId: UUID): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Remove a product from the wishlist.
|
|
49
|
+
*
|
|
50
|
+
* @param productId - The product ID to remove
|
|
51
|
+
* @emits wishlist:item:removed - On successful removal
|
|
52
|
+
*/
|
|
53
|
+
removeItem(productId: UUID): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Toggle a product in the wishlist.
|
|
56
|
+
* Adds the product if not present, removes it if present.
|
|
57
|
+
*
|
|
58
|
+
* @param productId - The product ID to toggle
|
|
59
|
+
* @returns True if the item was added, false if removed
|
|
60
|
+
*/
|
|
61
|
+
toggleItem(productId: UUID): Promise<boolean>;
|
|
62
|
+
/**
|
|
63
|
+
* Clear all items from the wishlist.
|
|
64
|
+
*
|
|
65
|
+
* @emits wishlist:cleared - On successful clear
|
|
66
|
+
*/
|
|
67
|
+
clear(): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Get the total number of items in the wishlist.
|
|
70
|
+
*
|
|
71
|
+
* @returns Total item count
|
|
72
|
+
*/
|
|
73
|
+
getItemCount(): number;
|
|
74
|
+
/**
|
|
75
|
+
* Check if a product is in the wishlist.
|
|
76
|
+
*
|
|
77
|
+
* @param productId - The product ID to check
|
|
78
|
+
* @returns True if the product is in the wishlist
|
|
79
|
+
*/
|
|
80
|
+
hasProduct(productId: UUID): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Get a wishlist item by product ID.
|
|
83
|
+
*
|
|
84
|
+
* @param productId - The product ID to find
|
|
85
|
+
* @returns The wishlist item or undefined if not found
|
|
86
|
+
*/
|
|
87
|
+
getItem(productId: UUID): WishlistItem | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* Get all product IDs in the wishlist.
|
|
90
|
+
*
|
|
91
|
+
* @returns Array of product IDs
|
|
92
|
+
*/
|
|
93
|
+
getProductIds(): UUID[];
|
|
94
|
+
/**
|
|
95
|
+
* Get wishlist items enriched with product data.
|
|
96
|
+
*
|
|
97
|
+
* @param productManager - The ProductManager to fetch product data from
|
|
98
|
+
* @returns Array of enriched wishlist items with product data
|
|
99
|
+
*/
|
|
100
|
+
getEnrichedItems(productManager: ProductManager): Promise<EnrichedWishlistItem[]>;
|
|
101
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module domains/wishlist
|
|
3
|
+
*
|
|
4
|
+
* Wishlist domain manager with optimistic updates and localStorage persistence.
|
|
5
|
+
* Manages wishlist state with automatic server synchronization.
|
|
6
|
+
*/
|
|
7
|
+
import { BaseDomainManager } from "./base.js";
|
|
8
|
+
/**
|
|
9
|
+
* Wishlist domain manager with optimistic updates and localStorage persistence.
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - Automatic localStorage persistence (configurable)
|
|
13
|
+
* - Optimistic updates with automatic rollback on server error
|
|
14
|
+
* - Server synchronization via WishlistAdapter
|
|
15
|
+
* - Toggle functionality for easy add/remove
|
|
16
|
+
* - Enriched items with product data support
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const wishlist = new WishlistManager({ adapter: myWishlistAdapter });
|
|
21
|
+
* await wishlist.initialize();
|
|
22
|
+
*
|
|
23
|
+
* await wishlist.toggleItem("prod-1"); // Adds item
|
|
24
|
+
* await wishlist.toggleItem("prod-1"); // Removes item
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export class WishlistManager extends BaseDomainManager {
|
|
28
|
+
constructor(options = {}) {
|
|
29
|
+
super("wishlist", {
|
|
30
|
+
...options,
|
|
31
|
+
// Wishlist defaults to localStorage persistence
|
|
32
|
+
storageKey: options.storageKey ?? "ecsuite:wishlist",
|
|
33
|
+
storageType: options.storageType ?? "local",
|
|
34
|
+
});
|
|
35
|
+
if (options.adapter) {
|
|
36
|
+
this._adapter = options.adapter;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Initialize wishlist (load from storage, then sync with server) */
|
|
40
|
+
async initialize() {
|
|
41
|
+
this._clog.debug("initialize start");
|
|
42
|
+
const current = this._store.get();
|
|
43
|
+
// If we have persisted data, use it immediately
|
|
44
|
+
if (current.data) {
|
|
45
|
+
this._setState("ready");
|
|
46
|
+
}
|
|
47
|
+
// Then sync with server if adapter is available
|
|
48
|
+
if (this._adapter) {
|
|
49
|
+
this._setState("syncing");
|
|
50
|
+
try {
|
|
51
|
+
const result = await this._adapter.fetch(this._context);
|
|
52
|
+
if (result.success && result.data) {
|
|
53
|
+
this._setData(result.data);
|
|
54
|
+
this._markSynced();
|
|
55
|
+
}
|
|
56
|
+
else if (result.error) {
|
|
57
|
+
this._setError({
|
|
58
|
+
code: result.error.code,
|
|
59
|
+
message: result.error.message,
|
|
60
|
+
operation: "initialize",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
this._setError({
|
|
66
|
+
code: "FETCH_FAILED",
|
|
67
|
+
message: e instanceof Error ? e.message : "Failed to fetch wishlist",
|
|
68
|
+
originalError: e,
|
|
69
|
+
operation: "initialize",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
// No adapter, just use local storage or create empty wishlist
|
|
75
|
+
if (!current.data) {
|
|
76
|
+
this._setData({ items: [] });
|
|
77
|
+
}
|
|
78
|
+
this._setState("ready");
|
|
79
|
+
}
|
|
80
|
+
this._clog.debug("initialize complete", { itemCount: this.getItemCount() });
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Add a product to the wishlist.
|
|
84
|
+
* No-op if the product is already in the wishlist.
|
|
85
|
+
*
|
|
86
|
+
* @param productId - The product ID to add
|
|
87
|
+
* @emits wishlist:item:added - On successful addition
|
|
88
|
+
*/
|
|
89
|
+
async addItem(productId) {
|
|
90
|
+
this._clog.debug("addItem", { productId });
|
|
91
|
+
// Check if already in wishlist
|
|
92
|
+
const current = this._store.get().data ?? { items: [] };
|
|
93
|
+
if (current.items.some((i) => i.product_id === productId)) {
|
|
94
|
+
return; // Already in wishlist, no-op
|
|
95
|
+
}
|
|
96
|
+
const newItem = {
|
|
97
|
+
product_id: productId,
|
|
98
|
+
added_at: Date.now(),
|
|
99
|
+
};
|
|
100
|
+
await this._withOptimisticUpdate("addItem", () => {
|
|
101
|
+
const items = [...current.items, newItem];
|
|
102
|
+
this._setData({ items }, false);
|
|
103
|
+
}, async () => {
|
|
104
|
+
if (this._adapter) {
|
|
105
|
+
const result = await this._adapter.addItem(productId, this._context);
|
|
106
|
+
if (!result.success) {
|
|
107
|
+
throw new Error(result.error?.message ?? "Failed to add item");
|
|
108
|
+
}
|
|
109
|
+
return result.data;
|
|
110
|
+
}
|
|
111
|
+
return this._store.get().data;
|
|
112
|
+
}, (serverData) => {
|
|
113
|
+
if (serverData) {
|
|
114
|
+
this._setData(serverData);
|
|
115
|
+
}
|
|
116
|
+
this._emit({
|
|
117
|
+
type: "wishlist:item:added",
|
|
118
|
+
domain: "wishlist",
|
|
119
|
+
timestamp: Date.now(),
|
|
120
|
+
productId,
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Remove a product from the wishlist.
|
|
126
|
+
*
|
|
127
|
+
* @param productId - The product ID to remove
|
|
128
|
+
* @emits wishlist:item:removed - On successful removal
|
|
129
|
+
*/
|
|
130
|
+
async removeItem(productId) {
|
|
131
|
+
this._clog.debug("removeItem", { productId });
|
|
132
|
+
await this._withOptimisticUpdate("removeItem", () => {
|
|
133
|
+
const current = this._store.get().data ?? { items: [] };
|
|
134
|
+
const newItems = current.items.filter((i) => i.product_id !== productId);
|
|
135
|
+
this._setData({ items: newItems }, false);
|
|
136
|
+
}, async () => {
|
|
137
|
+
if (this._adapter) {
|
|
138
|
+
const result = await this._adapter.removeItem(productId, this._context);
|
|
139
|
+
if (!result.success) {
|
|
140
|
+
throw new Error(result.error?.message ?? "Failed to remove item");
|
|
141
|
+
}
|
|
142
|
+
return result.data;
|
|
143
|
+
}
|
|
144
|
+
return this._store.get().data;
|
|
145
|
+
}, (serverData) => {
|
|
146
|
+
if (serverData) {
|
|
147
|
+
this._setData(serverData);
|
|
148
|
+
}
|
|
149
|
+
this._emit({
|
|
150
|
+
type: "wishlist:item:removed",
|
|
151
|
+
domain: "wishlist",
|
|
152
|
+
timestamp: Date.now(),
|
|
153
|
+
productId,
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Toggle a product in the wishlist.
|
|
159
|
+
* Adds the product if not present, removes it if present.
|
|
160
|
+
*
|
|
161
|
+
* @param productId - The product ID to toggle
|
|
162
|
+
* @returns True if the item was added, false if removed
|
|
163
|
+
*/
|
|
164
|
+
async toggleItem(productId) {
|
|
165
|
+
this._clog.debug("toggleItem", { productId });
|
|
166
|
+
if (this.hasProduct(productId)) {
|
|
167
|
+
await this.removeItem(productId);
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
await this.addItem(productId);
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Clear all items from the wishlist.
|
|
177
|
+
*
|
|
178
|
+
* @emits wishlist:cleared - On successful clear
|
|
179
|
+
*/
|
|
180
|
+
async clear() {
|
|
181
|
+
this._clog.debug("clear");
|
|
182
|
+
await this._withOptimisticUpdate("clear", () => {
|
|
183
|
+
this._setData({ items: [] }, false);
|
|
184
|
+
}, async () => {
|
|
185
|
+
if (this._adapter) {
|
|
186
|
+
const result = await this._adapter.clear(this._context);
|
|
187
|
+
if (!result.success) {
|
|
188
|
+
throw new Error(result.error?.message ?? "Failed to clear wishlist");
|
|
189
|
+
}
|
|
190
|
+
return result.data;
|
|
191
|
+
}
|
|
192
|
+
return { items: [] };
|
|
193
|
+
}, () => {
|
|
194
|
+
this._emit({
|
|
195
|
+
type: "wishlist:cleared",
|
|
196
|
+
domain: "wishlist",
|
|
197
|
+
timestamp: Date.now(),
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Get the total number of items in the wishlist.
|
|
203
|
+
*
|
|
204
|
+
* @returns Total item count
|
|
205
|
+
*/
|
|
206
|
+
getItemCount() {
|
|
207
|
+
const data = this._store.get().data;
|
|
208
|
+
return data?.items.length ?? 0;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Check if a product is in the wishlist.
|
|
212
|
+
*
|
|
213
|
+
* @param productId - The product ID to check
|
|
214
|
+
* @returns True if the product is in the wishlist
|
|
215
|
+
*/
|
|
216
|
+
hasProduct(productId) {
|
|
217
|
+
const data = this._store.get().data;
|
|
218
|
+
return data?.items.some((i) => i.product_id === productId) ?? false;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Get a wishlist item by product ID.
|
|
222
|
+
*
|
|
223
|
+
* @param productId - The product ID to find
|
|
224
|
+
* @returns The wishlist item or undefined if not found
|
|
225
|
+
*/
|
|
226
|
+
getItem(productId) {
|
|
227
|
+
const data = this._store.get().data;
|
|
228
|
+
return data?.items.find((i) => i.product_id === productId);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Get all product IDs in the wishlist.
|
|
232
|
+
*
|
|
233
|
+
* @returns Array of product IDs
|
|
234
|
+
*/
|
|
235
|
+
getProductIds() {
|
|
236
|
+
const data = this._store.get().data;
|
|
237
|
+
return data?.items.map((i) => i.product_id) ?? [];
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Get wishlist items enriched with product data.
|
|
241
|
+
*
|
|
242
|
+
* @param productManager - The ProductManager to fetch product data from
|
|
243
|
+
* @returns Array of enriched wishlist items with product data
|
|
244
|
+
*/
|
|
245
|
+
async getEnrichedItems(productManager) {
|
|
246
|
+
const data = this._store.get().data;
|
|
247
|
+
if (!data?.items.length)
|
|
248
|
+
return [];
|
|
249
|
+
const productIds = data.items.map((i) => i.product_id);
|
|
250
|
+
const products = await productManager.getByIds(productIds);
|
|
251
|
+
return data.items.map((item) => ({
|
|
252
|
+
...item,
|
|
253
|
+
product: products.get(item.product_id) ?? null,
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
}
|
package/dist/mod.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @marianmeres/ecsuite
|
|
3
|
+
*
|
|
4
|
+
* E-commerce frontend UI helper library with optimistic updates,
|
|
5
|
+
* Svelte-compatible stores, and adapter-based server sync.
|
|
6
|
+
*
|
|
7
|
+
* @example Basic usage
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { createECSuite } from "@marianmeres/ecsuite";
|
|
10
|
+
*
|
|
11
|
+
* const suite = createECSuite({
|
|
12
|
+
* context: { customerId: "user-123" },
|
|
13
|
+
* adapters: { cart: myCartAdapter },
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* // Subscribe to cart state (Svelte-compatible)
|
|
17
|
+
* suite.cart.subscribe((state) => {
|
|
18
|
+
* console.log(state.state, state.data);
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* // Add item with optimistic update
|
|
22
|
+
* await suite.cart.addItem({ product_id: "prod-1", quantity: 2 });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export { ECSuite, createECSuite, type ECSuiteConfig } from "./suite.js";
|
|
26
|
+
export * from "./types/mod.js";
|
|
27
|
+
export * from "./domains/mod.js";
|
|
28
|
+
export * from "./adapters/mod.js";
|
package/dist/mod.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @marianmeres/ecsuite
|
|
3
|
+
*
|
|
4
|
+
* E-commerce frontend UI helper library with optimistic updates,
|
|
5
|
+
* Svelte-compatible stores, and adapter-based server sync.
|
|
6
|
+
*
|
|
7
|
+
* @example Basic usage
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { createECSuite } from "@marianmeres/ecsuite";
|
|
10
|
+
*
|
|
11
|
+
* const suite = createECSuite({
|
|
12
|
+
* context: { customerId: "user-123" },
|
|
13
|
+
* adapters: { cart: myCartAdapter },
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* // Subscribe to cart state (Svelte-compatible)
|
|
17
|
+
* suite.cart.subscribe((state) => {
|
|
18
|
+
* console.log(state.state, state.data);
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* // Add item with optimistic update
|
|
22
|
+
* await suite.cart.addItem({ product_id: "prod-1", quantity: 2 });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
// Main exports
|
|
26
|
+
export { ECSuite, createECSuite } from "./suite.js";
|
|
27
|
+
// Types
|
|
28
|
+
export * from "./types/mod.js";
|
|
29
|
+
// Domain managers (for advanced usage)
|
|
30
|
+
export * from "./domains/mod.js";
|
|
31
|
+
// Adapters (including mock adapters for testing)
|
|
32
|
+
export * from "./adapters/mod.js";
|