@nexushub/client 0.9.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -1078
- package/dist/chunk-OP3LNZPC.cjs +116 -0
- package/dist/chunk-PB3YCBFH.js +116 -0
- package/dist/cli.cjs +259 -428
- package/dist/content/local-cache-server.cjs +36 -15
- package/dist/content/local-cache-server.d.cts +2 -5
- package/dist/content/local-cache-server.d.ts +2 -5
- package/dist/content/local-cache-server.js +36 -15
- package/dist/index.cjs +662 -341
- package/dist/index.d.cts +221 -32
- package/dist/index.d.ts +221 -32
- package/dist/index.js +652 -341
- package/dist/{local-cache-server-KUGMBMJP.cjs → local-cache-server-EKRKNL6N.cjs} +16 -8
- package/dist/{local-cache-server-RM7T456Z.js → local-cache-server-POO2S6OU.js} +15 -7
- package/dist/react.cjs +971 -374
- package/dist/react.d.cts +193 -76
- package/dist/react.d.ts +193 -76
- package/dist/react.js +1013 -416
- package/package.json +34 -28
- package/dist/chunk-3TDCUTZR.cjs +0 -53
- package/dist/chunk-VQ6IEN4I.js +0 -53
package/dist/index.cjs
CHANGED
|
@@ -31,27 +31,32 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
31
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
32
|
|
|
33
33
|
// src/config.ts
|
|
34
|
-
var DEFAULT_API_URL, DEFAULT_ANALYTICS_URL, LOCAL_NEST_URL, LOCAL_RUST_URL, getEnvConfig, mergeConfigs, getFullConfig, validateConfig;
|
|
34
|
+
var DEFAULT_API_URL, DEFAULT_ANALYTICS_URL, SDK_VERSION, LOCAL_NEST_URL, LOCAL_RUST_URL, getEnvConfig, mergeConfigs, getFullConfig, validateConfig, hasRequiredConfig;
|
|
35
35
|
var init_config = __esm({
|
|
36
36
|
"src/config.ts"() {
|
|
37
37
|
"use strict";
|
|
38
38
|
DEFAULT_API_URL = "https://api.gnapex.com";
|
|
39
39
|
DEFAULT_ANALYTICS_URL = "https://sentry.gnapex.com";
|
|
40
|
+
SDK_VERSION = "1.1.0";
|
|
40
41
|
LOCAL_NEST_URL = "https://api.gnapex.com";
|
|
41
42
|
LOCAL_RUST_URL = "https://sentry.gnapex.com";
|
|
42
43
|
getEnvConfig = () => {
|
|
43
|
-
const NEXT_PUBLIC_ID = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_NEXUS_ID : void 0;
|
|
44
|
-
const NEXT_PUBLIC_KEY = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_NEXUS_KEY : void 0;
|
|
45
|
-
const NEXT_PUBLIC_URL = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_NEXUS_API_URL : void 0;
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
44
|
+
const NEXT_PUBLIC_ID = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_GNAPEX_ID ?? process.env.NEXT_PUBLIC_NEXUS_ID : void 0;
|
|
45
|
+
const NEXT_PUBLIC_KEY = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_GNAPEX_KEY ?? process.env.NEXT_PUBLIC_NEXUS_KEY : void 0;
|
|
46
|
+
const NEXT_PUBLIC_URL = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_GNAPEX_API_URL ?? process.env.NEXT_PUBLIC_NEXUS_API_URL : void 0;
|
|
47
|
+
const NEXT_PUBLIC_ANALYTICS_URL = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_GNAPEX_ANALYTICS_URL ?? process.env.NEXT_PUBLIC_NEXUS_ANALYTICS_URL : void 0;
|
|
48
|
+
const NODE_ID = typeof process !== "undefined" ? process.env.GNAPEX_PROJECT_ID ?? process.env.NEXUS_PROJECT_ID : void 0;
|
|
49
|
+
const NODE_KEY = typeof process !== "undefined" ? process.env.GNAPEX_API_KEY ?? process.env.NEXUS_API_KEY : void 0;
|
|
50
|
+
const NODE_URL = typeof process !== "undefined" ? process.env.GNAPEX_API_URL ?? process.env.NEXUS_API_URL : void 0;
|
|
51
|
+
const NODE_ANALYTICS_URL = typeof process !== "undefined" ? process.env.GNAPEX_ANALYTICS_URL ?? process.env.NEXUS_ANALYTICS_URL : void 0;
|
|
49
52
|
const NODE_ENV = typeof process !== "undefined" ? process.env.NODE_ENV : "production";
|
|
50
53
|
const isDev = NODE_ENV === "development" || typeof window !== "undefined" && window.location.hostname === "localhost";
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const
|
|
54
|
+
const runtime = typeof window !== "undefined" ? window.__GNAPEX__ ?? window.__NEXUS__ : void 0;
|
|
55
|
+
const meta = typeof document !== "undefined" ? (name) => document.querySelector(`meta[name="${name}"]`)?.getAttribute("content") ?? void 0 : (_name) => void 0;
|
|
56
|
+
const projectId = runtime?.projectId ?? meta("gnapex-project") ?? meta("nexus-project") ?? NEXT_PUBLIC_ID ?? NODE_ID;
|
|
57
|
+
const apiKey = runtime?.apiKey ?? meta("gnapex-key") ?? meta("nexus-key") ?? NEXT_PUBLIC_KEY ?? NODE_KEY;
|
|
58
|
+
const apiUrl = runtime?.apiUrl ?? meta("gnapex-api") ?? meta("nexus-api") ?? NEXT_PUBLIC_URL ?? NODE_URL ?? (isDev ? LOCAL_NEST_URL : DEFAULT_API_URL);
|
|
59
|
+
const analyticsUrl = runtime?.analyticsUrl ?? meta("gnapex-analytics") ?? meta("nexus-analytics") ?? NEXT_PUBLIC_ANALYTICS_URL ?? NODE_ANALYTICS_URL ?? (apiUrl.includes("localhost") ? LOCAL_RUST_URL : DEFAULT_ANALYTICS_URL);
|
|
55
60
|
return {
|
|
56
61
|
projectId,
|
|
57
62
|
apiKey,
|
|
@@ -79,8 +84,12 @@ var init_config = __esm({
|
|
|
79
84
|
const errors = [];
|
|
80
85
|
if (!config.projectId) errors.push("Missing projectId");
|
|
81
86
|
if (!config.apiUrl) errors.push("Missing apiUrl");
|
|
87
|
+
else if (!/^https?:\/\//.test(config.apiUrl)) {
|
|
88
|
+
errors.push("apiUrl must be a valid URL starting with http:// or https://");
|
|
89
|
+
}
|
|
82
90
|
return errors;
|
|
83
91
|
};
|
|
92
|
+
hasRequiredConfig = (config) => validateConfig(config).length === 0;
|
|
84
93
|
}
|
|
85
94
|
});
|
|
86
95
|
|
|
@@ -188,6 +197,61 @@ var init_binary_compiler = __esm({
|
|
|
188
197
|
}
|
|
189
198
|
});
|
|
190
199
|
|
|
200
|
+
// src/content/utils.ts
|
|
201
|
+
function validateSlug(slug) {
|
|
202
|
+
if (!slug || typeof slug !== "string") {
|
|
203
|
+
throw new Error("Slug must be a non-empty string");
|
|
204
|
+
}
|
|
205
|
+
if (!/^[a-z0-9-_]+$/.test(slug)) {
|
|
206
|
+
throw new Error("Slug can only contain lowercase letters, numbers, hyphens, and underscores");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function normalizeQuery(query) {
|
|
210
|
+
const normalized = { ...query };
|
|
211
|
+
normalized.page = Math.max(1, normalized.page || 1);
|
|
212
|
+
normalized.limit = Math.min(100, Math.max(1, normalized.limit || 10));
|
|
213
|
+
normalized.order = normalized.order || "desc";
|
|
214
|
+
if (normalized.page < 1) {
|
|
215
|
+
throw new Error("Page must be greater than 0");
|
|
216
|
+
}
|
|
217
|
+
if (normalized.limit < 1 || normalized.limit > 100) {
|
|
218
|
+
throw new Error("Limit must be between 1 and 100");
|
|
219
|
+
}
|
|
220
|
+
return normalized;
|
|
221
|
+
}
|
|
222
|
+
function buildQueryString(query) {
|
|
223
|
+
const params = new URLSearchParams();
|
|
224
|
+
if (query.page) params.append("page", query.page.toString());
|
|
225
|
+
if (query.limit) params.append("limit", query.limit.toString());
|
|
226
|
+
if (query.sort) params.append("sort", query.sort);
|
|
227
|
+
if (query.order) params.append("order", query.order);
|
|
228
|
+
if (query.search) params.append("search", query.search);
|
|
229
|
+
if (query.include?.length) {
|
|
230
|
+
params.append("include", query.include.join(","));
|
|
231
|
+
}
|
|
232
|
+
if (query.fields?.length) {
|
|
233
|
+
params.append("fields", query.fields.join(","));
|
|
234
|
+
}
|
|
235
|
+
if (query.filter) {
|
|
236
|
+
params.append("filter", JSON.stringify(query.filter));
|
|
237
|
+
}
|
|
238
|
+
return params.toString();
|
|
239
|
+
}
|
|
240
|
+
function measurePerformance(name, fn) {
|
|
241
|
+
const start = performance.now();
|
|
242
|
+
const result = fn();
|
|
243
|
+
const end = performance.now();
|
|
244
|
+
if (process.env.NODE_ENV === "development") {
|
|
245
|
+
console.log(`\u23F1\uFE0F ${name}: ${(end - start).toFixed(2)}ms`);
|
|
246
|
+
}
|
|
247
|
+
return { result, duration: end - start };
|
|
248
|
+
}
|
|
249
|
+
var init_utils = __esm({
|
|
250
|
+
"src/content/utils.ts"() {
|
|
251
|
+
"use strict";
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
191
255
|
// src/content/local-cache-server.ts
|
|
192
256
|
var local_cache_server_exports = {};
|
|
193
257
|
__export(local_cache_server_exports, {
|
|
@@ -201,6 +265,7 @@ var init_local_cache_server = __esm({
|
|
|
201
265
|
import_path = __toESM(require("path"), 1);
|
|
202
266
|
init_config();
|
|
203
267
|
init_binary_compiler();
|
|
268
|
+
init_utils();
|
|
204
269
|
LocalCache = class {
|
|
205
270
|
constructor(customPath, apiKey) {
|
|
206
271
|
this.apiKey = "";
|
|
@@ -211,9 +276,6 @@ var init_local_cache_server = __esm({
|
|
|
211
276
|
isLoaded() {
|
|
212
277
|
return import_fs.default.existsSync(import_path.default.resolve(process.cwd(), this.baseDir));
|
|
213
278
|
}
|
|
214
|
-
/**
|
|
215
|
-
* Helper to decrypt and decompile `.nx` files on-the-fly.
|
|
216
|
-
*/
|
|
217
279
|
decompileFile(filePath) {
|
|
218
280
|
if (!this.apiKey) {
|
|
219
281
|
if (process.env.NODE_ENV === "development") {
|
|
@@ -242,9 +304,14 @@ var init_local_cache_server = __esm({
|
|
|
242
304
|
}
|
|
243
305
|
}
|
|
244
306
|
/**
|
|
245
|
-
* Retrieve a specific page
|
|
307
|
+
* Retrieve a specific page safely from its .nx file.
|
|
246
308
|
*/
|
|
247
309
|
async getPage(slug) {
|
|
310
|
+
try {
|
|
311
|
+
validateSlug(slug);
|
|
312
|
+
} catch {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
248
315
|
const filePath = import_path.default.resolve(
|
|
249
316
|
process.cwd(),
|
|
250
317
|
this.baseDir,
|
|
@@ -263,9 +330,14 @@ var init_local_cache_server = __esm({
|
|
|
263
330
|
return null;
|
|
264
331
|
}
|
|
265
332
|
/**
|
|
266
|
-
* Retrieve an entire collection from its specific .nx file.
|
|
333
|
+
* Retrieve an entire collection safely from its specific .nx file.
|
|
267
334
|
*/
|
|
268
335
|
async getCollection(collectionId) {
|
|
336
|
+
try {
|
|
337
|
+
validateSlug(collectionId);
|
|
338
|
+
} catch {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
269
341
|
const filePath = import_path.default.resolve(
|
|
270
342
|
process.cwd(),
|
|
271
343
|
this.baseDir,
|
|
@@ -398,15 +470,25 @@ __export(index_exports, {
|
|
|
398
470
|
ContentEngine: () => ContentEngine,
|
|
399
471
|
DEFAULT_ANALYTICS_URL: () => DEFAULT_ANALYTICS_URL,
|
|
400
472
|
DEFAULT_API_URL: () => DEFAULT_API_URL,
|
|
473
|
+
FeatureFlags: () => FeatureFlags,
|
|
401
474
|
LOCAL_NEST_URL: () => LOCAL_NEST_URL,
|
|
402
475
|
LOCAL_RUST_URL: () => LOCAL_RUST_URL,
|
|
403
476
|
LocalCache: () => LocalCacheProxy,
|
|
404
477
|
MemoryCache: () => MemoryCache,
|
|
405
478
|
NexusClient: () => NexusClient,
|
|
479
|
+
NexusError: () => NexusError,
|
|
480
|
+
NexusEventBus: () => NexusEventBus,
|
|
481
|
+
NexusHttpClient: () => NexusHttpClient,
|
|
482
|
+
NexusPushClient: () => NexusPushClient,
|
|
483
|
+
RemoteConfig: () => RemoteConfig,
|
|
484
|
+
SDK_VERSION: () => SDK_VERSION,
|
|
406
485
|
VERSION: () => VERSION,
|
|
407
486
|
createNexusClient: () => createNexusClient,
|
|
487
|
+
getDiagnostics: () => getDiagnostics,
|
|
408
488
|
getEnvConfig: () => getEnvConfig,
|
|
409
489
|
getFullConfig: () => getFullConfig,
|
|
490
|
+
hasRequiredConfig: () => hasRequiredConfig,
|
|
491
|
+
isNexusError: () => isNexusError,
|
|
410
492
|
mergeConfigs: () => mergeConfigs,
|
|
411
493
|
nexus: () => nexus,
|
|
412
494
|
validateConfig: () => validateConfig
|
|
@@ -715,90 +797,69 @@ var LocalCacheProxy = class {
|
|
|
715
797
|
// src/content/strategies.ts
|
|
716
798
|
var RateLimiter = class {
|
|
717
799
|
constructor(config) {
|
|
718
|
-
this.requests = [];
|
|
719
800
|
this.config = config;
|
|
801
|
+
this.requests = [];
|
|
720
802
|
}
|
|
721
803
|
async checkLimit() {
|
|
722
804
|
while (true) {
|
|
723
805
|
const now = Date.now();
|
|
724
|
-
const
|
|
725
|
-
this.requests = this.requests.filter((
|
|
806
|
+
const cutoff = now - this.config.timeWindow;
|
|
807
|
+
this.requests = this.requests.filter((t) => t > cutoff);
|
|
726
808
|
if (this.requests.length < this.config.maxRequests) {
|
|
727
809
|
this.requests.push(now);
|
|
728
810
|
return;
|
|
729
811
|
}
|
|
730
|
-
|
|
731
|
-
const waitTime = oldestRequest + this.config.timeWindow - now;
|
|
732
|
-
if (waitTime > 0) {
|
|
733
|
-
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
734
|
-
} else {
|
|
735
|
-
}
|
|
812
|
+
await new Promise((r) => setTimeout(r, Math.max(1, this.requests[0] + this.config.timeWindow - now)));
|
|
736
813
|
}
|
|
737
814
|
}
|
|
738
815
|
getStats() {
|
|
739
816
|
const now = Date.now();
|
|
740
|
-
|
|
741
|
-
const currentRequests = this.requests.filter(
|
|
742
|
-
(time) => time > windowStart
|
|
743
|
-
).length;
|
|
744
|
-
return { currentRequests, limit: this.config.maxRequests };
|
|
817
|
+
return { currentRequests: this.requests.filter((t) => t > now - this.config.timeWindow).length, limit: this.config.maxRequests };
|
|
745
818
|
}
|
|
746
819
|
};
|
|
747
820
|
var ExponentialBackoff = class {
|
|
748
821
|
constructor(config) {
|
|
822
|
+
this.config = config;
|
|
749
823
|
this.config = { jitter: true, ...config };
|
|
750
824
|
}
|
|
751
825
|
async execute(fn, onRetry) {
|
|
752
|
-
let
|
|
753
|
-
for (let
|
|
826
|
+
let last;
|
|
827
|
+
for (let a = 0; a <= this.config.maxRetries; a++) {
|
|
754
828
|
try {
|
|
755
829
|
return await fn();
|
|
756
|
-
} catch (
|
|
757
|
-
|
|
758
|
-
if (
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
if (
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
830
|
+
} catch (e) {
|
|
831
|
+
last = e;
|
|
832
|
+
if (e?.code === "ABORTED" || e?.code === "VALIDATION_ERROR" || e?.status >= 400 && e?.status < 500 && e?.status !== 429) throw e;
|
|
833
|
+
if (a === this.config.maxRetries) break;
|
|
834
|
+
let delay = Math.min(this.config.maxDelay, this.config.baseDelay * 2 ** a);
|
|
835
|
+
if (this.config.jitter) delay *= 0.5 + Math.random();
|
|
836
|
+
onRetry?.(a + 1, delay, e);
|
|
837
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
765
838
|
}
|
|
766
839
|
}
|
|
767
|
-
throw
|
|
768
|
-
}
|
|
769
|
-
calculateDelay(attempt) {
|
|
770
|
-
let delay = this.config.baseDelay * Math.pow(2, attempt);
|
|
771
|
-
delay = Math.min(delay, this.config.maxDelay);
|
|
772
|
-
if (this.config.jitter) {
|
|
773
|
-
delay = delay * (0.5 + Math.random());
|
|
774
|
-
}
|
|
775
|
-
return delay;
|
|
776
|
-
}
|
|
777
|
-
isClientError(error) {
|
|
778
|
-
return error?.status >= 400 && error?.status < 500;
|
|
779
|
-
}
|
|
780
|
-
isRateLimitError(error) {
|
|
781
|
-
return error?.status === 429;
|
|
840
|
+
throw last;
|
|
782
841
|
}
|
|
783
842
|
};
|
|
843
|
+
var CircuitState = /* @__PURE__ */ ((CircuitState2) => {
|
|
844
|
+
CircuitState2[CircuitState2["CLOSED"] = 0] = "CLOSED";
|
|
845
|
+
CircuitState2[CircuitState2["OPEN"] = 1] = "OPEN";
|
|
846
|
+
CircuitState2[CircuitState2["HALF_OPEN"] = 2] = "HALF_OPEN";
|
|
847
|
+
return CircuitState2;
|
|
848
|
+
})(CircuitState || {});
|
|
784
849
|
var CircuitBreaker = class {
|
|
785
|
-
constructor() {
|
|
850
|
+
constructor(threshold = 5, reset = 3e4) {
|
|
851
|
+
this.threshold = threshold;
|
|
852
|
+
this.reset = reset;
|
|
786
853
|
this.state = 0 /* CLOSED */;
|
|
787
854
|
this.failures = 0;
|
|
788
|
-
this.
|
|
789
|
-
this.failureThreshold = 5;
|
|
790
|
-
this.resetTimeout = 3e4;
|
|
855
|
+
this.lastFailure = 0;
|
|
791
856
|
}
|
|
792
|
-
// 30 seconds
|
|
793
857
|
isOpen() {
|
|
794
|
-
if (this.state === 1 /* OPEN */) {
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
return false;
|
|
798
|
-
}
|
|
799
|
-
return true;
|
|
858
|
+
if (this.state === 1 /* OPEN */ && Date.now() - this.lastFailure >= this.reset) {
|
|
859
|
+
this.state = 2 /* HALF_OPEN */;
|
|
860
|
+
return false;
|
|
800
861
|
}
|
|
801
|
-
return
|
|
862
|
+
return this.state === 1 /* OPEN */;
|
|
802
863
|
}
|
|
803
864
|
recordSuccess() {
|
|
804
865
|
this.failures = 0;
|
|
@@ -806,111 +867,35 @@ var CircuitBreaker = class {
|
|
|
806
867
|
}
|
|
807
868
|
recordFailure() {
|
|
808
869
|
this.failures++;
|
|
809
|
-
this.
|
|
810
|
-
if (this.failures >= this.
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
"[NexusHub] \u{1F50C} Circuit Breaker OPEN. Pausing network requests."
|
|
815
|
-
);
|
|
816
|
-
}
|
|
817
|
-
}
|
|
870
|
+
this.lastFailure = Date.now();
|
|
871
|
+
if (this.failures >= this.threshold) this.state = 1 /* OPEN */;
|
|
872
|
+
}
|
|
873
|
+
getState() {
|
|
874
|
+
return CircuitState[this.state];
|
|
818
875
|
}
|
|
819
876
|
};
|
|
820
877
|
var RequestBatcher = class {
|
|
821
|
-
constructor(
|
|
822
|
-
this.
|
|
878
|
+
constructor(windowMs = 10, maxBatchSize = 50) {
|
|
879
|
+
this.windowMs = windowMs;
|
|
823
880
|
this.maxBatchSize = maxBatchSize;
|
|
824
|
-
this.
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
} else if (!this.batchTimeout) {
|
|
833
|
-
this.batchTimeout = setTimeout(
|
|
834
|
-
() => this.processBatch(request),
|
|
835
|
-
this.batchWindow
|
|
836
|
-
);
|
|
837
|
-
}
|
|
881
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
882
|
+
}
|
|
883
|
+
schedule(key, request) {
|
|
884
|
+
const existing = this.pending.get(key);
|
|
885
|
+
if (existing) return existing;
|
|
886
|
+
const promise = new Promise((resolve, reject) => setTimeout(() => request().then(resolve, reject), this.windowMs));
|
|
887
|
+
this.pending.set(key, promise);
|
|
888
|
+
promise.finally(() => this.pending.delete(key)).catch(() => {
|
|
838
889
|
});
|
|
890
|
+
return promise;
|
|
839
891
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
this.processing = true;
|
|
843
|
-
if (this.batchTimeout) {
|
|
844
|
-
clearTimeout(this.batchTimeout);
|
|
845
|
-
this.batchTimeout = void 0;
|
|
846
|
-
}
|
|
847
|
-
const currentBatch = [...this.batch];
|
|
848
|
-
this.batch = [];
|
|
849
|
-
try {
|
|
850
|
-
const result = await request();
|
|
851
|
-
currentBatch.forEach((item) => item.resolve(result));
|
|
852
|
-
} catch (error) {
|
|
853
|
-
currentBatch.forEach((item) => item.reject(error));
|
|
854
|
-
} finally {
|
|
855
|
-
this.processing = false;
|
|
856
|
-
if (this.batch.length > 0) {
|
|
857
|
-
setTimeout(() => this.processBatch(request), 0);
|
|
858
|
-
}
|
|
859
|
-
}
|
|
892
|
+
clear() {
|
|
893
|
+
this.pending.clear();
|
|
860
894
|
}
|
|
861
895
|
};
|
|
862
896
|
|
|
863
|
-
// src/content/utils.ts
|
|
864
|
-
function validateSlug(slug) {
|
|
865
|
-
if (!slug || typeof slug !== "string") {
|
|
866
|
-
throw new Error("Slug must be a non-empty string");
|
|
867
|
-
}
|
|
868
|
-
if (!/^[a-z0-9-_]+$/.test(slug)) {
|
|
869
|
-
throw new Error("Slug can only contain lowercase letters, numbers, hyphens, and underscores");
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
function normalizeQuery(query) {
|
|
873
|
-
const normalized = { ...query };
|
|
874
|
-
normalized.page = Math.max(1, normalized.page || 1);
|
|
875
|
-
normalized.limit = Math.min(100, Math.max(1, normalized.limit || 10));
|
|
876
|
-
normalized.order = normalized.order || "desc";
|
|
877
|
-
if (normalized.page < 1) {
|
|
878
|
-
throw new Error("Page must be greater than 0");
|
|
879
|
-
}
|
|
880
|
-
if (normalized.limit < 1 || normalized.limit > 100) {
|
|
881
|
-
throw new Error("Limit must be between 1 and 100");
|
|
882
|
-
}
|
|
883
|
-
return normalized;
|
|
884
|
-
}
|
|
885
|
-
function buildQueryString(query) {
|
|
886
|
-
const params = new URLSearchParams();
|
|
887
|
-
if (query.page) params.append("page", query.page.toString());
|
|
888
|
-
if (query.limit) params.append("limit", query.limit.toString());
|
|
889
|
-
if (query.sort) params.append("sort", query.sort);
|
|
890
|
-
if (query.order) params.append("order", query.order);
|
|
891
|
-
if (query.search) params.append("search", query.search);
|
|
892
|
-
if (query.include?.length) {
|
|
893
|
-
params.append("include", query.include.join(","));
|
|
894
|
-
}
|
|
895
|
-
if (query.fields?.length) {
|
|
896
|
-
params.append("fields", query.fields.join(","));
|
|
897
|
-
}
|
|
898
|
-
if (query.filter) {
|
|
899
|
-
params.append("filter", JSON.stringify(query.filter));
|
|
900
|
-
}
|
|
901
|
-
return params.toString();
|
|
902
|
-
}
|
|
903
|
-
function measurePerformance(name, fn) {
|
|
904
|
-
const start = performance.now();
|
|
905
|
-
const result = fn();
|
|
906
|
-
const end = performance.now();
|
|
907
|
-
if (process.env.NODE_ENV === "development") {
|
|
908
|
-
console.log(`\u23F1\uFE0F ${name}: ${(end - start).toFixed(2)}ms`);
|
|
909
|
-
}
|
|
910
|
-
return { result, duration: end - start };
|
|
911
|
-
}
|
|
912
|
-
|
|
913
897
|
// src/content/index.ts
|
|
898
|
+
init_utils();
|
|
914
899
|
var ContentEngine = class {
|
|
915
900
|
constructor(config) {
|
|
916
901
|
this.config = config;
|
|
@@ -942,6 +927,12 @@ var ContentEngine = class {
|
|
|
942
927
|
window.addEventListener("beforeunload", this.cleanup.bind(this));
|
|
943
928
|
}
|
|
944
929
|
}
|
|
930
|
+
/** Update runtime configuration without exposing internal mutation. */
|
|
931
|
+
updateConfig(config) {
|
|
932
|
+
this.config = config;
|
|
933
|
+
this.defaultRevalidate = config.revalidateTime ?? false;
|
|
934
|
+
this.cacheStrategy = config.cacheStrategy || "memory";
|
|
935
|
+
}
|
|
945
936
|
/**
|
|
946
937
|
* Fetch a Single Page with full strategy pipeline
|
|
947
938
|
*/
|
|
@@ -1100,7 +1091,7 @@ var ContentEngine = class {
|
|
|
1100
1091
|
}
|
|
1101
1092
|
}
|
|
1102
1093
|
/**
|
|
1103
|
-
* Fetch Global Settings
|
|
1094
|
+
* Fetch Global Settings
|
|
1104
1095
|
*/
|
|
1105
1096
|
async getGlobals(options = {}) {
|
|
1106
1097
|
const {
|
|
@@ -1152,6 +1143,9 @@ var ContentEngine = class {
|
|
|
1152
1143
|
revalidate
|
|
1153
1144
|
});
|
|
1154
1145
|
if (!res.ok) {
|
|
1146
|
+
if (res.status === 408) {
|
|
1147
|
+
throw new Error("Request timeout");
|
|
1148
|
+
}
|
|
1155
1149
|
throw new Error(`API Error ${res.status}: ${res.statusText}`);
|
|
1156
1150
|
}
|
|
1157
1151
|
const json = await res.json();
|
|
@@ -1200,12 +1194,12 @@ var ContentEngine = class {
|
|
|
1200
1194
|
method: "GET",
|
|
1201
1195
|
headers: this.getHeaders(),
|
|
1202
1196
|
tags: fetchTags,
|
|
1203
|
-
// ?? not || — see the constructor comment on defaultRevalidate for
|
|
1204
|
-
// why: an explicit `revalidate: 0` on this call must not be
|
|
1205
|
-
// discarded in favor of the engine's default.
|
|
1206
1197
|
revalidate: options.revalidate ?? this.defaultRevalidate
|
|
1207
1198
|
});
|
|
1208
1199
|
if (!res.ok) {
|
|
1200
|
+
if (res.status === 408) {
|
|
1201
|
+
throw new Error("Request timeout");
|
|
1202
|
+
}
|
|
1209
1203
|
throw new Error(`API Error ${res.status}: ${res.statusText}`);
|
|
1210
1204
|
}
|
|
1211
1205
|
const json = await res.json();
|
|
@@ -1237,12 +1231,15 @@ var ContentEngine = class {
|
|
|
1237
1231
|
headers: this.getHeaders()
|
|
1238
1232
|
});
|
|
1239
1233
|
if (!res.ok) {
|
|
1234
|
+
if (res.status === 408) {
|
|
1235
|
+
throw new Error("Request timeout");
|
|
1236
|
+
}
|
|
1240
1237
|
throw new Error(`API Error ${res.status}: ${res.statusText}`);
|
|
1241
1238
|
}
|
|
1242
1239
|
return await res.json();
|
|
1243
1240
|
}
|
|
1244
1241
|
/**
|
|
1245
|
-
* Prefetch content
|
|
1242
|
+
* Prefetch content
|
|
1246
1243
|
*/
|
|
1247
1244
|
async prefetch(urls) {
|
|
1248
1245
|
if (typeof window !== "undefined" && "requestIdleCallback" in window) {
|
|
@@ -1255,41 +1252,19 @@ var ContentEngine = class {
|
|
|
1255
1252
|
}
|
|
1256
1253
|
/**
|
|
1257
1254
|
* Robust Real-time Subscriptions (SSE) with Auto-Reconnect
|
|
1258
|
-
*
|
|
1259
|
-
* ⚠️ SCOPE — READ BEFORE RELYING ON THIS FOR "instant" UPDATES:
|
|
1260
|
-
* This connects from the BROWSER TAB it's called in, and on message it
|
|
1261
|
-
* only clears THIS SDK INSTANCE's in-memory `memoryCache` (via
|
|
1262
|
-
* invalidateCache below) in THAT browser tab's JS heap. In a typical
|
|
1263
|
-
* Next.js deployment — and Cloudflare specifically, which is stateless
|
|
1264
|
-
* per-request at the edge — that is a different process/isolate than the
|
|
1265
|
-
* one that will render the NEXT server request for this content. So:
|
|
1266
|
-
* - ✅ Useful for: a client component that reads from `nexus.content`
|
|
1267
|
-
* directly in the browser and re-renders in place without a page
|
|
1268
|
-
* navigation (e.g. a live-updating dashboard widget).
|
|
1269
|
-
* - ❌ NOT sufficient for: "user A edits content in the CMS, user B's
|
|
1270
|
-
* already-loaded page updates" via a server-rendered page. That
|
|
1271
|
-
* requires the backend's /api/revalidate webhook (see
|
|
1272
|
-
* content.service.ts `pingNextRevalidateWebhook`) to have actually
|
|
1273
|
-
* cleared the *server's* Data Cache, so the NEXT navigation or
|
|
1274
|
-
* server request picks up fresh data. This SSE channel does not
|
|
1275
|
-
* replace that — it's a complementary, browser-local optimization.
|
|
1276
|
-
* If your symptom was "stale content after editing," fix the webhook
|
|
1277
|
-
* wiring first; treat this method as an enhancement layered on top.
|
|
1278
1255
|
*/
|
|
1279
1256
|
subscribeToUpdates(callback) {
|
|
1280
1257
|
if (this.isServer || typeof EventSource === "undefined") {
|
|
1281
1258
|
if (this.config.debug) {
|
|
1282
1259
|
console.warn(
|
|
1283
|
-
"[NexusHub] subscribeToUpdates() called in a non-browser environment
|
|
1260
|
+
"[NexusHub] subscribeToUpdates() called in a non-browser environment."
|
|
1284
1261
|
);
|
|
1285
1262
|
}
|
|
1286
1263
|
return () => {
|
|
1287
1264
|
};
|
|
1288
1265
|
}
|
|
1289
1266
|
if (!this.config.apiKey) {
|
|
1290
|
-
console.warn(
|
|
1291
|
-
"[NexusHub] subscribeToUpdates(): no apiKey configured, the SSE connection will likely be rejected by the backend. Set NEXT_PUBLIC_NEXUS_KEY."
|
|
1292
|
-
);
|
|
1267
|
+
console.warn("[NexusHub] subscribeToUpdates(): no apiKey configured.");
|
|
1293
1268
|
}
|
|
1294
1269
|
let eventSource = null;
|
|
1295
1270
|
let retryCount = 0;
|
|
@@ -1453,6 +1428,9 @@ var ContentEngine = class {
|
|
|
1453
1428
|
`Page '${slug}' not found. Check your Dashboard or Seed data.`
|
|
1454
1429
|
);
|
|
1455
1430
|
}
|
|
1431
|
+
if (res.status === 408) {
|
|
1432
|
+
throw new Error("Request timeout");
|
|
1433
|
+
}
|
|
1456
1434
|
throw new Error(`API Error ${res.status}: ${res.statusText}`);
|
|
1457
1435
|
}
|
|
1458
1436
|
const json = await res.json();
|
|
@@ -1487,6 +1465,9 @@ var ContentEngine = class {
|
|
|
1487
1465
|
revalidate
|
|
1488
1466
|
});
|
|
1489
1467
|
if (!res.ok) {
|
|
1468
|
+
if (res.status === 408) {
|
|
1469
|
+
throw new Error("Request timeout");
|
|
1470
|
+
}
|
|
1490
1471
|
throw new Error(`API Error ${res.status}: ${res.statusText}`);
|
|
1491
1472
|
}
|
|
1492
1473
|
const json = await res.json();
|
|
@@ -1573,7 +1554,6 @@ var ContentEngine = class {
|
|
|
1573
1554
|
const response = await fetch(url, {
|
|
1574
1555
|
...fetchOptions,
|
|
1575
1556
|
...nextConfig,
|
|
1576
|
-
// Inject Next.js tags
|
|
1577
1557
|
signal: controller.signal
|
|
1578
1558
|
});
|
|
1579
1559
|
clearTimeout(id);
|
|
@@ -1586,7 +1566,8 @@ var ContentEngine = class {
|
|
|
1586
1566
|
getHeaders() {
|
|
1587
1567
|
const headers = {
|
|
1588
1568
|
"Content-Type": "application/json",
|
|
1589
|
-
"X-Nexus-Client":
|
|
1569
|
+
"X-Nexus-Client": `client-sdk/${this.config.sdkVersion ?? "1.1.0"}`,
|
|
1570
|
+
"X-Nexus-Request-ID": typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `nx_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
|
1590
1571
|
"X-Nexus-Project": this.config.projectId
|
|
1591
1572
|
};
|
|
1592
1573
|
if (this.config.apiKey) {
|
|
@@ -1597,13 +1578,22 @@ var ContentEngine = class {
|
|
|
1597
1578
|
isCacheValid(metadata) {
|
|
1598
1579
|
return Date.now() < metadata.expiresAt;
|
|
1599
1580
|
}
|
|
1581
|
+
/**
|
|
1582
|
+
* 🛡️ FIX: Normalizes timeout errors (AbortError, 408, or Timeout messages)
|
|
1583
|
+
* into clean, standardized "Request timeout" errors.
|
|
1584
|
+
*/
|
|
1600
1585
|
normalizeError(error, context) {
|
|
1601
1586
|
if (error instanceof Error) {
|
|
1602
|
-
|
|
1587
|
+
const msg = error.message.toLowerCase();
|
|
1588
|
+
if (error.name === "AbortError" || error.message.includes("408") || msg.includes("timeout") || msg.includes("aborted")) {
|
|
1603
1589
|
return new Error(`${context}: Request timeout`);
|
|
1604
1590
|
}
|
|
1605
1591
|
return error;
|
|
1606
1592
|
}
|
|
1593
|
+
const str = String(error).toLowerCase();
|
|
1594
|
+
if (str.includes("408") || str.includes("timeout") || str.includes("aborted")) {
|
|
1595
|
+
return new Error(`${context}: Request timeout`);
|
|
1596
|
+
}
|
|
1607
1597
|
return new Error(`${context}: ${String(error)}`);
|
|
1608
1598
|
}
|
|
1609
1599
|
cancelRequests() {
|
|
@@ -1620,9 +1610,12 @@ var ContentEngine = class {
|
|
|
1620
1610
|
}
|
|
1621
1611
|
};
|
|
1622
1612
|
|
|
1613
|
+
// src/analytics/tracker.ts
|
|
1614
|
+
init_config();
|
|
1615
|
+
|
|
1623
1616
|
// src/analytics/fingerprint.ts
|
|
1624
1617
|
var cachedEntropy = null;
|
|
1625
|
-
var getDeviceEntropy = async () => {
|
|
1618
|
+
var getDeviceEntropy = async (allowFingerprinting = true) => {
|
|
1626
1619
|
if (cachedEntropy) return cachedEntropy;
|
|
1627
1620
|
if (typeof window === "undefined") return {};
|
|
1628
1621
|
const nav = window.navigator;
|
|
@@ -1637,7 +1630,7 @@ var getDeviceEntropy = async () => {
|
|
|
1637
1630
|
platform: nav.platform,
|
|
1638
1631
|
language: nav.language,
|
|
1639
1632
|
touch_support: "ontouchstart" in window || nav.maxTouchPoints > 0,
|
|
1640
|
-
canvas_hash: await generateCanvasHash()
|
|
1633
|
+
canvas_hash: allowFingerprinting ? await generateCanvasHash() : void 0
|
|
1641
1634
|
};
|
|
1642
1635
|
return cachedEntropy;
|
|
1643
1636
|
};
|
|
@@ -1650,7 +1643,6 @@ var generateCanvasHash = async () => {
|
|
|
1650
1643
|
canvas.height = 50;
|
|
1651
1644
|
ctx.textBaseline = "top";
|
|
1652
1645
|
ctx.font = '16px "Arial"';
|
|
1653
|
-
ctx.textBaseline = "alphabetic";
|
|
1654
1646
|
ctx.fillStyle = "#f60";
|
|
1655
1647
|
ctx.fillRect(125, 1, 62, 20);
|
|
1656
1648
|
ctx.fillStyle = "#069";
|
|
@@ -1669,12 +1661,12 @@ var generateCanvasHash = async () => {
|
|
|
1669
1661
|
return "";
|
|
1670
1662
|
}
|
|
1671
1663
|
};
|
|
1672
|
-
var getVisitorId = async () => {
|
|
1664
|
+
var getVisitorId = async (allowFingerprinting = true) => {
|
|
1673
1665
|
if (typeof window === "undefined") return "server_visitor";
|
|
1674
1666
|
const STORAGE_KEY = "nexus_vid";
|
|
1675
1667
|
let vid = localStorage.getItem(STORAGE_KEY);
|
|
1676
1668
|
if (!vid) {
|
|
1677
|
-
const entropy = await getDeviceEntropy();
|
|
1669
|
+
const entropy = await getDeviceEntropy(allowFingerprinting);
|
|
1678
1670
|
const random = Math.random().toString(36).substring(2, 15);
|
|
1679
1671
|
const timestamp = Date.now().toString(36);
|
|
1680
1672
|
const fingerprint = [
|
|
@@ -1682,7 +1674,7 @@ var getVisitorId = async () => {
|
|
|
1682
1674
|
entropy.hardware_concurrency,
|
|
1683
1675
|
entropy.timezone_offset,
|
|
1684
1676
|
entropy.platform,
|
|
1685
|
-
entropy.canvas_hash
|
|
1677
|
+
entropy.canvas_hash || "standard_entropy"
|
|
1686
1678
|
].join("|");
|
|
1687
1679
|
let hash = 0;
|
|
1688
1680
|
for (let i = 0; i < fingerprint.length; i++) {
|
|
@@ -1893,7 +1885,6 @@ var EventStorage = class {
|
|
|
1893
1885
|
var eventStorage = new EventStorage();
|
|
1894
1886
|
|
|
1895
1887
|
// src/analytics/tracker.ts
|
|
1896
|
-
var SDK_VERSION = "0.1.0";
|
|
1897
1888
|
var safeGetItem = (key) => {
|
|
1898
1889
|
try {
|
|
1899
1890
|
return localStorage.getItem(key);
|
|
@@ -1929,6 +1920,8 @@ var Tracker = class {
|
|
|
1929
1920
|
this.visitorId = "";
|
|
1930
1921
|
this.anonymousId = "";
|
|
1931
1922
|
this.isFlushing = false;
|
|
1923
|
+
this.flushPromise = null;
|
|
1924
|
+
this.batchSize = 25;
|
|
1932
1925
|
this.config = config;
|
|
1933
1926
|
this.endpoint = `${this.config.analyticsUrl}/api/collect`;
|
|
1934
1927
|
this.circuitBreaker = new CircuitBreaker();
|
|
@@ -1939,7 +1932,9 @@ var Tracker = class {
|
|
|
1939
1932
|
}
|
|
1940
1933
|
}
|
|
1941
1934
|
async initSession() {
|
|
1942
|
-
this.visitorId = await getVisitorId(
|
|
1935
|
+
this.visitorId = await getVisitorId(
|
|
1936
|
+
this.config.privacy?.fingerprinting ?? false
|
|
1937
|
+
);
|
|
1943
1938
|
let anonId = safeGetItem("nexus_anon_id");
|
|
1944
1939
|
if (!anonId) {
|
|
1945
1940
|
anonId = `anon_${generateUUID().replace(/-/g, "")}`;
|
|
@@ -1952,8 +1947,8 @@ var Tracker = class {
|
|
|
1952
1947
|
const SESSION_TIMEOUT = 30 * 60 * 1e3;
|
|
1953
1948
|
const isExpired = !sid || !lastActivity || now - parseInt(lastActivity, 10) > SESSION_TIMEOUT;
|
|
1954
1949
|
if (isExpired) {
|
|
1955
|
-
const
|
|
1956
|
-
sid = `sess_${
|
|
1950
|
+
const uuid2 = generateUUID().replace(/-/g, "").substring(0, 16);
|
|
1951
|
+
sid = `sess_${uuid2}_${now}`;
|
|
1957
1952
|
safeSetItem("nexus_sid", sid);
|
|
1958
1953
|
this.sessionStart = now;
|
|
1959
1954
|
}
|
|
@@ -1963,7 +1958,9 @@ var Tracker = class {
|
|
|
1963
1958
|
async send(eventType, data = {}, eventName, ecommerce) {
|
|
1964
1959
|
if (typeof window === "undefined") return;
|
|
1965
1960
|
safeSetItem("nexus_last_active", Date.now().toString());
|
|
1966
|
-
const entropy = await getDeviceEntropy(
|
|
1961
|
+
const entropy = await getDeviceEntropy(
|
|
1962
|
+
this.config.privacy?.fingerprinting ?? false
|
|
1963
|
+
);
|
|
1967
1964
|
const perfMetrics = vitalsCollector.getMetricsSnapshot();
|
|
1968
1965
|
const utmParams = extractUtmParams(window.location.href);
|
|
1969
1966
|
const payload = {
|
|
@@ -1974,6 +1971,7 @@ var Tracker = class {
|
|
|
1974
1971
|
messageId: generateUUID(),
|
|
1975
1972
|
sentAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1976
1973
|
version: SDK_VERSION,
|
|
1974
|
+
// Dynamically synced to SDK 1.1.0
|
|
1977
1975
|
url: window.location.href,
|
|
1978
1976
|
referrer: document.referrer,
|
|
1979
1977
|
userAgent: window.navigator.userAgent,
|
|
@@ -1991,8 +1989,8 @@ var Tracker = class {
|
|
|
1991
1989
|
hardwareConcurrency: entropy.hardware_concurrency,
|
|
1992
1990
|
deviceMemory: entropy.device_memory,
|
|
1993
1991
|
pixelRatio: entropy.pixel_ratio,
|
|
1994
|
-
|
|
1995
|
-
|
|
1992
|
+
platform: entropy.platform,
|
|
1993
|
+
...this.config.privacy?.fingerprinting ? { canvasFingerprint: entropy.canvas_hash } : {}
|
|
1996
1994
|
},
|
|
1997
1995
|
visitorIdLocal: this.visitorId,
|
|
1998
1996
|
utm: utmParams
|
|
@@ -2012,53 +2010,69 @@ var Tracker = class {
|
|
|
2012
2010
|
});
|
|
2013
2011
|
}
|
|
2014
2012
|
async flushQueue(useBeacon = false) {
|
|
2015
|
-
if (this.
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2013
|
+
if (this.flushPromise) return this.flushPromise;
|
|
2014
|
+
this.flushPromise = (async () => {
|
|
2015
|
+
if (this.circuitBreaker.isOpen()) return;
|
|
2016
|
+
try {
|
|
2017
|
+
const storedEvents = await eventStorage.peek(this.batchSize);
|
|
2018
|
+
if (!storedEvents.length) return;
|
|
2019
|
+
const payloads = storedEvents.map((e) => e.payload);
|
|
2020
|
+
const headers = {
|
|
2021
|
+
"Content-Type": "application/json",
|
|
2022
|
+
Authorization: this.config.apiKey ? `Bearer ${this.config.apiKey}` : "",
|
|
2023
|
+
"X-Nexus-Client": `analytics/${SDK_VERSION}`,
|
|
2024
|
+
"X-Nexus-Project": this.config.projectId
|
|
2025
|
+
};
|
|
2026
|
+
let response;
|
|
2027
|
+
try {
|
|
2028
|
+
response = await fetch(
|
|
2029
|
+
`${this.config.analyticsUrl}/api/collect/batch`,
|
|
2030
|
+
{
|
|
2031
|
+
method: "POST",
|
|
2032
|
+
headers,
|
|
2033
|
+
body: JSON.stringify({ events: payloads }),
|
|
2034
|
+
keepalive: useBeacon
|
|
2035
|
+
}
|
|
2036
|
+
);
|
|
2037
|
+
} catch {
|
|
2038
|
+
response = void 0;
|
|
2039
|
+
}
|
|
2040
|
+
if (!response || response.status === 404 || response.status === 405) {
|
|
2041
|
+
const results = await Promise.allSettled(
|
|
2042
|
+
payloads.map(
|
|
2043
|
+
(event) => fetch(this.endpoint, {
|
|
2044
|
+
method: "POST",
|
|
2045
|
+
headers,
|
|
2046
|
+
body: JSON.stringify(event),
|
|
2047
|
+
keepalive: useBeacon
|
|
2048
|
+
})
|
|
2049
|
+
)
|
|
2050
|
+
);
|
|
2051
|
+
const successIds = results.flatMap(
|
|
2052
|
+
(r, i) => r.status === "fulfilled" && r.value.ok ? [storedEvents[i].id] : []
|
|
2053
|
+
);
|
|
2054
|
+
if (successIds.length) await eventStorage.remove(successIds);
|
|
2055
|
+
if (successIds.length === storedEvents.length) {
|
|
2056
|
+
this.circuitBreaker.recordSuccess();
|
|
2057
|
+
} else {
|
|
2058
|
+
this.circuitBreaker.recordFailure();
|
|
2059
|
+
}
|
|
2060
|
+
} else if (response.ok) {
|
|
2061
|
+
await eventStorage.remove(storedEvents.map((e) => e.id));
|
|
2062
|
+
this.circuitBreaker.recordSuccess();
|
|
2042
2063
|
} else {
|
|
2043
|
-
|
|
2064
|
+
this.circuitBreaker.recordFailure();
|
|
2044
2065
|
}
|
|
2045
|
-
})
|
|
2046
|
-
if (successIds.length > 0) {
|
|
2047
|
-
await eventStorage.remove(successIds);
|
|
2048
|
-
this.circuitBreaker.recordSuccess();
|
|
2049
|
-
}
|
|
2050
|
-
if (failureCount > 0) {
|
|
2066
|
+
} catch (err) {
|
|
2051
2067
|
this.circuitBreaker.recordFailure();
|
|
2068
|
+
if (this.config.debug) {
|
|
2069
|
+
console.warn("[GN-Apex] Analytics flush deferred:", err);
|
|
2070
|
+
}
|
|
2071
|
+
} finally {
|
|
2072
|
+
this.flushPromise = null;
|
|
2052
2073
|
}
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
|
-
this.circuitBreaker.recordFailure();
|
|
2056
|
-
} finally {
|
|
2057
|
-
this.isFlushing = false;
|
|
2058
|
-
if (!this.circuitBreaker.isOpen() && await eventStorage.count() > 0) {
|
|
2059
|
-
setTimeout(() => this.flushQueue(), 100);
|
|
2060
|
-
}
|
|
2061
|
-
}
|
|
2074
|
+
})();
|
|
2075
|
+
return this.flushPromise;
|
|
2062
2076
|
}
|
|
2063
2077
|
getSession() {
|
|
2064
2078
|
return this.sessionId;
|
|
@@ -2337,70 +2351,35 @@ var AnalyticsEngine = class {
|
|
|
2337
2351
|
}
|
|
2338
2352
|
setupVideoTracking() {
|
|
2339
2353
|
if (typeof document === "undefined") return;
|
|
2340
|
-
const
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
});
|
|
2352
|
-
video.addEventListener("
|
|
2353
|
-
this.tracker.send(
|
|
2354
|
-
"custom_event",
|
|
2355
|
-
{
|
|
2356
|
-
event_name: "video_pause",
|
|
2357
|
-
src,
|
|
2358
|
-
position_seconds: Math.round(video.currentTime)
|
|
2359
|
-
},
|
|
2360
|
-
"video_pause"
|
|
2361
|
-
);
|
|
2362
|
-
});
|
|
2354
|
+
const attach = (video) => {
|
|
2355
|
+
const v = video;
|
|
2356
|
+
if (v.__gnexusTracked) return;
|
|
2357
|
+
v.__gnexusTracked = true;
|
|
2358
|
+
v.__gnexusMilestones = /* @__PURE__ */ new Set();
|
|
2359
|
+
const src = () => video.currentSrc || video.src || "unknown";
|
|
2360
|
+
const emit = (name, data = {}) => this.tracker.send("media", { media_type: "video", event_name: name, src: src(), ...data }, name);
|
|
2361
|
+
video.addEventListener("loadedmetadata", () => emit("video_loaded", { duration_seconds: Number.isFinite(video.duration) ? Math.round(video.duration) : void 0 }));
|
|
2362
|
+
video.addEventListener("play", () => emit("video_play", { position_seconds: Math.round(video.currentTime) }));
|
|
2363
|
+
video.addEventListener("pause", () => emit("video_pause", { position_seconds: Math.round(video.currentTime) }));
|
|
2364
|
+
video.addEventListener("seeking", () => emit("video_seek", { position_seconds: Math.round(video.currentTime) }));
|
|
2365
|
+
video.addEventListener("ended", () => emit("video_complete", { duration_seconds: Math.round(video.duration) }));
|
|
2366
|
+
video.addEventListener("error", () => emit("video_error"));
|
|
2363
2367
|
video.addEventListener("timeupdate", () => {
|
|
2364
|
-
if (!video.duration || video.duration
|
|
2365
|
-
const
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
{
|
|
2371
|
-
event_name: "video_50_percent",
|
|
2372
|
-
src
|
|
2373
|
-
},
|
|
2374
|
-
"video_50_percent"
|
|
2375
|
-
);
|
|
2368
|
+
if (!Number.isFinite(video.duration) || video.duration <= 0) return;
|
|
2369
|
+
for (const milestone of [25, 50, 75, 90, 100]) {
|
|
2370
|
+
if (video.currentTime / video.duration * 100 >= milestone && !v.__gnexusMilestones.has(milestone)) {
|
|
2371
|
+
v.__gnexusMilestones.add(milestone);
|
|
2372
|
+
emit(`video_${milestone}_percent`, { progress: milestone / 100 });
|
|
2373
|
+
}
|
|
2376
2374
|
}
|
|
2377
2375
|
});
|
|
2378
|
-
video.addEventListener("ended", () => {
|
|
2379
|
-
this.tracker.send(
|
|
2380
|
-
"custom_event",
|
|
2381
|
-
{
|
|
2382
|
-
event_name: "video_complete",
|
|
2383
|
-
src,
|
|
2384
|
-
duration_seconds: Math.round(video.duration)
|
|
2385
|
-
},
|
|
2386
|
-
"video_complete"
|
|
2387
|
-
);
|
|
2388
|
-
});
|
|
2389
2376
|
};
|
|
2390
|
-
document.querySelectorAll("video").forEach((v) =>
|
|
2391
|
-
const observer = new MutationObserver((
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
}
|
|
2397
|
-
if (node instanceof Element) {
|
|
2398
|
-
node.querySelectorAll("video").forEach((v) => attachVideoListeners(v));
|
|
2399
|
-
}
|
|
2400
|
-
});
|
|
2401
|
-
});
|
|
2402
|
-
});
|
|
2403
|
-
observer.observe(document.body, { childList: true, subtree: true });
|
|
2377
|
+
document.querySelectorAll("video").forEach((v) => attach(v));
|
|
2378
|
+
const observer = new MutationObserver((ms) => ms.forEach((m) => m.addedNodes.forEach((n) => {
|
|
2379
|
+
if (n instanceof HTMLVideoElement) attach(n);
|
|
2380
|
+
if (n instanceof Element) n.querySelectorAll("video").forEach((v) => attach(v));
|
|
2381
|
+
})));
|
|
2382
|
+
if (document.body) observer.observe(document.body, { childList: true, subtree: true });
|
|
2404
2383
|
this.cleanupFns.push(() => observer.disconnect());
|
|
2405
2384
|
}
|
|
2406
2385
|
setupErrorTracking() {
|
|
@@ -2589,65 +2568,397 @@ function getSelector(el, depth = 0) {
|
|
|
2589
2568
|
return `${parent}${self}`;
|
|
2590
2569
|
}
|
|
2591
2570
|
|
|
2571
|
+
// src/errors.ts
|
|
2572
|
+
var NexusError = class extends Error {
|
|
2573
|
+
constructor(message, code = "UNKNOWN", status, requestId, details, retryable = false, cause) {
|
|
2574
|
+
super(message);
|
|
2575
|
+
this.code = code;
|
|
2576
|
+
this.status = status;
|
|
2577
|
+
this.requestId = requestId;
|
|
2578
|
+
this.details = details;
|
|
2579
|
+
this.retryable = retryable;
|
|
2580
|
+
this.cause = cause;
|
|
2581
|
+
this.name = "NexusError";
|
|
2582
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
2583
|
+
}
|
|
2584
|
+
};
|
|
2585
|
+
var isNexusError = (e) => e instanceof NexusError;
|
|
2586
|
+
|
|
2587
|
+
// src/events.ts
|
|
2588
|
+
var NexusEventBus = class {
|
|
2589
|
+
constructor() {
|
|
2590
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
2591
|
+
}
|
|
2592
|
+
on(event, listener) {
|
|
2593
|
+
const set = this.listeners.get(event) ?? /* @__PURE__ */ new Set();
|
|
2594
|
+
set.add(listener);
|
|
2595
|
+
this.listeners.set(event, set);
|
|
2596
|
+
return () => set.delete(listener);
|
|
2597
|
+
}
|
|
2598
|
+
emit(event, payload) {
|
|
2599
|
+
this.listeners.get(event)?.forEach((l) => {
|
|
2600
|
+
try {
|
|
2601
|
+
l(payload);
|
|
2602
|
+
} catch {
|
|
2603
|
+
}
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
clear() {
|
|
2607
|
+
this.listeners.clear();
|
|
2608
|
+
}
|
|
2609
|
+
};
|
|
2610
|
+
|
|
2611
|
+
// src/http.ts
|
|
2612
|
+
var uuid = () => typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `nx_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
2613
|
+
var NexusHttpClient = class {
|
|
2614
|
+
constructor(config, events = new NexusEventBus()) {
|
|
2615
|
+
this.config = config;
|
|
2616
|
+
this.events = events;
|
|
2617
|
+
this.breaker = new CircuitBreaker();
|
|
2618
|
+
this.limiter = new RateLimiter({
|
|
2619
|
+
maxRequests: config.debug ? 200 : 100,
|
|
2620
|
+
timeWindow: 6e4
|
|
2621
|
+
});
|
|
2622
|
+
this.backoff = new ExponentialBackoff({
|
|
2623
|
+
maxRetries: config.retries ?? 3,
|
|
2624
|
+
baseDelay: 150,
|
|
2625
|
+
maxDelay: 8e3,
|
|
2626
|
+
jitter: true
|
|
2627
|
+
});
|
|
2628
|
+
}
|
|
2629
|
+
updateConfig(config) {
|
|
2630
|
+
this.config = config;
|
|
2631
|
+
}
|
|
2632
|
+
async request(input, options = {}) {
|
|
2633
|
+
if (this.breaker.isOpen()) {
|
|
2634
|
+
throw new NexusError(
|
|
2635
|
+
"GN-Apex service circuit is open",
|
|
2636
|
+
"CIRCUIT_OPEN",
|
|
2637
|
+
void 0,
|
|
2638
|
+
void 0,
|
|
2639
|
+
void 0,
|
|
2640
|
+
true
|
|
2641
|
+
);
|
|
2642
|
+
}
|
|
2643
|
+
const requestId = uuid();
|
|
2644
|
+
const url = input;
|
|
2645
|
+
const started = Date.now();
|
|
2646
|
+
const headers = new Headers(options.headers);
|
|
2647
|
+
headers.set("X-Nexus-Request-ID", requestId);
|
|
2648
|
+
headers.set(
|
|
2649
|
+
"X-Nexus-Client",
|
|
2650
|
+
`gnexus-sdk/${this.config.sdkVersion ?? "1.1.0"}`
|
|
2651
|
+
);
|
|
2652
|
+
headers.set("X-Nexus-Project", this.config.projectId);
|
|
2653
|
+
if (this.config.apiKey)
|
|
2654
|
+
headers.set("Authorization", `Bearer ${this.config.apiKey}`);
|
|
2655
|
+
this.events.emit("request:start", {
|
|
2656
|
+
requestId,
|
|
2657
|
+
url,
|
|
2658
|
+
method: options.method ?? "GET"
|
|
2659
|
+
});
|
|
2660
|
+
try {
|
|
2661
|
+
await this.limiter.checkLimit();
|
|
2662
|
+
const response = await this.backoff.execute(async () => {
|
|
2663
|
+
const controller = new AbortController();
|
|
2664
|
+
const timeout = options.timeout ?? this.config.timeout ?? 1e4;
|
|
2665
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
2666
|
+
try {
|
|
2667
|
+
return await fetch(url, {
|
|
2668
|
+
...options,
|
|
2669
|
+
headers,
|
|
2670
|
+
signal: options.signal ?? controller.signal
|
|
2671
|
+
});
|
|
2672
|
+
} catch (e) {
|
|
2673
|
+
if (e?.name === "AbortError") {
|
|
2674
|
+
throw new NexusError(
|
|
2675
|
+
`Request timed out after ${timeout}ms`,
|
|
2676
|
+
"TIMEOUT",
|
|
2677
|
+
void 0,
|
|
2678
|
+
requestId,
|
|
2679
|
+
void 0,
|
|
2680
|
+
true,
|
|
2681
|
+
e
|
|
2682
|
+
);
|
|
2683
|
+
}
|
|
2684
|
+
throw new NexusError(
|
|
2685
|
+
"Network request failed",
|
|
2686
|
+
"NETWORK_ERROR",
|
|
2687
|
+
void 0,
|
|
2688
|
+
requestId,
|
|
2689
|
+
void 0,
|
|
2690
|
+
true,
|
|
2691
|
+
e
|
|
2692
|
+
);
|
|
2693
|
+
} finally {
|
|
2694
|
+
clearTimeout(timer);
|
|
2695
|
+
}
|
|
2696
|
+
});
|
|
2697
|
+
if (!response.ok) {
|
|
2698
|
+
const retryable = response.status === 408 || response.status === 429 || response.status >= 500;
|
|
2699
|
+
let details;
|
|
2700
|
+
try {
|
|
2701
|
+
details = await response.clone().json();
|
|
2702
|
+
} catch {
|
|
2703
|
+
}
|
|
2704
|
+
const code = response.status === 404 ? "NOT_FOUND" : response.status === 429 ? "RATE_LIMITED" : "HTTP_ERROR";
|
|
2705
|
+
const err = new NexusError(
|
|
2706
|
+
`API request failed (${response.status})`,
|
|
2707
|
+
code,
|
|
2708
|
+
response.status,
|
|
2709
|
+
response.headers.get("x-nexus-request-id") ?? requestId,
|
|
2710
|
+
details,
|
|
2711
|
+
retryable
|
|
2712
|
+
);
|
|
2713
|
+
throw err;
|
|
2714
|
+
}
|
|
2715
|
+
this.breaker.recordSuccess();
|
|
2716
|
+
this.events.emit("request:end", {
|
|
2717
|
+
requestId,
|
|
2718
|
+
url,
|
|
2719
|
+
status: response.status,
|
|
2720
|
+
duration: Date.now() - started
|
|
2721
|
+
});
|
|
2722
|
+
return response;
|
|
2723
|
+
} catch (e) {
|
|
2724
|
+
if (e instanceof NexusError) {
|
|
2725
|
+
if (e.retryable || e.status && (e.status >= 500 || e.status === 429)) {
|
|
2726
|
+
this.breaker.recordFailure();
|
|
2727
|
+
} else {
|
|
2728
|
+
this.breaker.recordSuccess();
|
|
2729
|
+
}
|
|
2730
|
+
} else {
|
|
2731
|
+
this.breaker.recordFailure();
|
|
2732
|
+
}
|
|
2733
|
+
this.events.emit("error", { error: e });
|
|
2734
|
+
throw e;
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
getStats() {
|
|
2738
|
+
return {
|
|
2739
|
+
rateLimit: this.limiter.getStats(),
|
|
2740
|
+
circuit: this.breaker.getState()
|
|
2741
|
+
};
|
|
2742
|
+
}
|
|
2743
|
+
};
|
|
2744
|
+
|
|
2745
|
+
// src/flags.ts
|
|
2746
|
+
var FeatureFlags = class {
|
|
2747
|
+
constructor(initial) {
|
|
2748
|
+
this.values = {};
|
|
2749
|
+
this.values = { ...initial };
|
|
2750
|
+
}
|
|
2751
|
+
set(values) {
|
|
2752
|
+
this.values = { ...this.values, ...values };
|
|
2753
|
+
}
|
|
2754
|
+
isEnabled(key, fallback = false) {
|
|
2755
|
+
const v = this.values[key];
|
|
2756
|
+
return typeof v === "boolean" ? v : fallback;
|
|
2757
|
+
}
|
|
2758
|
+
get(key, fallback) {
|
|
2759
|
+
return this.values[key] ?? fallback;
|
|
2760
|
+
}
|
|
2761
|
+
all() {
|
|
2762
|
+
return { ...this.values };
|
|
2763
|
+
}
|
|
2764
|
+
};
|
|
2765
|
+
var RemoteConfig = class {
|
|
2766
|
+
constructor() {
|
|
2767
|
+
this.values = {};
|
|
2768
|
+
}
|
|
2769
|
+
set(values) {
|
|
2770
|
+
this.values = { ...this.values, ...values };
|
|
2771
|
+
}
|
|
2772
|
+
get(key, fallback) {
|
|
2773
|
+
return this.values[key] ?? fallback;
|
|
2774
|
+
}
|
|
2775
|
+
all() {
|
|
2776
|
+
return { ...this.values };
|
|
2777
|
+
}
|
|
2778
|
+
};
|
|
2779
|
+
|
|
2780
|
+
// src/diagnostics.ts
|
|
2781
|
+
var getDiagnostics = (version, environment) => ({ version, environment, online: typeof navigator === "undefined" ? true : navigator.onLine, userAgent: typeof navigator === "undefined" ? void 0 : navigator.userAgent, memory: typeof performance !== "undefined" && "memory" in performance ? performance.memory?.usedJSHeapSize : void 0 });
|
|
2782
|
+
|
|
2592
2783
|
// src/client.ts
|
|
2593
|
-
var DEFAULT_REVALIDATE_SECONDS = false;
|
|
2594
2784
|
var NexusClient = class {
|
|
2595
|
-
constructor(config) {
|
|
2596
|
-
const
|
|
2785
|
+
constructor(config = {}) {
|
|
2786
|
+
const full = getFullConfig(config);
|
|
2597
2787
|
this.config = {
|
|
2598
|
-
debug:
|
|
2599
|
-
cacheStrategy:
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2788
|
+
debug: false,
|
|
2789
|
+
cacheStrategy: "memory",
|
|
2790
|
+
revalidateTime: false,
|
|
2791
|
+
timeout: 1e4,
|
|
2792
|
+
retries: 3,
|
|
2793
|
+
sdkVersion: SDK_VERSION,
|
|
2794
|
+
cacheInvalidation: "platform",
|
|
2795
|
+
environment: typeof process !== "undefined" && process.env?.NODE_ENV === "development" ? "development" : "production",
|
|
2796
|
+
autoTracking: true,
|
|
2797
|
+
publicKeyOnly: true,
|
|
2798
|
+
// 🚀 Default: fingerprinting: true (Active by default for high-precision analytics)
|
|
2799
|
+
privacy: { analytics: true, fingerprinting: true, redact: true },
|
|
2800
|
+
...full,
|
|
2801
|
+
...config
|
|
2610
2802
|
};
|
|
2611
2803
|
const errors = validateConfig(this.config);
|
|
2612
|
-
if (errors.length
|
|
2613
|
-
console.warn("
|
|
2614
|
-
if (!this.config.projectId) {
|
|
2615
|
-
console.warn("\u26A0\uFE0F NexusHub: No Project ID found. Tracking will fail.");
|
|
2616
|
-
}
|
|
2804
|
+
if (errors.length && this.config.debug) {
|
|
2805
|
+
console.warn("[GN-Apex] Configuration warnings:", errors);
|
|
2617
2806
|
}
|
|
2807
|
+
this.events = new NexusEventBus();
|
|
2808
|
+
this.http = new NexusHttpClient(this.config, this.events);
|
|
2809
|
+
this.flags = new FeatureFlags();
|
|
2810
|
+
this.remoteConfig = new RemoteConfig();
|
|
2618
2811
|
this.content = new ContentEngine(this.config);
|
|
2619
|
-
if (typeof window !== "undefined") {
|
|
2812
|
+
if (typeof window !== "undefined" && this.config.autoTracking !== false && this.config.privacy?.analytics !== false) {
|
|
2620
2813
|
this.analytics = new AnalyticsEngine(this.config);
|
|
2621
2814
|
this.analytics.start();
|
|
2622
2815
|
}
|
|
2623
2816
|
}
|
|
2624
2817
|
/**
|
|
2625
|
-
* Helper alias for cleaner content fetching.
|
|
2818
|
+
* Helper alias for cleaner page content fetching.
|
|
2626
2819
|
*/
|
|
2627
2820
|
getPage(slug, options) {
|
|
2628
2821
|
return this.content.getPage(slug, options);
|
|
2629
2822
|
}
|
|
2630
2823
|
/**
|
|
2631
|
-
* Returns a readonly snapshot of the
|
|
2824
|
+
* Returns a readonly snapshot of the active configuration.
|
|
2632
2825
|
*/
|
|
2633
2826
|
getConfig() {
|
|
2634
|
-
return {
|
|
2827
|
+
return Object.freeze({
|
|
2828
|
+
...this.config,
|
|
2829
|
+
privacy: { ...this.config.privacy }
|
|
2830
|
+
});
|
|
2635
2831
|
}
|
|
2636
2832
|
/**
|
|
2637
|
-
* Updates
|
|
2638
|
-
* Replaces the previous pattern of `(nexus as any).config.projectId = x`
|
|
2639
|
-
* which bypassed TypeScript and mutated internal state unsafely.
|
|
2833
|
+
* Updates runtime configuration dynamically without breaking active listeners.
|
|
2640
2834
|
*/
|
|
2641
2835
|
updateConfig(updates) {
|
|
2642
|
-
this.config = {
|
|
2836
|
+
this.config = {
|
|
2837
|
+
...this.config,
|
|
2838
|
+
...updates,
|
|
2839
|
+
privacy: { ...this.config.privacy, ...updates.privacy }
|
|
2840
|
+
};
|
|
2841
|
+
this.http.updateConfig(this.config);
|
|
2842
|
+
this.content.updateConfig?.(this.config);
|
|
2843
|
+
if (typeof window !== "undefined" && this.config.autoTracking !== false && this.config.privacy?.analytics !== false && !this.analytics) {
|
|
2844
|
+
this.analytics = new AnalyticsEngine(this.config);
|
|
2845
|
+
this.analytics.start();
|
|
2846
|
+
}
|
|
2847
|
+
if (this.config.privacy?.analytics === false) {
|
|
2848
|
+
this.analytics?.stop();
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
/**
|
|
2852
|
+
* Returns deep system diagnostics including HTTP and cache performance.
|
|
2853
|
+
*/
|
|
2854
|
+
diagnostics() {
|
|
2855
|
+
return {
|
|
2856
|
+
...getDiagnostics(SDK_VERSION, this.config.environment ?? "production"),
|
|
2857
|
+
cache: this.content.getCacheStats(),
|
|
2858
|
+
http: this.http.getStats(),
|
|
2859
|
+
analyticsQueue: this.analytics ? void 0 : 0
|
|
2860
|
+
};
|
|
2861
|
+
}
|
|
2862
|
+
/**
|
|
2863
|
+
* Gracefully terminates background tasks and cleans up listeners.
|
|
2864
|
+
*/
|
|
2865
|
+
destroy() {
|
|
2866
|
+
this.analytics?.stop(true);
|
|
2867
|
+
this.content.cleanup?.();
|
|
2868
|
+
this.events.clear();
|
|
2643
2869
|
}
|
|
2644
2870
|
};
|
|
2645
2871
|
var nexus = new NexusClient();
|
|
2646
|
-
var createNexusClient = (config) => new NexusClient(config);
|
|
2872
|
+
var createNexusClient = (config = {}) => new NexusClient(config);
|
|
2647
2873
|
|
|
2648
2874
|
// src/index.ts
|
|
2649
2875
|
init_config();
|
|
2650
|
-
|
|
2876
|
+
|
|
2877
|
+
// src/notifications/push-client.ts
|
|
2878
|
+
var NexusPushClient = class {
|
|
2879
|
+
constructor(config) {
|
|
2880
|
+
this.config = config;
|
|
2881
|
+
}
|
|
2882
|
+
/**
|
|
2883
|
+
* Helper to convert Base64 VAPID key to Uint8Array for WebPush security
|
|
2884
|
+
*/
|
|
2885
|
+
urlBase64ToUint8Array(base64String) {
|
|
2886
|
+
const padding = "=".repeat((4 - base64String.length % 4) % 4);
|
|
2887
|
+
const base64 = (base64String + padding).replace(/\-/g, "+").replace(/_/g, "/");
|
|
2888
|
+
const rawData = window.atob(base64);
|
|
2889
|
+
const outputArray = new Uint8Array(rawData.length);
|
|
2890
|
+
for (let i = 0; i < rawData.length; ++i) {
|
|
2891
|
+
outputArray[i] = rawData.charCodeAt(i);
|
|
2892
|
+
}
|
|
2893
|
+
return outputArray;
|
|
2894
|
+
}
|
|
2895
|
+
/**
|
|
2896
|
+
* Requests browser notification permissions and registers the WebPush subscription
|
|
2897
|
+
*/
|
|
2898
|
+
async requestSubscription(serviceWorkerPath = "/sw.js") {
|
|
2899
|
+
if (typeof window === "undefined" || !("serviceWorker" in navigator) || !("PushManager" in window)) {
|
|
2900
|
+
console.warn(
|
|
2901
|
+
"[NexusHub] Push notifications are not supported in this browser environment."
|
|
2902
|
+
);
|
|
2903
|
+
return false;
|
|
2904
|
+
}
|
|
2905
|
+
try {
|
|
2906
|
+
const permission = await Notification.requestPermission();
|
|
2907
|
+
if (permission !== "granted") {
|
|
2908
|
+
console.warn("[NexusHub] Notification permission denied by user.");
|
|
2909
|
+
return false;
|
|
2910
|
+
}
|
|
2911
|
+
const vapidRes = await fetch(
|
|
2912
|
+
`${this.config.apiUrl}/notifications/vapid-key`,
|
|
2913
|
+
{
|
|
2914
|
+
headers: {
|
|
2915
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
2916
|
+
"x-nexus-project": this.config.projectId
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
);
|
|
2920
|
+
if (!vapidRes.ok) throw new Error("Failed to fetch VAPID public key.");
|
|
2921
|
+
const { publicKey } = await vapidRes.json();
|
|
2922
|
+
const registration = await navigator.serviceWorker.register(serviceWorkerPath);
|
|
2923
|
+
await navigator.serviceWorker.ready;
|
|
2924
|
+
const subscription = await registration.pushManager.subscribe({
|
|
2925
|
+
userVisibleOnly: true,
|
|
2926
|
+
// 🚀 RESOLVED: Cast as 'any' to bypass strict DOM BufferSource typings
|
|
2927
|
+
applicationServerKey: this.urlBase64ToUint8Array(publicKey)
|
|
2928
|
+
});
|
|
2929
|
+
const rawSub = subscription.toJSON();
|
|
2930
|
+
if (!rawSub.endpoint || !rawSub.keys?.auth || !rawSub.keys?.p256dh) {
|
|
2931
|
+
throw new Error(
|
|
2932
|
+
"Malformed subscription payload received from browser."
|
|
2933
|
+
);
|
|
2934
|
+
}
|
|
2935
|
+
const res = await fetch(
|
|
2936
|
+
`${this.config.apiUrl}/notifications/project/${this.config.projectId}/subscribe`,
|
|
2937
|
+
{
|
|
2938
|
+
method: "POST",
|
|
2939
|
+
headers: {
|
|
2940
|
+
"Content-Type": "application/json",
|
|
2941
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
2942
|
+
"x-nexus-project": this.config.projectId
|
|
2943
|
+
},
|
|
2944
|
+
body: JSON.stringify({
|
|
2945
|
+
endpoint: rawSub.endpoint,
|
|
2946
|
+
auth: rawSub.keys.auth,
|
|
2947
|
+
p256dh: rawSub.keys.p256dh,
|
|
2948
|
+
provider: "WEB_PUSH"
|
|
2949
|
+
})
|
|
2950
|
+
}
|
|
2951
|
+
);
|
|
2952
|
+
return res.ok;
|
|
2953
|
+
} catch (err) {
|
|
2954
|
+
console.error("[NexusHub] WebPush subscription failed:", err);
|
|
2955
|
+
return false;
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
};
|
|
2959
|
+
|
|
2960
|
+
// src/index.ts
|
|
2961
|
+
var VERSION = "1.1.0";
|
|
2651
2962
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2652
2963
|
0 && (module.exports = {
|
|
2653
2964
|
AnalyticsEngine,
|
|
@@ -2656,15 +2967,25 @@ var VERSION = "0.0.1";
|
|
|
2656
2967
|
ContentEngine,
|
|
2657
2968
|
DEFAULT_ANALYTICS_URL,
|
|
2658
2969
|
DEFAULT_API_URL,
|
|
2970
|
+
FeatureFlags,
|
|
2659
2971
|
LOCAL_NEST_URL,
|
|
2660
2972
|
LOCAL_RUST_URL,
|
|
2661
2973
|
LocalCache,
|
|
2662
2974
|
MemoryCache,
|
|
2663
2975
|
NexusClient,
|
|
2976
|
+
NexusError,
|
|
2977
|
+
NexusEventBus,
|
|
2978
|
+
NexusHttpClient,
|
|
2979
|
+
NexusPushClient,
|
|
2980
|
+
RemoteConfig,
|
|
2981
|
+
SDK_VERSION,
|
|
2664
2982
|
VERSION,
|
|
2665
2983
|
createNexusClient,
|
|
2984
|
+
getDiagnostics,
|
|
2666
2985
|
getEnvConfig,
|
|
2667
2986
|
getFullConfig,
|
|
2987
|
+
hasRequiredConfig,
|
|
2988
|
+
isNexusError,
|
|
2668
2989
|
mergeConfigs,
|
|
2669
2990
|
nexus,
|
|
2670
2991
|
validateConfig
|