@nexushub/client 0.0.4 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-2JRYABGG.cjs +117 -0
- package/dist/chunk-BPQMAYT3.js +110 -0
- package/dist/{tools/cli.js → cli.cjs} +14 -8
- package/dist/content/local-cache-client.cjs +56 -0
- package/dist/content/local-cache-client.d.cts +48 -0
- package/dist/content/local-cache-client.d.ts +48 -0
- package/dist/content/local-cache-client.js +54 -0
- package/dist/content/local-cache-server.cjs +10 -0
- package/dist/content/local-cache-server.d.cts +38 -0
- package/dist/content/local-cache-server.d.ts +38 -0
- package/dist/content/local-cache-server.js +1 -0
- package/dist/index.cjs +2008 -0
- package/dist/{src/index.d.mts → index.d.cts} +32 -28
- package/dist/{src/index.d.ts → index.d.ts} +32 -28
- package/dist/index.js +1984 -0
- package/package.json +18 -5
- package/dist/src/index.js +0 -7259
- package/dist/src/index.mjs +0 -7238
- package/dist/tools/cli.d.mts +0 -1
- package/dist/tools/cli.d.ts +0 -1
- package/dist/tools/cli.mjs +0 -639
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2008 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunk2JRYABGG_cjs = require('./chunk-2JRYABGG.cjs');
|
|
4
|
+
var webVitals = require('web-vitals');
|
|
5
|
+
var react = require('react');
|
|
6
|
+
var navigation = require('next/navigation');
|
|
7
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
8
|
+
|
|
9
|
+
// src/config.ts
|
|
10
|
+
var DEFAULT_API_URL = "http://localhost:3001";
|
|
11
|
+
var DEFAULT_ANALYTICS_URL = "http://localhost:3002";
|
|
12
|
+
var LOCAL_NEST_URL = "http://localhost:3001";
|
|
13
|
+
var LOCAL_RUST_URL = "http://localhost:3002";
|
|
14
|
+
var hasEnv = () => {
|
|
15
|
+
return typeof process !== "undefined" && typeof process.env !== "undefined" && Object.keys(process.env).length > 0;
|
|
16
|
+
};
|
|
17
|
+
var getEnvConfig = () => {
|
|
18
|
+
const env = hasEnv() ? process.env : {};
|
|
19
|
+
const isDev = env.NODE_ENV === "development" || env.NEXT_PUBLIC_NODE_ENV === "development";
|
|
20
|
+
const projectId = env.NEXT_PUBLIC_NEXUS_ID ?? env.NEXUS_PROJECT_ID;
|
|
21
|
+
const apiKey = env.NEXT_PUBLIC_NEXUS_KEY ?? env.NEXUS_API_KEY;
|
|
22
|
+
const apiUrl = env.NEXT_PUBLIC_NEXUS_API_URL ?? env.NEXUS_API_URL ?? (isDev ? LOCAL_NEST_URL : DEFAULT_API_URL);
|
|
23
|
+
const analyticsUrl = env.NEXT_PUBLIC_NEXUS_ANALYTICS_URL ?? (apiUrl.includes("localhost") ? LOCAL_RUST_URL : DEFAULT_ANALYTICS_URL);
|
|
24
|
+
return {
|
|
25
|
+
projectId,
|
|
26
|
+
apiKey,
|
|
27
|
+
apiUrl,
|
|
28
|
+
analyticsUrl
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
var mergeConfigs = (base, override) => {
|
|
32
|
+
const mergedApiUrl = override.apiUrl || base.apiUrl || DEFAULT_API_URL;
|
|
33
|
+
const mergedAnalyticsUrl = override.analyticsUrl || base.analyticsUrl || (mergedApiUrl.includes("localhost") ? LOCAL_RUST_URL : DEFAULT_ANALYTICS_URL);
|
|
34
|
+
return {
|
|
35
|
+
...base,
|
|
36
|
+
...override,
|
|
37
|
+
apiUrl: mergedApiUrl,
|
|
38
|
+
analyticsUrl: mergedAnalyticsUrl,
|
|
39
|
+
projectId: override.projectId || base.projectId
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
var getFullConfig = (partialConfig) => {
|
|
43
|
+
const envConfig = getEnvConfig();
|
|
44
|
+
return mergeConfigs(envConfig, partialConfig || {});
|
|
45
|
+
};
|
|
46
|
+
var validateConfig = (config) => {
|
|
47
|
+
const errors = [];
|
|
48
|
+
if (!config.projectId) errors.push("Missing projectId");
|
|
49
|
+
if (!config.apiUrl) errors.push("Missing apiUrl");
|
|
50
|
+
if (config.apiUrl && !config.apiUrl.startsWith("http")) {
|
|
51
|
+
errors.push("apiUrl must be a valid URL (http:// or https://)");
|
|
52
|
+
}
|
|
53
|
+
return errors;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// src/content/cache-implementations.ts
|
|
57
|
+
var CacheTags = {
|
|
58
|
+
project: (projectId) => `nexus_project_${projectId}`,
|
|
59
|
+
content: (slug) => `content_${slug}`,
|
|
60
|
+
collection: (collectionId) => `collection_${collectionId}`,
|
|
61
|
+
global: "nexus_global_config",
|
|
62
|
+
user: (userId) => `user_${userId}`,
|
|
63
|
+
media: (mediaId) => `media_${mediaId}`
|
|
64
|
+
};
|
|
65
|
+
var MemoryCache = class {
|
|
66
|
+
constructor(options = {}) {
|
|
67
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
68
|
+
this.tagIndex = /* @__PURE__ */ new Map();
|
|
69
|
+
this.stats = {
|
|
70
|
+
size: 0,
|
|
71
|
+
hits: 0,
|
|
72
|
+
misses: 0,
|
|
73
|
+
hitRate: 0,
|
|
74
|
+
evictions: 0
|
|
75
|
+
};
|
|
76
|
+
this.maxSize = options.maxSize || 1e3;
|
|
77
|
+
this.defaultTTL = options.ttl || 5 * 60 * 1e3;
|
|
78
|
+
}
|
|
79
|
+
set(key, data, options = {}) {
|
|
80
|
+
if (this.cache.size >= this.maxSize) {
|
|
81
|
+
this.evictLRU();
|
|
82
|
+
}
|
|
83
|
+
const metadata = {
|
|
84
|
+
timestamp: Date.now(),
|
|
85
|
+
expiresAt: Date.now() + (options.ttl || this.defaultTTL),
|
|
86
|
+
tags: options.tags || [],
|
|
87
|
+
size: this.calculateSize(data)
|
|
88
|
+
};
|
|
89
|
+
this.cache.set(key, { data, metadata });
|
|
90
|
+
this.stats.size = this.cache.size;
|
|
91
|
+
metadata.tags.forEach((tag) => {
|
|
92
|
+
if (!this.tagIndex.has(tag)) {
|
|
93
|
+
this.tagIndex.set(tag, /* @__PURE__ */ new Set());
|
|
94
|
+
}
|
|
95
|
+
this.tagIndex.get(tag).add(key);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
get(key) {
|
|
99
|
+
const entry = this.cache.get(key);
|
|
100
|
+
if (!entry) {
|
|
101
|
+
this.stats.misses++;
|
|
102
|
+
this.updateHitRate();
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
if (Date.now() > entry.metadata.expiresAt) {
|
|
106
|
+
this.delete(key);
|
|
107
|
+
this.stats.misses++;
|
|
108
|
+
this.updateHitRate();
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
entry.metadata.timestamp = Date.now();
|
|
112
|
+
this.stats.hits++;
|
|
113
|
+
this.updateHitRate();
|
|
114
|
+
return entry;
|
|
115
|
+
}
|
|
116
|
+
delete(key) {
|
|
117
|
+
const entry = this.cache.get(key);
|
|
118
|
+
if (!entry) return false;
|
|
119
|
+
entry.metadata.tags.forEach((tag) => {
|
|
120
|
+
const keys = this.tagIndex.get(tag);
|
|
121
|
+
if (keys) {
|
|
122
|
+
keys.delete(key);
|
|
123
|
+
if (keys.size === 0) {
|
|
124
|
+
this.tagIndex.delete(tag);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
this.cache.delete(key);
|
|
129
|
+
this.stats.size = this.cache.size;
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
invalidateByTags(tags) {
|
|
133
|
+
const keysToDelete = /* @__PURE__ */ new Set();
|
|
134
|
+
tags.forEach((tag) => {
|
|
135
|
+
const keys = this.tagIndex.get(tag);
|
|
136
|
+
if (keys) {
|
|
137
|
+
keys.forEach((key) => keysToDelete.add(key));
|
|
138
|
+
this.tagIndex.delete(tag);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
keysToDelete.forEach((key) => this.delete(key));
|
|
142
|
+
if (process.env.NODE_ENV === "development") {
|
|
143
|
+
console.log(
|
|
144
|
+
`[MemoryCache] Invalidated ${keysToDelete.size} entries by tags: ${tags.join(", ")}`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
clear() {
|
|
149
|
+
this.cache.clear();
|
|
150
|
+
this.tagIndex.clear();
|
|
151
|
+
this.stats = {
|
|
152
|
+
size: 0,
|
|
153
|
+
hits: 0,
|
|
154
|
+
misses: 0,
|
|
155
|
+
hitRate: 0,
|
|
156
|
+
evictions: 0
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
getStats() {
|
|
160
|
+
return { ...this.stats };
|
|
161
|
+
}
|
|
162
|
+
evictLRU() {
|
|
163
|
+
let oldestKey = null;
|
|
164
|
+
let oldestTime = Infinity;
|
|
165
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
166
|
+
if (entry.metadata.timestamp < oldestTime) {
|
|
167
|
+
oldestTime = entry.metadata.timestamp;
|
|
168
|
+
oldestKey = key;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (oldestKey) {
|
|
172
|
+
this.delete(oldestKey);
|
|
173
|
+
this.stats.evictions++;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
calculateSize(data) {
|
|
177
|
+
try {
|
|
178
|
+
const jsonString = JSON.stringify(data);
|
|
179
|
+
return new Blob([jsonString]).size;
|
|
180
|
+
} catch {
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
updateHitRate() {
|
|
185
|
+
const total = this.stats.hits + this.stats.misses;
|
|
186
|
+
this.stats.hitRate = total > 0 ? this.stats.hits / total : 0;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
var BrowserCache = class {
|
|
190
|
+
constructor(projectId) {
|
|
191
|
+
this.projectId = projectId;
|
|
192
|
+
this.prefix = "nexushub_";
|
|
193
|
+
this.maxSize = 5 * 1024 * 1024;
|
|
194
|
+
// 5MB standard LocalStorage limit
|
|
195
|
+
this.currentSize = 0;
|
|
196
|
+
if (typeof window !== "undefined") {
|
|
197
|
+
this.calculateCurrentSize();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
set(key, data, ttl = 5 * 60 * 1e3) {
|
|
201
|
+
if (typeof window === "undefined") return;
|
|
202
|
+
const storageKey = this.getStorageKey(key);
|
|
203
|
+
const entry = {
|
|
204
|
+
data,
|
|
205
|
+
metadata: {
|
|
206
|
+
timestamp: Date.now(),
|
|
207
|
+
expiresAt: Date.now() + ttl,
|
|
208
|
+
size: this.calculateStorageSize(data)
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
const serialized = JSON.stringify(entry);
|
|
212
|
+
const newSize = new Blob([serialized]).size;
|
|
213
|
+
if (this.currentSize + newSize > this.maxSize) {
|
|
214
|
+
this.evictOldest();
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
localStorage.setItem(storageKey, serialized);
|
|
218
|
+
this.currentSize += newSize;
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.warn("[BrowserCache] Failed to save to localStorage:", error);
|
|
221
|
+
this.evictOldest();
|
|
222
|
+
try {
|
|
223
|
+
localStorage.setItem(storageKey, serialized);
|
|
224
|
+
} catch (e) {
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
get(key) {
|
|
229
|
+
if (typeof window === "undefined") return null;
|
|
230
|
+
const storageKey = this.getStorageKey(key);
|
|
231
|
+
const item = localStorage.getItem(storageKey);
|
|
232
|
+
if (!item) return null;
|
|
233
|
+
try {
|
|
234
|
+
const entry = JSON.parse(item);
|
|
235
|
+
if (Date.now() > entry.metadata.expiresAt) {
|
|
236
|
+
this.delete(key);
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
return entry.data;
|
|
240
|
+
} catch {
|
|
241
|
+
this.delete(key);
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
delete(key) {
|
|
246
|
+
if (typeof window === "undefined") return;
|
|
247
|
+
const storageKey = this.getStorageKey(key);
|
|
248
|
+
const item = localStorage.getItem(storageKey);
|
|
249
|
+
if (item) {
|
|
250
|
+
this.currentSize -= new Blob([item]).size;
|
|
251
|
+
}
|
|
252
|
+
localStorage.removeItem(storageKey);
|
|
253
|
+
}
|
|
254
|
+
clear() {
|
|
255
|
+
if (typeof window === "undefined") return;
|
|
256
|
+
const keysToRemove = [];
|
|
257
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
258
|
+
const key = localStorage.key(i);
|
|
259
|
+
if (key?.startsWith(this.prefix)) {
|
|
260
|
+
keysToRemove.push(key);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
keysToRemove.forEach((key) => localStorage.removeItem(key));
|
|
264
|
+
this.currentSize = 0;
|
|
265
|
+
}
|
|
266
|
+
getStorageKey(key) {
|
|
267
|
+
return `${this.prefix}${this.projectId}_${key}`;
|
|
268
|
+
}
|
|
269
|
+
calculateCurrentSize() {
|
|
270
|
+
this.currentSize = 0;
|
|
271
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
272
|
+
const key = localStorage.key(i);
|
|
273
|
+
if (key?.startsWith(this.prefix)) {
|
|
274
|
+
const item = localStorage.getItem(key);
|
|
275
|
+
if (item) {
|
|
276
|
+
this.currentSize += new Blob([item]).size;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
calculateStorageSize(data) {
|
|
282
|
+
return new Blob([JSON.stringify(data)]).size;
|
|
283
|
+
}
|
|
284
|
+
evictOldest() {
|
|
285
|
+
let oldestKey = null;
|
|
286
|
+
let oldestTime = Infinity;
|
|
287
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
288
|
+
const key = localStorage.key(i);
|
|
289
|
+
if (key?.startsWith(this.prefix)) {
|
|
290
|
+
const item = localStorage.getItem(key);
|
|
291
|
+
if (item) {
|
|
292
|
+
try {
|
|
293
|
+
const entry = JSON.parse(item);
|
|
294
|
+
if (entry.metadata.timestamp < oldestTime) {
|
|
295
|
+
oldestTime = entry.metadata.timestamp;
|
|
296
|
+
oldestKey = key;
|
|
297
|
+
}
|
|
298
|
+
} catch {
|
|
299
|
+
localStorage.removeItem(key);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (oldestKey) {
|
|
305
|
+
const item = localStorage.getItem(oldestKey);
|
|
306
|
+
if (item) {
|
|
307
|
+
this.currentSize -= new Blob([item]).size;
|
|
308
|
+
}
|
|
309
|
+
localStorage.removeItem(oldestKey);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// src/content/strategies.ts
|
|
315
|
+
var RateLimiter = class {
|
|
316
|
+
constructor(config) {
|
|
317
|
+
this.requests = [];
|
|
318
|
+
this.config = config;
|
|
319
|
+
}
|
|
320
|
+
async checkLimit() {
|
|
321
|
+
const now = Date.now();
|
|
322
|
+
const windowStart = now - this.config.timeWindow;
|
|
323
|
+
this.requests = this.requests.filter((time) => time > windowStart);
|
|
324
|
+
if (this.requests.length >= this.config.maxRequests) {
|
|
325
|
+
this.requests[0];
|
|
326
|
+
const waitTime = windowStart + this.config.timeWindow - now;
|
|
327
|
+
if (waitTime > 0) {
|
|
328
|
+
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
329
|
+
return this.checkLimit();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
this.requests.push(now);
|
|
333
|
+
}
|
|
334
|
+
getStats() {
|
|
335
|
+
const now = Date.now();
|
|
336
|
+
const windowStart = now - this.config.timeWindow;
|
|
337
|
+
const currentRequests = this.requests.filter((time) => time > windowStart).length;
|
|
338
|
+
return {
|
|
339
|
+
currentRequests,
|
|
340
|
+
limit: this.config.maxRequests
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
var ExponentialBackoff = class {
|
|
345
|
+
constructor(config) {
|
|
346
|
+
this.config = {
|
|
347
|
+
jitter: true,
|
|
348
|
+
...config
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
async execute(fn, onRetry) {
|
|
352
|
+
let lastError;
|
|
353
|
+
for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
|
|
354
|
+
try {
|
|
355
|
+
return await fn();
|
|
356
|
+
} catch (error) {
|
|
357
|
+
lastError = error;
|
|
358
|
+
if (this.isClientError(error) && !this.isRateLimitError(error)) {
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
if (attempt === this.config.maxRetries) {
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
const delay = this.calculateDelay(attempt);
|
|
365
|
+
if (onRetry) {
|
|
366
|
+
onRetry(attempt + 1, delay, error);
|
|
367
|
+
}
|
|
368
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
throw lastError;
|
|
372
|
+
}
|
|
373
|
+
calculateDelay(attempt) {
|
|
374
|
+
let delay = this.config.baseDelay * Math.pow(2, attempt);
|
|
375
|
+
delay = Math.min(delay, this.config.maxDelay);
|
|
376
|
+
if (this.config.jitter) {
|
|
377
|
+
delay = delay * (0.5 + Math.random());
|
|
378
|
+
}
|
|
379
|
+
return delay;
|
|
380
|
+
}
|
|
381
|
+
isClientError(error) {
|
|
382
|
+
return error?.status >= 400 && error?.status < 500;
|
|
383
|
+
}
|
|
384
|
+
isRateLimitError(error) {
|
|
385
|
+
return error?.status === 429;
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
var CircuitBreaker = class {
|
|
389
|
+
constructor() {
|
|
390
|
+
this.state = 0 /* CLOSED */;
|
|
391
|
+
this.failures = 0;
|
|
392
|
+
this.lastFailureTime = 0;
|
|
393
|
+
this.failureThreshold = 5;
|
|
394
|
+
this.resetTimeout = 3e4;
|
|
395
|
+
}
|
|
396
|
+
// 30 seconds
|
|
397
|
+
isOpen() {
|
|
398
|
+
if (this.state === 1 /* OPEN */) {
|
|
399
|
+
const now = Date.now();
|
|
400
|
+
if (now - this.lastFailureTime > this.resetTimeout) {
|
|
401
|
+
this.state = 2 /* HALF_OPEN */;
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
recordSuccess() {
|
|
409
|
+
this.failures = 0;
|
|
410
|
+
this.state = 0 /* CLOSED */;
|
|
411
|
+
}
|
|
412
|
+
recordFailure() {
|
|
413
|
+
this.failures++;
|
|
414
|
+
this.lastFailureTime = Date.now();
|
|
415
|
+
if (this.failures >= this.failureThreshold) {
|
|
416
|
+
this.state = 1 /* OPEN */;
|
|
417
|
+
if (process.env.NODE_ENV === "development") {
|
|
418
|
+
console.warn(`[NexusHub] \u{1F50C} Circuit Breaker OPEN. Pausing network requests.`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
var RequestBatcher = class {
|
|
424
|
+
constructor(batchWindow = 10, maxBatchSize = 20) {
|
|
425
|
+
this.batchWindow = batchWindow;
|
|
426
|
+
this.maxBatchSize = maxBatchSize;
|
|
427
|
+
this.batch = [];
|
|
428
|
+
this.processing = false;
|
|
429
|
+
}
|
|
430
|
+
async schedule(key, request) {
|
|
431
|
+
return new Promise((resolve, reject) => {
|
|
432
|
+
this.batch.push({ key, resolve, reject });
|
|
433
|
+
if (this.batch.length >= this.maxBatchSize) {
|
|
434
|
+
this.processBatch(request);
|
|
435
|
+
} else if (!this.batchTimeout) {
|
|
436
|
+
this.batchTimeout = setTimeout(
|
|
437
|
+
() => this.processBatch(request),
|
|
438
|
+
this.batchWindow
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
async processBatch(request) {
|
|
444
|
+
if (this.processing || this.batch.length === 0) return;
|
|
445
|
+
this.processing = true;
|
|
446
|
+
if (this.batchTimeout) {
|
|
447
|
+
clearTimeout(this.batchTimeout);
|
|
448
|
+
this.batchTimeout = void 0;
|
|
449
|
+
}
|
|
450
|
+
const currentBatch = [...this.batch];
|
|
451
|
+
this.batch = [];
|
|
452
|
+
try {
|
|
453
|
+
const result = await request();
|
|
454
|
+
currentBatch.forEach((item) => {
|
|
455
|
+
item.resolve(result);
|
|
456
|
+
});
|
|
457
|
+
} catch (error) {
|
|
458
|
+
currentBatch.forEach((item) => {
|
|
459
|
+
item.reject(error);
|
|
460
|
+
});
|
|
461
|
+
} finally {
|
|
462
|
+
this.processing = false;
|
|
463
|
+
if (this.batch.length > 0) {
|
|
464
|
+
setTimeout(() => this.processBatch(request), 0);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
// src/content/utils.ts
|
|
471
|
+
function validateSlug(slug) {
|
|
472
|
+
if (!slug || typeof slug !== "string") {
|
|
473
|
+
throw new Error("Slug must be a non-empty string");
|
|
474
|
+
}
|
|
475
|
+
if (!/^[a-z0-9-_]+$/.test(slug)) {
|
|
476
|
+
throw new Error("Slug can only contain lowercase letters, numbers, hyphens, and underscores");
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
function normalizeQuery(query) {
|
|
480
|
+
const normalized = { ...query };
|
|
481
|
+
normalized.page = Math.max(1, normalized.page || 1);
|
|
482
|
+
normalized.limit = Math.min(100, Math.max(1, normalized.limit || 10));
|
|
483
|
+
normalized.order = normalized.order || "desc";
|
|
484
|
+
if (normalized.page < 1) {
|
|
485
|
+
throw new Error("Page must be greater than 0");
|
|
486
|
+
}
|
|
487
|
+
if (normalized.limit < 1 || normalized.limit > 100) {
|
|
488
|
+
throw new Error("Limit must be between 1 and 100");
|
|
489
|
+
}
|
|
490
|
+
return normalized;
|
|
491
|
+
}
|
|
492
|
+
function buildQueryString(query) {
|
|
493
|
+
const params = new URLSearchParams();
|
|
494
|
+
if (query.page) params.append("page", query.page.toString());
|
|
495
|
+
if (query.limit) params.append("limit", query.limit.toString());
|
|
496
|
+
if (query.sort) params.append("sort", query.sort);
|
|
497
|
+
if (query.order) params.append("order", query.order);
|
|
498
|
+
if (query.search) params.append("search", query.search);
|
|
499
|
+
if (query.include?.length) {
|
|
500
|
+
params.append("include", query.include.join(","));
|
|
501
|
+
}
|
|
502
|
+
if (query.fields?.length) {
|
|
503
|
+
params.append("fields", query.fields.join(","));
|
|
504
|
+
}
|
|
505
|
+
if (query.filter) {
|
|
506
|
+
params.append("filter", JSON.stringify(query.filter));
|
|
507
|
+
}
|
|
508
|
+
return params.toString();
|
|
509
|
+
}
|
|
510
|
+
function measurePerformance(name, fn) {
|
|
511
|
+
const start = performance.now();
|
|
512
|
+
const result = fn();
|
|
513
|
+
const end = performance.now();
|
|
514
|
+
if (process.env.NODE_ENV === "development") {
|
|
515
|
+
console.log(`\u23F1\uFE0F ${name}: ${(end - start).toFixed(2)}ms`);
|
|
516
|
+
}
|
|
517
|
+
return { result, duration: end - start };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// src/content/index.ts
|
|
521
|
+
var ContentEngine = class {
|
|
522
|
+
constructor(config) {
|
|
523
|
+
this.config = config;
|
|
524
|
+
this.isServer = typeof window === "undefined";
|
|
525
|
+
this.localCache = new chunk2JRYABGG_cjs.LocalCache();
|
|
526
|
+
this.memoryCache = new MemoryCache({
|
|
527
|
+
maxSize: 500,
|
|
528
|
+
ttl: 5 * 60 * 1e3
|
|
529
|
+
// 5 minutes
|
|
530
|
+
});
|
|
531
|
+
if (!this.isServer) {
|
|
532
|
+
this.browserCache = new BrowserCache(config.projectId);
|
|
533
|
+
}
|
|
534
|
+
this.rateLimiter = new RateLimiter({
|
|
535
|
+
maxRequests: config.debug ? 100 : 50,
|
|
536
|
+
timeWindow: 6e4
|
|
537
|
+
});
|
|
538
|
+
this.backoff = new ExponentialBackoff({
|
|
539
|
+
maxRetries: config.retries || 3,
|
|
540
|
+
baseDelay: 100,
|
|
541
|
+
maxDelay: 5e3,
|
|
542
|
+
jitter: true
|
|
543
|
+
});
|
|
544
|
+
this.requestBatcher = new RequestBatcher(20, 50);
|
|
545
|
+
this.circuitBreaker = new CircuitBreaker();
|
|
546
|
+
this.defaultRevalidate = config.revalidateTime || 60;
|
|
547
|
+
this.cacheStrategy = config.cacheStrategy || "memory";
|
|
548
|
+
if (!this.isServer) {
|
|
549
|
+
window.addEventListener("beforeunload", this.cleanup.bind(this));
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Fetch a Single Page with full strategy pipeline
|
|
554
|
+
*/
|
|
555
|
+
async getPage(slug, options = {}) {
|
|
556
|
+
const { result, duration } = measurePerformance(
|
|
557
|
+
`getPage("${slug}")`,
|
|
558
|
+
() => this._getPage(slug, options)
|
|
559
|
+
);
|
|
560
|
+
if (this.config.debug && duration > 100) {
|
|
561
|
+
console.warn(`[NexusHub] \u26A0\uFE0F getPage("${slug}") took ${duration.toFixed(2)}ms`);
|
|
562
|
+
}
|
|
563
|
+
return result;
|
|
564
|
+
}
|
|
565
|
+
async _getPage(slug, options = {}) {
|
|
566
|
+
validateSlug(slug);
|
|
567
|
+
const {
|
|
568
|
+
revalidate = this.defaultRevalidate,
|
|
569
|
+
tags = [],
|
|
570
|
+
forceRefresh = false,
|
|
571
|
+
includeMetadata = false
|
|
572
|
+
} = options;
|
|
573
|
+
const cacheKey = `page:${slug}`;
|
|
574
|
+
const cached = this.checkCaches(cacheKey, forceRefresh, includeMetadata);
|
|
575
|
+
if (cached) {
|
|
576
|
+
if (this.config.debug) console.log(`[NexusHub] \u26A1 Cache hit: ${slug}`);
|
|
577
|
+
return includeMetadata ? cached : cached.data;
|
|
578
|
+
}
|
|
579
|
+
if (this.circuitBreaker.isOpen()) {
|
|
580
|
+
throw new Error(`[NexusHub] Circuit open. API is unavailable.`);
|
|
581
|
+
}
|
|
582
|
+
try {
|
|
583
|
+
await this.rateLimiter.checkLimit();
|
|
584
|
+
const data = await this.backoff.execute(
|
|
585
|
+
async () => {
|
|
586
|
+
return this.fetchPage(slug, cacheKey, tags, revalidate, forceRefresh);
|
|
587
|
+
},
|
|
588
|
+
(attempt, delay, error) => {
|
|
589
|
+
if (this.config.debug) {
|
|
590
|
+
console.log(`[NexusHub] \u{1F504} Retry ${attempt} for '${slug}' after ${delay}ms: ${error.message}`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
);
|
|
594
|
+
this.circuitBreaker.recordSuccess();
|
|
595
|
+
return includeMetadata ? data : data.data;
|
|
596
|
+
} catch (error) {
|
|
597
|
+
this.circuitBreaker.recordFailure();
|
|
598
|
+
const staleCache = this.memoryCache.get(cacheKey);
|
|
599
|
+
if (staleCache && !forceRefresh) {
|
|
600
|
+
console.warn(`[NexusHub] \u26A0\uFE0F Serving stale content for '${slug}'`);
|
|
601
|
+
return includeMetadata ? staleCache : staleCache.data;
|
|
602
|
+
}
|
|
603
|
+
throw this.normalizeError(error, `Failed to fetch page '${slug}'`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Fetch a Collection (Optimized)
|
|
608
|
+
*/
|
|
609
|
+
async getCollection(collectionId, query = {}, options = {}) {
|
|
610
|
+
const { result, duration } = measurePerformance(
|
|
611
|
+
`getCollection("${collectionId}")`,
|
|
612
|
+
() => this._getCollection(collectionId, query, options)
|
|
613
|
+
);
|
|
614
|
+
if (this.config.debug && duration > 100) {
|
|
615
|
+
console.warn(`[NexusHub] \u26A0\uFE0F getCollection("${collectionId}") took ${duration.toFixed(2)}ms`);
|
|
616
|
+
}
|
|
617
|
+
return result;
|
|
618
|
+
}
|
|
619
|
+
async _getCollection(collectionId, query = {}, options = {}) {
|
|
620
|
+
const normalizedQuery = normalizeQuery(query);
|
|
621
|
+
const {
|
|
622
|
+
revalidate = this.defaultRevalidate,
|
|
623
|
+
tags = [],
|
|
624
|
+
forceRefresh = false,
|
|
625
|
+
includeMetadata = false
|
|
626
|
+
} = options;
|
|
627
|
+
const queryString = buildQueryString(normalizedQuery);
|
|
628
|
+
const cacheKey = `collection:${collectionId}:${queryString}`;
|
|
629
|
+
const cached = this.checkCaches(cacheKey, forceRefresh, includeMetadata);
|
|
630
|
+
if (cached) {
|
|
631
|
+
if (this.config.debug) {
|
|
632
|
+
console.log(`[NexusHub] \u26A1 Served collection '${collectionId}' from memory cache.`);
|
|
633
|
+
}
|
|
634
|
+
return includeMetadata ? cached : cached.data;
|
|
635
|
+
}
|
|
636
|
+
if (!forceRefresh && process.env.NODE_ENV === "development") {
|
|
637
|
+
const localCollection = this.localCache.getCollection(collectionId);
|
|
638
|
+
if (localCollection) {
|
|
639
|
+
const result = this.applyLocalQuery(localCollection, normalizedQuery);
|
|
640
|
+
if (this.config.debug) {
|
|
641
|
+
console.log(`[NexusHub] \u{1F4C1} Served collection '${collectionId}' from local cache.`);
|
|
642
|
+
}
|
|
643
|
+
this.memoryCache.set(cacheKey, result, {
|
|
644
|
+
tags: [...tags, CacheTags.collection(collectionId)],
|
|
645
|
+
revalidate
|
|
646
|
+
});
|
|
647
|
+
return result;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (this.circuitBreaker.isOpen()) throw new Error("Circuit open");
|
|
651
|
+
try {
|
|
652
|
+
await this.rateLimiter.checkLimit();
|
|
653
|
+
const result = await this.backoff.execute(async () => {
|
|
654
|
+
return this.fetchCollection(collectionId, normalizedQuery, cacheKey, tags, revalidate);
|
|
655
|
+
});
|
|
656
|
+
this.circuitBreaker.recordSuccess();
|
|
657
|
+
return includeMetadata ? result : result.data;
|
|
658
|
+
} catch (error) {
|
|
659
|
+
this.circuitBreaker.recordFailure();
|
|
660
|
+
const staleCache = this.memoryCache.get(cacheKey);
|
|
661
|
+
if (staleCache && !forceRefresh) {
|
|
662
|
+
console.warn(`[NexusHub] \u26A0\uFE0F Using stale cache for collection '${collectionId}'`);
|
|
663
|
+
return includeMetadata ? staleCache : staleCache.data;
|
|
664
|
+
}
|
|
665
|
+
throw this.normalizeError(error, `Failed to fetch collection '${collectionId}'`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Fetch Global Settings with nested includes support
|
|
670
|
+
*/
|
|
671
|
+
async getGlobals(options = {}) {
|
|
672
|
+
const {
|
|
673
|
+
include = [],
|
|
674
|
+
revalidate = this.defaultRevalidate,
|
|
675
|
+
forceRefresh = false
|
|
676
|
+
} = options;
|
|
677
|
+
const cacheKey = `globals:${include.join(",")}`;
|
|
678
|
+
if (!forceRefresh && this.cacheStrategy === "memory") {
|
|
679
|
+
const cached = this.memoryCache.get(cacheKey);
|
|
680
|
+
if (cached && this.isCacheValid(cached.metadata)) {
|
|
681
|
+
if (this.config.debug) console.log("[NexusHub] \u26A1 Served globals from memory cache.");
|
|
682
|
+
return cached.data;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (!forceRefresh && !this.isServer && this.cacheStrategy === "localStorage" && this.browserCache) {
|
|
686
|
+
const cached = this.browserCache.get(cacheKey);
|
|
687
|
+
if (cached) {
|
|
688
|
+
return cached;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (!forceRefresh && process.env.NODE_ENV === "development") {
|
|
692
|
+
const localGlobals = this.localCache.getGlobals();
|
|
693
|
+
if (localGlobals) {
|
|
694
|
+
this.memoryCache.set(cacheKey, localGlobals, {
|
|
695
|
+
tags: [CacheTags.global],
|
|
696
|
+
revalidate
|
|
697
|
+
});
|
|
698
|
+
return localGlobals;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
try {
|
|
702
|
+
await this.rateLimiter.checkLimit();
|
|
703
|
+
const url = `${this.config.apiUrl}/content/${this.config.projectId}/globals`;
|
|
704
|
+
const params = include.length > 0 ? `?include=${include.join(",")}` : "";
|
|
705
|
+
const res = await this.fetchWithTimeout(
|
|
706
|
+
`${url}${params}`,
|
|
707
|
+
{
|
|
708
|
+
method: "GET",
|
|
709
|
+
headers: this.getHeaders()
|
|
710
|
+
}
|
|
711
|
+
);
|
|
712
|
+
if (!res.ok) {
|
|
713
|
+
throw new Error(`API Error ${res.status}: ${res.statusText}`);
|
|
714
|
+
}
|
|
715
|
+
const json = await res.json();
|
|
716
|
+
const data = json.data || json;
|
|
717
|
+
this.writeCache(cacheKey, data, {
|
|
718
|
+
revalidate,
|
|
719
|
+
tags: [CacheTags.project(this.config.projectId), CacheTags.global]
|
|
720
|
+
});
|
|
721
|
+
return data;
|
|
722
|
+
} catch (error) {
|
|
723
|
+
const staleCache = this.memoryCache.get(cacheKey);
|
|
724
|
+
if (staleCache && !forceRefresh) {
|
|
725
|
+
console.warn("[NexusHub] \u26A0\uFE0F Using stale cache for globals");
|
|
726
|
+
return staleCache.data;
|
|
727
|
+
}
|
|
728
|
+
throw this.normalizeError(error, "Failed to fetch globals");
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Get a single item from a collection (Uses Request Batching)
|
|
733
|
+
* If you call this 10 times in a loop, it sends 1 HTTP request.
|
|
734
|
+
*/
|
|
735
|
+
async getItem(collectionId, itemId, options = {}) {
|
|
736
|
+
const cacheKey = `item:${collectionId}:${itemId}`;
|
|
737
|
+
if (this.cacheStrategy === "memory") {
|
|
738
|
+
const cached = this.memoryCache.get(cacheKey);
|
|
739
|
+
if (cached && this.isCacheValid(cached.metadata)) {
|
|
740
|
+
if (this.config.debug) {
|
|
741
|
+
console.log(`[NexusHub] \u26A1 Served item '${itemId}' from cache.`);
|
|
742
|
+
}
|
|
743
|
+
return cached.data;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return this.requestBatcher.schedule(cacheKey, async () => {
|
|
747
|
+
const params = new URLSearchParams();
|
|
748
|
+
if (options.include?.length) {
|
|
749
|
+
params.append("include", options.include.join(","));
|
|
750
|
+
}
|
|
751
|
+
const url = `${this.config.apiUrl}/content/${this.config.projectId}/collection/${collectionId}/${itemId}?${params}`;
|
|
752
|
+
const res = await this.fetchWithTimeout(url, {
|
|
753
|
+
method: "GET",
|
|
754
|
+
headers: this.getHeaders()
|
|
755
|
+
});
|
|
756
|
+
if (!res.ok) {
|
|
757
|
+
throw new Error(`API Error ${res.status}: ${res.statusText}`);
|
|
758
|
+
}
|
|
759
|
+
const json = await res.json();
|
|
760
|
+
const data = json.data || json;
|
|
761
|
+
this.writeCache(cacheKey, data, {
|
|
762
|
+
revalidate: options.revalidate || this.defaultRevalidate,
|
|
763
|
+
tags: [
|
|
764
|
+
CacheTags.project(this.config.projectId),
|
|
765
|
+
CacheTags.collection(collectionId),
|
|
766
|
+
`item_${itemId}`,
|
|
767
|
+
...options.tags || []
|
|
768
|
+
]
|
|
769
|
+
});
|
|
770
|
+
return data;
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Search across collections
|
|
775
|
+
*/
|
|
776
|
+
async search(query, options = {}) {
|
|
777
|
+
const params = new URLSearchParams({
|
|
778
|
+
q: query,
|
|
779
|
+
limit: (options.limit || 20).toString()
|
|
780
|
+
});
|
|
781
|
+
if (options.collections?.length) {
|
|
782
|
+
params.append("collections", options.collections.join(","));
|
|
783
|
+
}
|
|
784
|
+
if (options.fields?.length) {
|
|
785
|
+
params.append("fields", options.fields.join(","));
|
|
786
|
+
}
|
|
787
|
+
const url = `${this.config.apiUrl}/content/${this.config.projectId}/search?${params}`;
|
|
788
|
+
const res = await this.fetchWithTimeout(url, {
|
|
789
|
+
method: "GET",
|
|
790
|
+
headers: this.getHeaders()
|
|
791
|
+
});
|
|
792
|
+
if (!res.ok) {
|
|
793
|
+
throw new Error(`API Error ${res.status}: ${res.statusText}`);
|
|
794
|
+
}
|
|
795
|
+
return await res.json();
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Prefetch content for better performance
|
|
799
|
+
*/
|
|
800
|
+
async prefetch(urls) {
|
|
801
|
+
if (typeof window !== "undefined" && "requestIdleCallback" in window) {
|
|
802
|
+
requestIdleCallback(async () => {
|
|
803
|
+
await Promise.allSettled(
|
|
804
|
+
urls.map((url) => fetch(url, { priority: "low" }))
|
|
805
|
+
);
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Robust Real-time Subscriptions (SSE) with Auto-Reconnect
|
|
811
|
+
*/
|
|
812
|
+
subscribeToUpdates(callback) {
|
|
813
|
+
if (this.isServer || typeof EventSource === "undefined") {
|
|
814
|
+
console.warn("[NexusHub] EventSource not supported in this environment");
|
|
815
|
+
return () => {
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
let eventSource = null;
|
|
819
|
+
let retryCount = 0;
|
|
820
|
+
let isClosed = false;
|
|
821
|
+
const connect = () => {
|
|
822
|
+
if (isClosed) return;
|
|
823
|
+
const url = `${this.config.apiUrl}/content/${this.config.projectId}/updates?key=${this.config.apiKey}`;
|
|
824
|
+
eventSource = new EventSource(url);
|
|
825
|
+
eventSource.onopen = () => {
|
|
826
|
+
retryCount = 0;
|
|
827
|
+
if (this.config.debug) console.log("[NexusHub] \u{1F7E2} Real-time stream connected");
|
|
828
|
+
};
|
|
829
|
+
eventSource.onmessage = (event) => {
|
|
830
|
+
try {
|
|
831
|
+
const data = JSON.parse(event.data);
|
|
832
|
+
if (data.type === "content.updated" && data.slug) {
|
|
833
|
+
this.invalidateCache([CacheTags.content(data.slug)]);
|
|
834
|
+
}
|
|
835
|
+
if (data.type === "collection.updated") {
|
|
836
|
+
this.invalidateCache([CacheTags.collection(data.collectionId)]);
|
|
837
|
+
}
|
|
838
|
+
callback(data);
|
|
839
|
+
} catch (e) {
|
|
840
|
+
console.error("[NexusHub] SSE Parse Error", e);
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
eventSource.onerror = () => {
|
|
844
|
+
eventSource?.close();
|
|
845
|
+
if (isClosed) return;
|
|
846
|
+
const timeout = Math.min(1e3 * Math.pow(2, retryCount), 1e4);
|
|
847
|
+
retryCount++;
|
|
848
|
+
console.warn(`[NexusHub] \u{1F534} Stream disconnected. Reconnecting in ${timeout}ms...`);
|
|
849
|
+
setTimeout(connect, timeout);
|
|
850
|
+
};
|
|
851
|
+
};
|
|
852
|
+
connect();
|
|
853
|
+
return () => {
|
|
854
|
+
isClosed = true;
|
|
855
|
+
eventSource?.close();
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
// --- CACHE MANAGEMENT ---
|
|
859
|
+
/**
|
|
860
|
+
* Check all caches in order of speed
|
|
861
|
+
*/
|
|
862
|
+
checkCaches(key, forceRefresh, includeMetadata) {
|
|
863
|
+
if (forceRefresh) return null;
|
|
864
|
+
if (this.cacheStrategy === "memory") {
|
|
865
|
+
const cached = this.memoryCache.get(key);
|
|
866
|
+
if (cached && this.isCacheValid(cached.metadata)) {
|
|
867
|
+
return cached;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
if (!this.isServer && this.cacheStrategy === "localStorage" && this.browserCache) {
|
|
871
|
+
const cached = this.browserCache.get(key);
|
|
872
|
+
if (cached) {
|
|
873
|
+
return {
|
|
874
|
+
data: cached,
|
|
875
|
+
metadata: {
|
|
876
|
+
timestamp: 0,
|
|
877
|
+
expiresAt: Infinity,
|
|
878
|
+
tags: [],
|
|
879
|
+
etag: void 0
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
if (process.env.NODE_ENV === "development") {
|
|
885
|
+
if (key.startsWith("page:")) {
|
|
886
|
+
const slug = key.split(":")[1];
|
|
887
|
+
const local = this.localCache.getPage(slug);
|
|
888
|
+
if (local) {
|
|
889
|
+
return {
|
|
890
|
+
data: local,
|
|
891
|
+
metadata: {
|
|
892
|
+
timestamp: 0,
|
|
893
|
+
expiresAt: Infinity,
|
|
894
|
+
tags: [],
|
|
895
|
+
etag: void 0
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return null;
|
|
902
|
+
}
|
|
903
|
+
writeCache(key, data, options) {
|
|
904
|
+
if (this.cacheStrategy === "memory") {
|
|
905
|
+
this.memoryCache.set(key, data, {
|
|
906
|
+
tags: options.tags,
|
|
907
|
+
revalidate: options.revalidate
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
if (!this.isServer && this.cacheStrategy === "localStorage" && this.browserCache) {
|
|
911
|
+
this.browserCache.set(key, data, options.revalidate * 1e3);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Invalidate cache by tags
|
|
916
|
+
*/
|
|
917
|
+
invalidateCache(tags) {
|
|
918
|
+
this.memoryCache.invalidateByTags(tags);
|
|
919
|
+
if (this.config.debug) {
|
|
920
|
+
console.log(`[NexusHub] \u267B\uFE0F Invalidated cache for tags: ${tags.join(", ")}`);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Clear all caches
|
|
925
|
+
*/
|
|
926
|
+
clearCache() {
|
|
927
|
+
this.memoryCache.clear();
|
|
928
|
+
if (this.browserCache) {
|
|
929
|
+
this.browserCache.clear();
|
|
930
|
+
}
|
|
931
|
+
if (this.config.debug) {
|
|
932
|
+
console.log("[NexusHub] \u{1F9F9} Cleared all caches");
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Get cache statistics
|
|
937
|
+
*/
|
|
938
|
+
getCacheStats() {
|
|
939
|
+
const stats = {
|
|
940
|
+
memory: this.memoryCache.getStats(),
|
|
941
|
+
local: { loaded: this.localCache.isLoaded() }
|
|
942
|
+
};
|
|
943
|
+
if (this.browserCache) {
|
|
944
|
+
stats.browser = { size: 0 };
|
|
945
|
+
}
|
|
946
|
+
return stats;
|
|
947
|
+
}
|
|
948
|
+
// --- REQUEST METHODS ---
|
|
949
|
+
async fetchPage(slug, cacheKey, tags, revalidate, forceRefresh) {
|
|
950
|
+
const url = `${this.config.apiUrl}/content/${this.config.projectId}/page/${slug}`;
|
|
951
|
+
if (this.config.debug) {
|
|
952
|
+
console.log(`[NexusHub] \u{1F310} Fetching: ${url}`);
|
|
953
|
+
}
|
|
954
|
+
const res = await this.fetchWithTimeout(url, {
|
|
955
|
+
method: "GET",
|
|
956
|
+
headers: this.getHeaders()
|
|
957
|
+
});
|
|
958
|
+
if (!res.ok) {
|
|
959
|
+
if (res.status === 404) {
|
|
960
|
+
throw new Error(`Page '${slug}' not found. Check your Dashboard or Seed data.`);
|
|
961
|
+
}
|
|
962
|
+
throw new Error(`API Error ${res.status}: ${res.statusText}`);
|
|
963
|
+
}
|
|
964
|
+
const json = await res.json();
|
|
965
|
+
const etag = res.headers.get("etag");
|
|
966
|
+
const cacheEntry = {
|
|
967
|
+
data: json.data,
|
|
968
|
+
metadata: {
|
|
969
|
+
timestamp: Date.now(),
|
|
970
|
+
etag: etag || void 0,
|
|
971
|
+
expiresAt: Date.now() + revalidate * 1e3,
|
|
972
|
+
tags: [...tags, CacheTags.content(slug)]
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
this.writeCache(cacheKey, cacheEntry.data, {
|
|
976
|
+
revalidate,
|
|
977
|
+
tags: [CacheTags.project(this.config.projectId), CacheTags.content(slug), ...tags]
|
|
978
|
+
});
|
|
979
|
+
return cacheEntry;
|
|
980
|
+
}
|
|
981
|
+
async fetchCollection(collectionId, query, cacheKey, tags, revalidate) {
|
|
982
|
+
const params = buildQueryString(query);
|
|
983
|
+
const url = `${this.config.apiUrl}/content/${this.config.projectId}/collection/${collectionId}?${params}`;
|
|
984
|
+
if (this.config.debug) {
|
|
985
|
+
console.log(`[NexusHub] \u{1F310} Fetching Collection: ${url}`);
|
|
986
|
+
}
|
|
987
|
+
const res = await this.fetchWithTimeout(url, {
|
|
988
|
+
method: "GET",
|
|
989
|
+
headers: this.getHeaders()
|
|
990
|
+
});
|
|
991
|
+
if (!res.ok) {
|
|
992
|
+
throw new Error(`API Error ${res.status}: ${res.statusText}`);
|
|
993
|
+
}
|
|
994
|
+
const json = await res.json();
|
|
995
|
+
const etag = res.headers.get("etag");
|
|
996
|
+
const cacheEntry = {
|
|
997
|
+
data: json,
|
|
998
|
+
metadata: {
|
|
999
|
+
timestamp: Date.now(),
|
|
1000
|
+
etag: etag || void 0,
|
|
1001
|
+
expiresAt: Date.now() + revalidate * 1e3,
|
|
1002
|
+
tags: [...tags, CacheTags.collection(collectionId)]
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
this.writeCache(cacheKey, cacheEntry.data, {
|
|
1006
|
+
revalidate,
|
|
1007
|
+
tags: [CacheTags.project(this.config.projectId), CacheTags.collection(collectionId), ...tags]
|
|
1008
|
+
});
|
|
1009
|
+
return cacheEntry;
|
|
1010
|
+
}
|
|
1011
|
+
// --- HELPER METHODS ---
|
|
1012
|
+
applyLocalQuery(items, query) {
|
|
1013
|
+
let filtered = [...items];
|
|
1014
|
+
if (query.search) {
|
|
1015
|
+
const searchLower = query.search.toLowerCase();
|
|
1016
|
+
filtered = filtered.filter(
|
|
1017
|
+
(item) => JSON.stringify(item).toLowerCase().includes(searchLower)
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
if (query.filter) {
|
|
1021
|
+
filtered = filtered.filter((item) => {
|
|
1022
|
+
return Object.entries(query.filter).every(([key, value]) => {
|
|
1023
|
+
const itemValue = item[key];
|
|
1024
|
+
if (itemValue === void 0) return false;
|
|
1025
|
+
if (Array.isArray(value)) {
|
|
1026
|
+
return value.includes(itemValue);
|
|
1027
|
+
}
|
|
1028
|
+
return itemValue === value;
|
|
1029
|
+
});
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
if (query.sort) {
|
|
1033
|
+
filtered.sort((a, b) => {
|
|
1034
|
+
const aVal = a[query.sort];
|
|
1035
|
+
const bVal = b[query.sort];
|
|
1036
|
+
const order = query.order === "asc" ? 1 : -1;
|
|
1037
|
+
if (aVal < bVal) return -1 * order;
|
|
1038
|
+
if (aVal > bVal) return 1 * order;
|
|
1039
|
+
return 0;
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
const page = query.page || 1;
|
|
1043
|
+
const limit = query.limit || 10;
|
|
1044
|
+
const start = (page - 1) * limit;
|
|
1045
|
+
const end = start + limit;
|
|
1046
|
+
const total = filtered.length;
|
|
1047
|
+
return {
|
|
1048
|
+
items: filtered.slice(start, end),
|
|
1049
|
+
total,
|
|
1050
|
+
page,
|
|
1051
|
+
limit,
|
|
1052
|
+
totalPages: Math.ceil(total / limit),
|
|
1053
|
+
hasNext: end < total,
|
|
1054
|
+
hasPrev: start > 0
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
async fetchWithTimeout(url, options = {}) {
|
|
1058
|
+
const { timeout = this.config.timeout || 1e4, ...fetchOptions } = options;
|
|
1059
|
+
const controller = new AbortController();
|
|
1060
|
+
const id = setTimeout(() => controller.abort(), timeout);
|
|
1061
|
+
try {
|
|
1062
|
+
const response = await fetch(url, {
|
|
1063
|
+
...fetchOptions,
|
|
1064
|
+
signal: controller.signal
|
|
1065
|
+
});
|
|
1066
|
+
clearTimeout(id);
|
|
1067
|
+
return response;
|
|
1068
|
+
} catch (error) {
|
|
1069
|
+
clearTimeout(id);
|
|
1070
|
+
throw error;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
getHeaders() {
|
|
1074
|
+
const headers = {
|
|
1075
|
+
"Content-Type": "application/json",
|
|
1076
|
+
"X-Nexus-Client": "client-sdk/1.0.0",
|
|
1077
|
+
"X-Nexus-Project": this.config.projectId
|
|
1078
|
+
};
|
|
1079
|
+
if (this.config.apiKey) {
|
|
1080
|
+
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
1081
|
+
}
|
|
1082
|
+
return headers;
|
|
1083
|
+
}
|
|
1084
|
+
isCacheValid(metadata) {
|
|
1085
|
+
return Date.now() < metadata.expiresAt;
|
|
1086
|
+
}
|
|
1087
|
+
normalizeError(error, context) {
|
|
1088
|
+
if (error instanceof Error) {
|
|
1089
|
+
if (error.name === "AbortError") {
|
|
1090
|
+
return new Error(`${context}: Request timeout`);
|
|
1091
|
+
}
|
|
1092
|
+
return error;
|
|
1093
|
+
}
|
|
1094
|
+
return new Error(`${context}: ${String(error)}`);
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Cancel ongoing requests
|
|
1098
|
+
*/
|
|
1099
|
+
cancelRequests() {
|
|
1100
|
+
if (this.abortController) {
|
|
1101
|
+
this.abortController.abort();
|
|
1102
|
+
this.abortController = new AbortController();
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Cleanup resources
|
|
1107
|
+
*/
|
|
1108
|
+
cleanup() {
|
|
1109
|
+
this.cancelRequests();
|
|
1110
|
+
if (!this.isServer) {
|
|
1111
|
+
window.removeEventListener("beforeunload", this.cleanup.bind(this));
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
// src/analytics/fingerprint.ts
|
|
1117
|
+
var getDeviceEntropy = async () => {
|
|
1118
|
+
if (typeof window === "undefined") return {};
|
|
1119
|
+
const nav = window.navigator;
|
|
1120
|
+
return {
|
|
1121
|
+
screen_resolution: `${window.screen.width}x${window.screen.height}`,
|
|
1122
|
+
color_depth: window.screen.colorDepth,
|
|
1123
|
+
pixel_ratio: window.devicePixelRatio || 1,
|
|
1124
|
+
hardware_concurrency: nav.hardwareConcurrency,
|
|
1125
|
+
device_memory: nav.deviceMemory,
|
|
1126
|
+
// Only available in Chrome/Edge
|
|
1127
|
+
timezone_offset: (/* @__PURE__ */ new Date()).getTimezoneOffset(),
|
|
1128
|
+
platform: nav.platform,
|
|
1129
|
+
language: nav.language,
|
|
1130
|
+
touch_support: "ontouchstart" in window || nav.maxTouchPoints > 0,
|
|
1131
|
+
// Optional: Calculate Canvas Fingerprint for high-security modes
|
|
1132
|
+
canvas_hash: await generateCanvasHash()
|
|
1133
|
+
};
|
|
1134
|
+
};
|
|
1135
|
+
var generateCanvasHash = async () => {
|
|
1136
|
+
try {
|
|
1137
|
+
const canvas = document.createElement("canvas");
|
|
1138
|
+
const ctx = canvas.getContext("2d");
|
|
1139
|
+
if (!ctx) return "";
|
|
1140
|
+
canvas.width = 200;
|
|
1141
|
+
canvas.height = 50;
|
|
1142
|
+
ctx.textBaseline = "top";
|
|
1143
|
+
ctx.font = '16px "Arial"';
|
|
1144
|
+
ctx.textBaseline = "alphabetic";
|
|
1145
|
+
ctx.fillStyle = "#f60";
|
|
1146
|
+
ctx.fillRect(125, 1, 62, 20);
|
|
1147
|
+
ctx.fillStyle = "#069";
|
|
1148
|
+
ctx.fillText("NexusHub Rocks! <canvas> 1.0", 2, 15);
|
|
1149
|
+
ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
|
|
1150
|
+
ctx.fillText("NexusHub Rocks! <canvas> 1.0", 4, 17);
|
|
1151
|
+
const dataUrl = canvas.toDataURL();
|
|
1152
|
+
let hash = 0;
|
|
1153
|
+
for (let i = 0; i < dataUrl.length; i++) {
|
|
1154
|
+
const char = dataUrl.charCodeAt(i);
|
|
1155
|
+
hash = (hash << 5) - hash + char;
|
|
1156
|
+
hash = hash & hash;
|
|
1157
|
+
}
|
|
1158
|
+
return hash.toString(16);
|
|
1159
|
+
} catch {
|
|
1160
|
+
return "";
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
var getVisitorId = async () => {
|
|
1164
|
+
if (typeof window === "undefined") return "server_visitor";
|
|
1165
|
+
const STORAGE_KEY = "nexus_vid";
|
|
1166
|
+
let vid = localStorage.getItem(STORAGE_KEY);
|
|
1167
|
+
if (!vid) {
|
|
1168
|
+
const entropy = await getDeviceEntropy();
|
|
1169
|
+
const random = Math.random().toString(36).substring(2, 15);
|
|
1170
|
+
const timestamp = Date.now().toString(36);
|
|
1171
|
+
const fingerprint = [
|
|
1172
|
+
entropy.screen_resolution,
|
|
1173
|
+
entropy.hardware_concurrency,
|
|
1174
|
+
entropy.timezone_offset,
|
|
1175
|
+
entropy.platform,
|
|
1176
|
+
entropy.canvas_hash
|
|
1177
|
+
].join("|");
|
|
1178
|
+
let hash = 0;
|
|
1179
|
+
for (let i = 0; i < fingerprint.length; i++) {
|
|
1180
|
+
hash = (hash << 5) - hash + fingerprint.charCodeAt(i);
|
|
1181
|
+
hash |= 0;
|
|
1182
|
+
}
|
|
1183
|
+
vid = `vis_${hash.toString(16)}_${timestamp}_${random}`;
|
|
1184
|
+
localStorage.setItem(STORAGE_KEY, vid);
|
|
1185
|
+
}
|
|
1186
|
+
return vid;
|
|
1187
|
+
};
|
|
1188
|
+
var METRIC_KEY_MAP = {
|
|
1189
|
+
CLS: "cls",
|
|
1190
|
+
LCP: "lcp",
|
|
1191
|
+
INP: "inp",
|
|
1192
|
+
TTFB: "ttfb",
|
|
1193
|
+
FCP: "fcp"
|
|
1194
|
+
};
|
|
1195
|
+
var VitalsCollector = class {
|
|
1196
|
+
constructor() {
|
|
1197
|
+
this.metrics = {};
|
|
1198
|
+
this.hasUpdates = false;
|
|
1199
|
+
if (typeof window !== "undefined") {
|
|
1200
|
+
this.init();
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
init() {
|
|
1204
|
+
const recordMetric = (metric) => {
|
|
1205
|
+
const key = METRIC_KEY_MAP[metric.name];
|
|
1206
|
+
this.metrics[key] = metric.value;
|
|
1207
|
+
this.hasUpdates = true;
|
|
1208
|
+
};
|
|
1209
|
+
webVitals.onCLS(recordMetric);
|
|
1210
|
+
webVitals.onLCP(recordMetric);
|
|
1211
|
+
webVitals.onTTFB(recordMetric);
|
|
1212
|
+
webVitals.onINP(recordMetric);
|
|
1213
|
+
webVitals.onFCP(recordMetric);
|
|
1214
|
+
const navEntry = performance.getEntriesByType("navigation")[0];
|
|
1215
|
+
if (navEntry) {
|
|
1216
|
+
this.metrics.navigation_timing = {
|
|
1217
|
+
dns_lookup: navEntry.domainLookupEnd - navEntry.domainLookupStart,
|
|
1218
|
+
tcp_connect: navEntry.connectEnd - navEntry.connectStart,
|
|
1219
|
+
request_time: navEntry.responseEnd - navEntry.requestStart,
|
|
1220
|
+
dom_load: navEntry.domComplete - navEntry.domInteractive
|
|
1221
|
+
};
|
|
1222
|
+
this.hasUpdates = true;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
getMetricsSnapshot() {
|
|
1226
|
+
if (!this.hasUpdates) return null;
|
|
1227
|
+
this.hasUpdates = false;
|
|
1228
|
+
return { ...this.metrics };
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
1231
|
+
var vitalsCollector = new VitalsCollector();
|
|
1232
|
+
var initVitals = (tracker) => {
|
|
1233
|
+
if (process.env.NODE_ENV === "development") {
|
|
1234
|
+
console.log("[NexusHub] Web Vitals monitoring active");
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
|
|
1238
|
+
// src/analytics/storage.ts
|
|
1239
|
+
var DB_NAME = "NexusHub_Analytics";
|
|
1240
|
+
var STORE_NAME = "events_queue";
|
|
1241
|
+
var DB_VERSION = 1;
|
|
1242
|
+
var EventStorage = class {
|
|
1243
|
+
constructor() {
|
|
1244
|
+
this.db = null;
|
|
1245
|
+
this.isReady = this.init();
|
|
1246
|
+
}
|
|
1247
|
+
init() {
|
|
1248
|
+
if (typeof window === "undefined" || !window.indexedDB) {
|
|
1249
|
+
return Promise.resolve();
|
|
1250
|
+
}
|
|
1251
|
+
return new Promise((resolve, reject) => {
|
|
1252
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
1253
|
+
request.onerror = () => {
|
|
1254
|
+
console.warn("[NexusHub] Failed to open IndexedDB. Falling back to memory.");
|
|
1255
|
+
resolve();
|
|
1256
|
+
};
|
|
1257
|
+
request.onsuccess = (event) => {
|
|
1258
|
+
this.db = event.target.result;
|
|
1259
|
+
resolve();
|
|
1260
|
+
};
|
|
1261
|
+
request.onupgradeneeded = (event) => {
|
|
1262
|
+
const db = event.target.result;
|
|
1263
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
1264
|
+
db.createObjectStore(STORE_NAME, { keyPath: "id", autoIncrement: true });
|
|
1265
|
+
}
|
|
1266
|
+
};
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Add an event to the persistent queue
|
|
1271
|
+
*/
|
|
1272
|
+
async enqueue(payload) {
|
|
1273
|
+
await this.isReady;
|
|
1274
|
+
if (!this.db) return;
|
|
1275
|
+
return new Promise((resolve, reject) => {
|
|
1276
|
+
const transaction = this.db.transaction([STORE_NAME], "readwrite");
|
|
1277
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
1278
|
+
const request = store.add({
|
|
1279
|
+
payload,
|
|
1280
|
+
timestamp: Date.now(),
|
|
1281
|
+
retryCount: 0
|
|
1282
|
+
});
|
|
1283
|
+
request.onsuccess = () => resolve();
|
|
1284
|
+
request.onerror = () => reject(request.error);
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* Get a batch of oldest events
|
|
1289
|
+
*/
|
|
1290
|
+
async peek(limit = 20) {
|
|
1291
|
+
await this.isReady;
|
|
1292
|
+
if (!this.db) return [];
|
|
1293
|
+
return new Promise((resolve) => {
|
|
1294
|
+
const transaction = this.db.transaction([STORE_NAME], "readonly");
|
|
1295
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
1296
|
+
const request = store.getAll(null, limit);
|
|
1297
|
+
request.onsuccess = () => resolve(request.result);
|
|
1298
|
+
request.onerror = () => resolve([]);
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Remove events after successful upload
|
|
1303
|
+
*/
|
|
1304
|
+
async remove(ids) {
|
|
1305
|
+
await this.isReady;
|
|
1306
|
+
if (!this.db || ids.length === 0) return;
|
|
1307
|
+
return new Promise((resolve, reject) => {
|
|
1308
|
+
const transaction = this.db.transaction([STORE_NAME], "readwrite");
|
|
1309
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
1310
|
+
transaction.oncomplete = () => resolve();
|
|
1311
|
+
transaction.onerror = () => reject(transaction.error);
|
|
1312
|
+
ids.forEach((id) => {
|
|
1313
|
+
store.delete(id);
|
|
1314
|
+
});
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Count pending events
|
|
1319
|
+
*/
|
|
1320
|
+
async count() {
|
|
1321
|
+
await this.isReady;
|
|
1322
|
+
if (!this.db) return 0;
|
|
1323
|
+
return new Promise((resolve) => {
|
|
1324
|
+
const transaction = this.db.transaction([STORE_NAME], "readonly");
|
|
1325
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
1326
|
+
const request = store.count();
|
|
1327
|
+
request.onsuccess = () => resolve(request.result);
|
|
1328
|
+
request.onerror = () => resolve(0);
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
var eventStorage = new EventStorage();
|
|
1333
|
+
|
|
1334
|
+
// src/analytics/tracker.ts
|
|
1335
|
+
var Tracker = class {
|
|
1336
|
+
// 🆕
|
|
1337
|
+
constructor(config) {
|
|
1338
|
+
this.sessionId = "";
|
|
1339
|
+
this.visitorId = "";
|
|
1340
|
+
// FIX: Add this property
|
|
1341
|
+
this.isFlushing = false;
|
|
1342
|
+
this.config = config;
|
|
1343
|
+
this.config.apiUrl.replace("/v1", "").replace(/\/$/, "");
|
|
1344
|
+
this.endpoint = `${this.config.analyticsUrl}/api/collect`;
|
|
1345
|
+
this.circuitBreaker = new CircuitBreaker();
|
|
1346
|
+
this.sessionStart = Date.now();
|
|
1347
|
+
if (typeof window !== "undefined") {
|
|
1348
|
+
this.initSession();
|
|
1349
|
+
this.startFlushing();
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
async initSession() {
|
|
1353
|
+
this.visitorId = await getVisitorId();
|
|
1354
|
+
let sid = localStorage.getItem("nexus_sid");
|
|
1355
|
+
let lastActivity = localStorage.getItem("nexus_last_active");
|
|
1356
|
+
const now = Date.now();
|
|
1357
|
+
const SESSION_TIMEOUT = 30 * 60 * 1e3;
|
|
1358
|
+
if (!sid || !lastActivity || now - parseInt(lastActivity) > SESSION_TIMEOUT) {
|
|
1359
|
+
sid = `sess_${Math.random().toString(36).substring(2, 9)}_${now}`;
|
|
1360
|
+
localStorage.setItem("nexus_sid", sid);
|
|
1361
|
+
this.sessionStart = now;
|
|
1362
|
+
}
|
|
1363
|
+
localStorage.setItem("nexus_last_active", now.toString());
|
|
1364
|
+
this.sessionId = sid;
|
|
1365
|
+
}
|
|
1366
|
+
async send(eventType, data = {}, eventName) {
|
|
1367
|
+
if (typeof window === "undefined") return;
|
|
1368
|
+
localStorage.setItem("nexus_last_active", Date.now().toString());
|
|
1369
|
+
const entropy = await getDeviceEntropy();
|
|
1370
|
+
const perfMetrics = vitalsCollector.getMetricsSnapshot();
|
|
1371
|
+
const payload = {
|
|
1372
|
+
project_id: this.config.projectId,
|
|
1373
|
+
session_id: this.sessionId,
|
|
1374
|
+
// We send this, Rust verifies it
|
|
1375
|
+
url: window.location.href,
|
|
1376
|
+
referrer: document.referrer,
|
|
1377
|
+
user_agent: window.navigator.userAgent,
|
|
1378
|
+
screen_width: window.screen.width,
|
|
1379
|
+
screen_height: window.screen.height,
|
|
1380
|
+
language: window.navigator.language,
|
|
1381
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
1382
|
+
event_type: eventType,
|
|
1383
|
+
event_name: eventName,
|
|
1384
|
+
event_data: data,
|
|
1385
|
+
// 3. Attach Performance Data (Rust: Option<PerformanceMetrics>)
|
|
1386
|
+
performance: perfMetrics || void 0,
|
|
1387
|
+
// 4. Attach Hardware Signals for Anomaly Detection (Rust: DeviceData)
|
|
1388
|
+
context: {
|
|
1389
|
+
device: {
|
|
1390
|
+
hardware_concurrency: entropy.hardware_concurrency,
|
|
1391
|
+
device_memory: entropy.device_memory,
|
|
1392
|
+
pixel_ratio: entropy.pixel_ratio,
|
|
1393
|
+
canvas_fingerprint: entropy.canvas_hash,
|
|
1394
|
+
platform: entropy.platform
|
|
1395
|
+
},
|
|
1396
|
+
visitor_id_local: this.visitorId
|
|
1397
|
+
// Hint for stitching
|
|
1398
|
+
},
|
|
1399
|
+
client_timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1400
|
+
};
|
|
1401
|
+
await eventStorage.enqueue(payload);
|
|
1402
|
+
const count = await eventStorage.count();
|
|
1403
|
+
if (count >= 10 || eventType === "purchase" || eventType === "identify") {
|
|
1404
|
+
this.flushQueue();
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
startFlushing() {
|
|
1408
|
+
this.flushInterval = setInterval(() => this.flushQueue(), 5e3);
|
|
1409
|
+
window.addEventListener("visibilitychange", () => {
|
|
1410
|
+
if (document.visibilityState === "hidden") this.flushQueue(true);
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
async flushQueue(useBeacon = false) {
|
|
1414
|
+
if (this.isFlushing) return;
|
|
1415
|
+
if (this.circuitBreaker.isOpen()) {
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
this.isFlushing = true;
|
|
1419
|
+
try {
|
|
1420
|
+
const storedEvents = await eventStorage.peek(20);
|
|
1421
|
+
if (storedEvents.length === 0) {
|
|
1422
|
+
this.isFlushing = false;
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
const payloads = storedEvents.map((e) => e.payload);
|
|
1426
|
+
const promises = payloads.map(
|
|
1427
|
+
(event) => fetch(this.endpoint, {
|
|
1428
|
+
method: "POST",
|
|
1429
|
+
headers: {
|
|
1430
|
+
"Content-Type": "application/json",
|
|
1431
|
+
Authorization: `Bearer ${this.config.apiKey}`
|
|
1432
|
+
},
|
|
1433
|
+
body: JSON.stringify(event),
|
|
1434
|
+
keepalive: useBeacon
|
|
1435
|
+
})
|
|
1436
|
+
);
|
|
1437
|
+
const results = await Promise.allSettled(promises);
|
|
1438
|
+
const successIds = [];
|
|
1439
|
+
let failureCount = 0;
|
|
1440
|
+
results.forEach((res, index) => {
|
|
1441
|
+
if (res.status === "fulfilled" && res.value.ok) {
|
|
1442
|
+
successIds.push(storedEvents[index].id);
|
|
1443
|
+
} else {
|
|
1444
|
+
failureCount++;
|
|
1445
|
+
}
|
|
1446
|
+
});
|
|
1447
|
+
if (successIds.length > 0) {
|
|
1448
|
+
await eventStorage.remove(successIds);
|
|
1449
|
+
this.circuitBreaker.recordSuccess();
|
|
1450
|
+
}
|
|
1451
|
+
if (failureCount > 0) {
|
|
1452
|
+
this.circuitBreaker.recordFailure();
|
|
1453
|
+
}
|
|
1454
|
+
} catch (err) {
|
|
1455
|
+
console.error("[NexusHub] Network Error:", err);
|
|
1456
|
+
this.circuitBreaker.recordFailure();
|
|
1457
|
+
} finally {
|
|
1458
|
+
this.isFlushing = false;
|
|
1459
|
+
if (!this.circuitBreaker.isOpen() && await eventStorage.count() > 0) {
|
|
1460
|
+
setTimeout(() => this.flushQueue(), 100);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
getSession() {
|
|
1465
|
+
return this.sessionId;
|
|
1466
|
+
}
|
|
1467
|
+
// FIX: Add this method to satisfy AnalyticsEngine
|
|
1468
|
+
getSessionDuration() {
|
|
1469
|
+
return Date.now() - this.sessionStart;
|
|
1470
|
+
}
|
|
1471
|
+
// FIX: Add this method to satisfy AnalyticsEngine
|
|
1472
|
+
stop() {
|
|
1473
|
+
if (this.flushInterval) {
|
|
1474
|
+
clearInterval(this.flushInterval);
|
|
1475
|
+
}
|
|
1476
|
+
this.flushQueue(true);
|
|
1477
|
+
}
|
|
1478
|
+
};
|
|
1479
|
+
|
|
1480
|
+
// src/analytics/index.ts
|
|
1481
|
+
var AnalyticsEngine = class {
|
|
1482
|
+
constructor(config) {
|
|
1483
|
+
this.cleanupFns = [];
|
|
1484
|
+
this.isInitialized = false;
|
|
1485
|
+
this.tracker = new Tracker(config);
|
|
1486
|
+
}
|
|
1487
|
+
start() {
|
|
1488
|
+
if (this.isInitialized || typeof window === "undefined") return;
|
|
1489
|
+
this.isInitialized = true;
|
|
1490
|
+
this.pageView();
|
|
1491
|
+
initVitals(this.tracker);
|
|
1492
|
+
this.setupClickTracking();
|
|
1493
|
+
this.setupFormTracking();
|
|
1494
|
+
this.setupRouteTracking();
|
|
1495
|
+
this.setupShareTracking();
|
|
1496
|
+
console.log("[NexusHub] \u{1F680} Analytics Engine Started");
|
|
1497
|
+
}
|
|
1498
|
+
pageView() {
|
|
1499
|
+
if (typeof window === "undefined") return;
|
|
1500
|
+
this.tracker.send("page_view", {
|
|
1501
|
+
path: window.location.pathname,
|
|
1502
|
+
search: window.location.search,
|
|
1503
|
+
title: document.title,
|
|
1504
|
+
timezone_offset: (/* @__PURE__ */ new Date()).getTimezoneOffset()
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
// 🆕 PRIVATE: Track "Dark Social" Intent
|
|
1508
|
+
setupShareTracking() {
|
|
1509
|
+
if (typeof window === "undefined") return;
|
|
1510
|
+
const copyHandler = () => {
|
|
1511
|
+
this.tracker.send("social_share", {
|
|
1512
|
+
method: "clipboard_copy",
|
|
1513
|
+
url: window.location.href
|
|
1514
|
+
});
|
|
1515
|
+
};
|
|
1516
|
+
if (navigator.share) {
|
|
1517
|
+
const originalShare = navigator.share;
|
|
1518
|
+
navigator.share = (data) => {
|
|
1519
|
+
this.tracker.send("social_share", {
|
|
1520
|
+
method: "native_share_menu",
|
|
1521
|
+
url: data?.url || window.location.href,
|
|
1522
|
+
title: data?.title
|
|
1523
|
+
});
|
|
1524
|
+
return originalShare.apply(navigator, [data]);
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
window.addEventListener("copy", copyHandler, { passive: true });
|
|
1528
|
+
this.cleanupFns.push(() => window.removeEventListener("copy", copyHandler));
|
|
1529
|
+
}
|
|
1530
|
+
// --- IDENTITY & B2B (The Missing Pieces) ---
|
|
1531
|
+
/**
|
|
1532
|
+
* 1. IDENTIFY: Link anonymous session to a User ID
|
|
1533
|
+
*/
|
|
1534
|
+
async identify(userId, traits = {}) {
|
|
1535
|
+
return this.sendIdentityRequest("identify", {
|
|
1536
|
+
user_id: userId,
|
|
1537
|
+
traits
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
/**
|
|
1541
|
+
* 2. GROUP: Link the current user to a Company/Organization (B2B)
|
|
1542
|
+
* Required for the /api/group Rust route.
|
|
1543
|
+
*/
|
|
1544
|
+
async group(groupId, traits = {}) {
|
|
1545
|
+
return this.sendIdentityRequest("group", {
|
|
1546
|
+
group_id: groupId,
|
|
1547
|
+
// The Rust backend handles looking up the user from the session if not provided,
|
|
1548
|
+
// but passing the user_id if known is safer.
|
|
1549
|
+
traits
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* 3. ALIAS: Merge two identities (e.g. "Guest_123" -> "User_99")
|
|
1554
|
+
* Required for the /api/alias Rust route.
|
|
1555
|
+
*/
|
|
1556
|
+
async alias(newId) {
|
|
1557
|
+
return this.sendIdentityRequest("alias", {
|
|
1558
|
+
previous_id: this.tracker.getSession(),
|
|
1559
|
+
// Or the old visitor_id
|
|
1560
|
+
user_id: newId
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* 4. RESET: Clear local data and (optionally) request GDPR scrub
|
|
1565
|
+
*/
|
|
1566
|
+
reset(performGdprScrub = false) {
|
|
1567
|
+
const config = this.tracker["config"];
|
|
1568
|
+
this.tracker.stop();
|
|
1569
|
+
localStorage.removeItem("nexus_sid");
|
|
1570
|
+
localStorage.removeItem("nexus_vid");
|
|
1571
|
+
if (performGdprScrub) {
|
|
1572
|
+
const endpoint = `${config.analyticsUrl}/api/privacy/scrub`;
|
|
1573
|
+
const userId = this.tracker.visitorId;
|
|
1574
|
+
fetch(endpoint, {
|
|
1575
|
+
method: "DELETE",
|
|
1576
|
+
headers: {
|
|
1577
|
+
"Content-Type": "application/json",
|
|
1578
|
+
Authorization: `Bearer ${config.apiKey}`
|
|
1579
|
+
},
|
|
1580
|
+
body: JSON.stringify({
|
|
1581
|
+
project_id: config.projectId,
|
|
1582
|
+
user_id: userId
|
|
1583
|
+
})
|
|
1584
|
+
}).catch((err) => console.error("[NexusHub] Privacy scrub failed:", err));
|
|
1585
|
+
}
|
|
1586
|
+
window.location.reload();
|
|
1587
|
+
}
|
|
1588
|
+
// --- E-COMMERCE & CUSTOM ---
|
|
1589
|
+
track(eventName, properties = {}) {
|
|
1590
|
+
this.tracker.send(
|
|
1591
|
+
"custom_event",
|
|
1592
|
+
{ event_name: eventName, ...properties },
|
|
1593
|
+
eventName
|
|
1594
|
+
);
|
|
1595
|
+
}
|
|
1596
|
+
trackPurchase(orderData) {
|
|
1597
|
+
this.tracker.send("purchase", {
|
|
1598
|
+
...orderData,
|
|
1599
|
+
currency: orderData.currency || "USD",
|
|
1600
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
trackError(error, context) {
|
|
1604
|
+
this.tracker.send("error", {
|
|
1605
|
+
message: error.message,
|
|
1606
|
+
stack: error.stack,
|
|
1607
|
+
context,
|
|
1608
|
+
url: typeof window !== "undefined" ? window.location.href : ""
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
// --- INTERNAL HELPER ---
|
|
1612
|
+
async sendIdentityRequest(type, data) {
|
|
1613
|
+
try {
|
|
1614
|
+
const config = this.tracker["config"];
|
|
1615
|
+
const endpoint = `${config.analyticsUrl}/api/${type}`;
|
|
1616
|
+
const payload = {
|
|
1617
|
+
project_id: this.tracker["config"].projectId,
|
|
1618
|
+
session_id: this.tracker.getSession(),
|
|
1619
|
+
visitor_id: this.tracker.visitorId,
|
|
1620
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1621
|
+
...data
|
|
1622
|
+
};
|
|
1623
|
+
if (type === "group" && !payload.user_id) {
|
|
1624
|
+
}
|
|
1625
|
+
const response = await fetch(endpoint, {
|
|
1626
|
+
method: "POST",
|
|
1627
|
+
headers: {
|
|
1628
|
+
"Content-Type": "application/json",
|
|
1629
|
+
Authorization: `Bearer ${this.tracker["config"].apiKey}`
|
|
1630
|
+
},
|
|
1631
|
+
body: JSON.stringify(payload)
|
|
1632
|
+
});
|
|
1633
|
+
if (response.ok) {
|
|
1634
|
+
this.tracker.send(type, data);
|
|
1635
|
+
return true;
|
|
1636
|
+
}
|
|
1637
|
+
return false;
|
|
1638
|
+
} catch (error) {
|
|
1639
|
+
console.error(`[NexusHub] ${type} failed:`, error);
|
|
1640
|
+
return false;
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
// --- PRIVATE EVENT LISTENERS (Keep existing setupClickTracking, setupFormTracking, setupRouteTracking) ---
|
|
1644
|
+
setupClickTracking() {
|
|
1645
|
+
if (typeof window === "undefined") return;
|
|
1646
|
+
const clickHandler = (e) => {
|
|
1647
|
+
const target = e.target;
|
|
1648
|
+
const link = target.closest("a");
|
|
1649
|
+
if (link) {
|
|
1650
|
+
this.tracker.send("click", {
|
|
1651
|
+
element_type: "link",
|
|
1652
|
+
href: link.href,
|
|
1653
|
+
text: link.innerText?.substring(0, 50),
|
|
1654
|
+
id: link.id,
|
|
1655
|
+
classes: link.className,
|
|
1656
|
+
dataset: { ...link.dataset }
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
const button = target.closest("button");
|
|
1660
|
+
if (button) {
|
|
1661
|
+
this.tracker.send("click", {
|
|
1662
|
+
element_type: "button",
|
|
1663
|
+
text: button.innerText?.substring(0, 50),
|
|
1664
|
+
id: button.id,
|
|
1665
|
+
classes: button.className,
|
|
1666
|
+
coordinates: { x: e.clientX, y: e.clientY }
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
};
|
|
1670
|
+
window.addEventListener("click", clickHandler, { passive: true });
|
|
1671
|
+
this.cleanupFns.push(
|
|
1672
|
+
() => window.removeEventListener("click", clickHandler)
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
setupFormTracking() {
|
|
1676
|
+
if (typeof document === "undefined") return;
|
|
1677
|
+
const submitHandler = (e) => {
|
|
1678
|
+
const form = e.target;
|
|
1679
|
+
if (form) {
|
|
1680
|
+
this.tracker.send("form_submit", {
|
|
1681
|
+
form_id: form.id || form.name || "unknown_form",
|
|
1682
|
+
action: form.action,
|
|
1683
|
+
method: form.method,
|
|
1684
|
+
field_count: form.elements.length
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
};
|
|
1688
|
+
document.addEventListener("submit", submitHandler, { passive: true });
|
|
1689
|
+
this.cleanupFns.push(
|
|
1690
|
+
() => document.removeEventListener("submit", submitHandler)
|
|
1691
|
+
);
|
|
1692
|
+
}
|
|
1693
|
+
setupRouteTracking() {
|
|
1694
|
+
if (typeof window === "undefined" || typeof window.history === "undefined")
|
|
1695
|
+
return;
|
|
1696
|
+
const originalPushState = history.pushState;
|
|
1697
|
+
const originalReplaceState = history.replaceState;
|
|
1698
|
+
history.pushState = (...args) => {
|
|
1699
|
+
originalPushState.apply(history, args);
|
|
1700
|
+
this.pageView();
|
|
1701
|
+
};
|
|
1702
|
+
history.replaceState = (...args) => {
|
|
1703
|
+
originalReplaceState.apply(history, args);
|
|
1704
|
+
this.pageView();
|
|
1705
|
+
};
|
|
1706
|
+
const popStateHandler = () => this.pageView();
|
|
1707
|
+
window.addEventListener("popstate", popStateHandler);
|
|
1708
|
+
this.cleanupFns.push(
|
|
1709
|
+
() => window.removeEventListener("popstate", popStateHandler)
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1712
|
+
getSessionId() {
|
|
1713
|
+
return this.tracker.getSession();
|
|
1714
|
+
}
|
|
1715
|
+
stop() {
|
|
1716
|
+
this.cleanupFns.forEach((fn) => fn());
|
|
1717
|
+
this.cleanupFns = [];
|
|
1718
|
+
this.tracker.send("session_end", {
|
|
1719
|
+
session_id: this.tracker.getSession(),
|
|
1720
|
+
duration: this.tracker.getSessionDuration()
|
|
1721
|
+
});
|
|
1722
|
+
this.tracker.stop();
|
|
1723
|
+
this.isInitialized = false;
|
|
1724
|
+
}
|
|
1725
|
+
};
|
|
1726
|
+
|
|
1727
|
+
// src/client.ts
|
|
1728
|
+
var NexusClient = class {
|
|
1729
|
+
// Make this public/accessible
|
|
1730
|
+
constructor(config) {
|
|
1731
|
+
const fullConfig = getFullConfig(config);
|
|
1732
|
+
this.config = {
|
|
1733
|
+
debug: config?.debug || false,
|
|
1734
|
+
cacheStrategy: config?.cacheStrategy || "memory",
|
|
1735
|
+
revalidateTime: config?.revalidateTime || 60,
|
|
1736
|
+
timeout: config?.timeout || 1e4,
|
|
1737
|
+
retries: config?.retries || 3,
|
|
1738
|
+
...fullConfig
|
|
1739
|
+
};
|
|
1740
|
+
this.content = new ContentEngine(this.config);
|
|
1741
|
+
if (typeof window !== "undefined") {
|
|
1742
|
+
this.analytics = new AnalyticsEngine(this.config);
|
|
1743
|
+
this.analytics.start();
|
|
1744
|
+
}
|
|
1745
|
+
const errors = validateConfig(this.config);
|
|
1746
|
+
if (errors.length > 0) {
|
|
1747
|
+
console.warn("\u26A0\uFE0F NexusHub: Configuration issues:", errors.join(", "));
|
|
1748
|
+
if (!this.config.projectId) {
|
|
1749
|
+
console.warn("\u26A0\uFE0F NexusHub: No Project ID found. Content fetching will fail.");
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
this.content = new ContentEngine(this.config);
|
|
1753
|
+
}
|
|
1754
|
+
// Helper alias for cleaner code
|
|
1755
|
+
getPage(slug, options) {
|
|
1756
|
+
return this.content.getPage(slug, options);
|
|
1757
|
+
}
|
|
1758
|
+
// Get current config (readonly)
|
|
1759
|
+
getConfig() {
|
|
1760
|
+
return { ...this.config };
|
|
1761
|
+
}
|
|
1762
|
+
};
|
|
1763
|
+
var nexus = new NexusClient();
|
|
1764
|
+
var createNexusClient = (config) => new NexusClient(config);
|
|
1765
|
+
var AuthContext = react.createContext(null);
|
|
1766
|
+
var AuthProvider = ({
|
|
1767
|
+
children,
|
|
1768
|
+
config
|
|
1769
|
+
}) => {
|
|
1770
|
+
const [user, setUser] = react.useState(null);
|
|
1771
|
+
const [isLoading, setIsLoading] = react.useState(true);
|
|
1772
|
+
const [error, setError] = react.useState(null);
|
|
1773
|
+
const AUTH_BASE = `${config.apiUrl}/auth/project/${config.projectId}`;
|
|
1774
|
+
const checkSession = react.useCallback(async () => {
|
|
1775
|
+
try {
|
|
1776
|
+
const res = await fetch(`${AUTH_BASE}/me`, {
|
|
1777
|
+
headers: getHeaders(config)
|
|
1778
|
+
});
|
|
1779
|
+
if (res.ok) {
|
|
1780
|
+
const data = await res.json();
|
|
1781
|
+
setUser(data.user);
|
|
1782
|
+
if (nexus.analytics) {
|
|
1783
|
+
nexus.analytics.identify(data.user.id, {
|
|
1784
|
+
email: data.user.email,
|
|
1785
|
+
role: data.user.role,
|
|
1786
|
+
...data.user.metadata
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
} else {
|
|
1790
|
+
setUser(null);
|
|
1791
|
+
}
|
|
1792
|
+
} catch (err) {
|
|
1793
|
+
console.debug("[NexusHub Auth] Session check failed:", err);
|
|
1794
|
+
setUser(null);
|
|
1795
|
+
} finally {
|
|
1796
|
+
setIsLoading(false);
|
|
1797
|
+
}
|
|
1798
|
+
}, [AUTH_BASE, config]);
|
|
1799
|
+
react.useEffect(() => {
|
|
1800
|
+
checkSession();
|
|
1801
|
+
}, [checkSession]);
|
|
1802
|
+
const login = async (creds) => {
|
|
1803
|
+
setIsLoading(true);
|
|
1804
|
+
setError(null);
|
|
1805
|
+
try {
|
|
1806
|
+
const res = await fetch(`${AUTH_BASE}/login`, {
|
|
1807
|
+
method: "POST",
|
|
1808
|
+
headers: getHeaders(config),
|
|
1809
|
+
body: JSON.stringify(creds)
|
|
1810
|
+
});
|
|
1811
|
+
if (!res.ok) throw await parseError(res);
|
|
1812
|
+
const data = await res.json();
|
|
1813
|
+
setUser(data.user);
|
|
1814
|
+
if (nexus.analytics) {
|
|
1815
|
+
await nexus.analytics.identify(data.user.id, {
|
|
1816
|
+
email: data.user.email,
|
|
1817
|
+
role: data.user.role,
|
|
1818
|
+
login_method: "email",
|
|
1819
|
+
...data.user.metadata
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
} catch (err) {
|
|
1823
|
+
setError(err);
|
|
1824
|
+
throw err;
|
|
1825
|
+
} finally {
|
|
1826
|
+
setIsLoading(false);
|
|
1827
|
+
}
|
|
1828
|
+
};
|
|
1829
|
+
const register = async (creds) => {
|
|
1830
|
+
setIsLoading(true);
|
|
1831
|
+
setError(null);
|
|
1832
|
+
try {
|
|
1833
|
+
const res = await fetch(`${AUTH_BASE}/register`, {
|
|
1834
|
+
method: "POST",
|
|
1835
|
+
headers: getHeaders(config),
|
|
1836
|
+
body: JSON.stringify(creds)
|
|
1837
|
+
});
|
|
1838
|
+
if (!res.ok) throw await parseError(res);
|
|
1839
|
+
const data = await res.json();
|
|
1840
|
+
setUser(data.user);
|
|
1841
|
+
if (nexus.analytics) {
|
|
1842
|
+
nexus.analytics.track("signup", { method: "email" });
|
|
1843
|
+
nexus.analytics.identify(data.user.id, {
|
|
1844
|
+
email: data.user.email,
|
|
1845
|
+
role: data.user.role,
|
|
1846
|
+
...data.user.metadata
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
} catch (err) {
|
|
1850
|
+
setError(err);
|
|
1851
|
+
throw err;
|
|
1852
|
+
} finally {
|
|
1853
|
+
setIsLoading(false);
|
|
1854
|
+
}
|
|
1855
|
+
};
|
|
1856
|
+
const logout = async () => {
|
|
1857
|
+
setIsLoading(true);
|
|
1858
|
+
try {
|
|
1859
|
+
await fetch(`${AUTH_BASE}/logout`, { method: "POST", headers: getHeaders(config) });
|
|
1860
|
+
} catch (e) {
|
|
1861
|
+
console.warn("Logout network error", e);
|
|
1862
|
+
} finally {
|
|
1863
|
+
setUser(null);
|
|
1864
|
+
setIsLoading(false);
|
|
1865
|
+
if (nexus.analytics) {
|
|
1866
|
+
nexus.analytics.track("logout");
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
};
|
|
1870
|
+
const updateProfile = async (updates) => {
|
|
1871
|
+
try {
|
|
1872
|
+
const res = await fetch(`${AUTH_BASE}/profile`, {
|
|
1873
|
+
method: "PATCH",
|
|
1874
|
+
headers: getHeaders(config),
|
|
1875
|
+
body: JSON.stringify(updates)
|
|
1876
|
+
});
|
|
1877
|
+
if (!res.ok) throw await parseError(res);
|
|
1878
|
+
const data = await res.json();
|
|
1879
|
+
setUser(data.user);
|
|
1880
|
+
if (nexus.analytics) {
|
|
1881
|
+
nexus.analytics.identify(data.user.id, updates);
|
|
1882
|
+
}
|
|
1883
|
+
} catch (err) {
|
|
1884
|
+
setError(err);
|
|
1885
|
+
throw err;
|
|
1886
|
+
}
|
|
1887
|
+
};
|
|
1888
|
+
const requestPasswordReset = async (email) => {
|
|
1889
|
+
const res = await fetch(`${AUTH_BASE}/password/reset-request`, {
|
|
1890
|
+
method: "POST",
|
|
1891
|
+
headers: getHeaders(config),
|
|
1892
|
+
body: JSON.stringify({ email })
|
|
1893
|
+
});
|
|
1894
|
+
if (!res.ok) throw await parseError(res);
|
|
1895
|
+
};
|
|
1896
|
+
return /* @__PURE__ */ jsxRuntime.jsx(AuthContext.Provider, { value: {
|
|
1897
|
+
user,
|
|
1898
|
+
isLoading,
|
|
1899
|
+
error,
|
|
1900
|
+
isAuthenticated: !!user,
|
|
1901
|
+
login,
|
|
1902
|
+
register,
|
|
1903
|
+
logout,
|
|
1904
|
+
updateProfile,
|
|
1905
|
+
requestPasswordReset
|
|
1906
|
+
}, children });
|
|
1907
|
+
};
|
|
1908
|
+
var useNexusAuth = () => {
|
|
1909
|
+
const context = react.useContext(AuthContext);
|
|
1910
|
+
if (!context) {
|
|
1911
|
+
throw new Error("useNexusAuth must be used within a NexusProvider");
|
|
1912
|
+
}
|
|
1913
|
+
return context;
|
|
1914
|
+
};
|
|
1915
|
+
function getHeaders(config) {
|
|
1916
|
+
return {
|
|
1917
|
+
"Content-Type": "application/json",
|
|
1918
|
+
"x-nexus-project": config.projectId
|
|
1919
|
+
// Note: We do NOT send the Master API Key here.
|
|
1920
|
+
// This is client-side. The backend uses Cookies or public tokens.
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
async function parseError(res) {
|
|
1924
|
+
try {
|
|
1925
|
+
const json = await res.json();
|
|
1926
|
+
return {
|
|
1927
|
+
status: res.status,
|
|
1928
|
+
code: json.code || "UNKNOWN_ERROR",
|
|
1929
|
+
message: json.message || "An error occurred during authentication"
|
|
1930
|
+
};
|
|
1931
|
+
} catch {
|
|
1932
|
+
return {
|
|
1933
|
+
status: res.status,
|
|
1934
|
+
code: "NETWORK_ERROR",
|
|
1935
|
+
message: res.statusText
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
var NexusContext = react.createContext(nexus);
|
|
1940
|
+
var NexusProvider = ({
|
|
1941
|
+
children,
|
|
1942
|
+
projectId,
|
|
1943
|
+
disableAnalytics = false
|
|
1944
|
+
}) => {
|
|
1945
|
+
const pathname = navigation.usePathname();
|
|
1946
|
+
const searchParams = navigation.useSearchParams();
|
|
1947
|
+
const isInitialized = react.useRef(false);
|
|
1948
|
+
const config = react.useMemo(() => {
|
|
1949
|
+
const currentConfig = nexus.getConfig();
|
|
1950
|
+
if (projectId && currentConfig.projectId !== projectId) {
|
|
1951
|
+
nexus.config.projectId = projectId;
|
|
1952
|
+
}
|
|
1953
|
+
return nexus.getConfig();
|
|
1954
|
+
}, [projectId]);
|
|
1955
|
+
react.useEffect(() => {
|
|
1956
|
+
if (typeof window === "undefined" || disableAnalytics) return;
|
|
1957
|
+
if (!isInitialized.current) {
|
|
1958
|
+
if (!nexus.analytics) {
|
|
1959
|
+
nexus.analytics = new AnalyticsEngine(nexus.getConfig());
|
|
1960
|
+
}
|
|
1961
|
+
nexus.analytics.start();
|
|
1962
|
+
isInitialized.current = true;
|
|
1963
|
+
if (nexus.getConfig().debug) {
|
|
1964
|
+
console.log("[NexusHub] \u{1F680} Provider initialized analytics");
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
return () => {
|
|
1968
|
+
if (nexus.analytics) {
|
|
1969
|
+
nexus.analytics.stop();
|
|
1970
|
+
isInitialized.current = false;
|
|
1971
|
+
}
|
|
1972
|
+
};
|
|
1973
|
+
}, [disableAnalytics]);
|
|
1974
|
+
react.useEffect(() => {
|
|
1975
|
+
if (nexus.analytics && !disableAnalytics) {
|
|
1976
|
+
nexus.analytics.pageView();
|
|
1977
|
+
}
|
|
1978
|
+
}, [pathname, searchParams, disableAnalytics]);
|
|
1979
|
+
return /* @__PURE__ */ jsxRuntime.jsx(NexusContext.Provider, { value: nexus, children: /* @__PURE__ */ jsxRuntime.jsx(AuthProvider, { config, children }) });
|
|
1980
|
+
};
|
|
1981
|
+
|
|
1982
|
+
// src/index.ts
|
|
1983
|
+
var VERSION = "0.0.1";
|
|
1984
|
+
|
|
1985
|
+
Object.defineProperty(exports, "LocalCache", {
|
|
1986
|
+
enumerable: true,
|
|
1987
|
+
get: function () { return chunk2JRYABGG_cjs.LocalCache; }
|
|
1988
|
+
});
|
|
1989
|
+
exports.AnalyticsEngine = AnalyticsEngine;
|
|
1990
|
+
exports.AuthProvider = AuthProvider;
|
|
1991
|
+
exports.BrowserCache = BrowserCache;
|
|
1992
|
+
exports.CacheTags = CacheTags;
|
|
1993
|
+
exports.ContentEngine = ContentEngine;
|
|
1994
|
+
exports.DEFAULT_ANALYTICS_URL = DEFAULT_ANALYTICS_URL;
|
|
1995
|
+
exports.DEFAULT_API_URL = DEFAULT_API_URL;
|
|
1996
|
+
exports.LOCAL_NEST_URL = LOCAL_NEST_URL;
|
|
1997
|
+
exports.LOCAL_RUST_URL = LOCAL_RUST_URL;
|
|
1998
|
+
exports.MemoryCache = MemoryCache;
|
|
1999
|
+
exports.NexusClient = NexusClient;
|
|
2000
|
+
exports.NexusProvider = NexusProvider;
|
|
2001
|
+
exports.VERSION = VERSION;
|
|
2002
|
+
exports.createNexusClient = createNexusClient;
|
|
2003
|
+
exports.getEnvConfig = getEnvConfig;
|
|
2004
|
+
exports.getFullConfig = getFullConfig;
|
|
2005
|
+
exports.mergeConfigs = mergeConfigs;
|
|
2006
|
+
exports.nexus = nexus;
|
|
2007
|
+
exports.useNexusAuth = useNexusAuth;
|
|
2008
|
+
exports.validateConfig = validateConfig;
|