gracefulerrors 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +199 -0
- package/dist/adapters/sonner.cjs +126 -0
- package/dist/adapters/sonner.cjs.map +1 -0
- package/dist/adapters/sonner.d.cts +6 -0
- package/dist/adapters/sonner.d.ts +6 -0
- package/dist/adapters/sonner.js +121 -0
- package/dist/adapters/sonner.js.map +1 -0
- package/dist/index.cjs +486 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +481 -0
- package/dist/index.js.map +1 -0
- package/dist/internal.cjs +169 -0
- package/dist/internal.cjs.map +1 -0
- package/dist/internal.d.cts +14 -0
- package/dist/internal.d.ts +14 -0
- package/dist/internal.js +166 -0
- package/dist/internal.js.map +1 -0
- package/dist/react.cjs +71 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +35 -0
- package/dist/react.d.ts +35 -0
- package/dist/react.js +61 -0
- package/dist/react.js.map +1 -0
- package/dist/types-CsPmpcbL.d.cts +149 -0
- package/dist/types-CsPmpcbL.d.ts +149 -0
- package/package.json +80 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/state-manager.ts
|
|
4
|
+
function createStateManager(config) {
|
|
5
|
+
const maxConcurrent = config.maxConcurrent ?? 3;
|
|
6
|
+
const maxQueue = config.maxQueue;
|
|
7
|
+
const dedupeWindow = config.dedupeWindow ?? 300;
|
|
8
|
+
const { onDropped } = config;
|
|
9
|
+
const activeSlots = /* @__PURE__ */ new Map();
|
|
10
|
+
const queue = [];
|
|
11
|
+
const dedupeMap = /* @__PURE__ */ new Map();
|
|
12
|
+
const ttlTimers = /* @__PURE__ */ new Map();
|
|
13
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
14
|
+
function notify(event) {
|
|
15
|
+
for (const listener of listeners) {
|
|
16
|
+
try {
|
|
17
|
+
listener(event);
|
|
18
|
+
} catch (err) {
|
|
19
|
+
if (process.env["NODE_ENV"] !== "production") {
|
|
20
|
+
console.error("[gracefulerrors] StateManager listener threw:", err);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function promoteFromQueue() {
|
|
26
|
+
if (queue.length > 0 && activeSlots.size < maxConcurrent) {
|
|
27
|
+
const next = queue.shift();
|
|
28
|
+
activateSlot(next, next._pendingAction);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function activateSlot(slot, action) {
|
|
32
|
+
slot.state = "ACTIVE";
|
|
33
|
+
activeSlots.set(slot.fingerprint, slot);
|
|
34
|
+
const ttl = slot.ttl;
|
|
35
|
+
if (ttl != null && ttl > 0) {
|
|
36
|
+
const timer = setTimeout(() => {
|
|
37
|
+
if (activeSlots.has(slot.fingerprint)) {
|
|
38
|
+
activeSlots.delete(slot.fingerprint);
|
|
39
|
+
ttlTimers.delete(slot.fingerprint);
|
|
40
|
+
onDropped?.(slot.error, "ttl_expired");
|
|
41
|
+
promoteFromQueue();
|
|
42
|
+
}
|
|
43
|
+
}, ttl);
|
|
44
|
+
ttlTimers.set(slot.fingerprint, timer);
|
|
45
|
+
}
|
|
46
|
+
notify({ type: "ERROR_ADDED", error: slot.error, action });
|
|
47
|
+
}
|
|
48
|
+
function canHandle(fingerprint) {
|
|
49
|
+
const lastSeen = dedupeMap.get(fingerprint);
|
|
50
|
+
if (lastSeen === void 0) return true;
|
|
51
|
+
return Date.now() - lastSeen >= dedupeWindow;
|
|
52
|
+
}
|
|
53
|
+
function enqueue(slot) {
|
|
54
|
+
if (!canHandle(slot.fingerprint)) {
|
|
55
|
+
onDropped?.(slot.error, "dedupe");
|
|
56
|
+
return "rejected";
|
|
57
|
+
}
|
|
58
|
+
dedupeMap.set(slot.fingerprint, Date.now());
|
|
59
|
+
if (activeSlots.size < maxConcurrent) {
|
|
60
|
+
activateSlot(slot, slot._pendingAction);
|
|
61
|
+
return "active";
|
|
62
|
+
}
|
|
63
|
+
if (maxQueue !== void 0 && queue.length >= maxQueue) {
|
|
64
|
+
onDropped?.(slot.error, "queue_overflow");
|
|
65
|
+
return "rejected";
|
|
66
|
+
}
|
|
67
|
+
slot.state = "QUEUED";
|
|
68
|
+
queue.push(slot);
|
|
69
|
+
return "queued";
|
|
70
|
+
}
|
|
71
|
+
function release(code) {
|
|
72
|
+
let targetFingerprint;
|
|
73
|
+
for (const [fingerprint, slot] of activeSlots) {
|
|
74
|
+
if (slot.error.code === code) {
|
|
75
|
+
targetFingerprint = fingerprint;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (targetFingerprint !== void 0) {
|
|
80
|
+
const slot = activeSlots.get(targetFingerprint);
|
|
81
|
+
slot.state = "EXPIRED";
|
|
82
|
+
activeSlots.delete(targetFingerprint);
|
|
83
|
+
const timer = ttlTimers.get(targetFingerprint);
|
|
84
|
+
if (timer !== void 0) {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
ttlTimers.delete(targetFingerprint);
|
|
87
|
+
}
|
|
88
|
+
notify({ type: "ERROR_CLEARED", code });
|
|
89
|
+
promoteFromQueue();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const queueIdx = queue.findIndex((slot) => slot.error.code === code);
|
|
93
|
+
if (queueIdx !== -1) {
|
|
94
|
+
const slot = queue[queueIdx];
|
|
95
|
+
slot.state = "EXPIRED";
|
|
96
|
+
queue.splice(queueIdx, 1);
|
|
97
|
+
notify({ type: "ERROR_CLEARED", code });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function getActiveSlots() {
|
|
101
|
+
return Array.from(activeSlots.values());
|
|
102
|
+
}
|
|
103
|
+
function getQueueLength() {
|
|
104
|
+
return queue.length;
|
|
105
|
+
}
|
|
106
|
+
function clearAll() {
|
|
107
|
+
for (const timer of ttlTimers.values()) {
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
}
|
|
110
|
+
ttlTimers.clear();
|
|
111
|
+
activeSlots.clear();
|
|
112
|
+
queue.length = 0;
|
|
113
|
+
notify({ type: "ALL_CLEARED" });
|
|
114
|
+
}
|
|
115
|
+
function subscribe(listener) {
|
|
116
|
+
listeners.add(listener);
|
|
117
|
+
return () => {
|
|
118
|
+
listeners.delete(listener);
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return { canHandle, enqueue, release, getActiveSlots, getQueueLength, clearAll, subscribe };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/registry.ts
|
|
125
|
+
function lookupEntry(registry, code) {
|
|
126
|
+
return registry[code];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/router.ts
|
|
130
|
+
function createUIRouter() {
|
|
131
|
+
function route(error, registry, config) {
|
|
132
|
+
const entry = lookupEntry(registry, error.code);
|
|
133
|
+
if (entry == null && config.requireRegistry) {
|
|
134
|
+
throw new Error(`[gracefulerrors] Registry entry required for code: ${error.code}`);
|
|
135
|
+
}
|
|
136
|
+
const routingContext = config.routingContext ?? {
|
|
137
|
+
activeCount: 0,
|
|
138
|
+
queueLength: 0
|
|
139
|
+
};
|
|
140
|
+
let strategyResult;
|
|
141
|
+
if (config.routingStrategy) {
|
|
142
|
+
try {
|
|
143
|
+
strategyResult = config.routingStrategy(error, entry, routingContext);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
if (process.env["NODE_ENV"] !== "production") {
|
|
146
|
+
console.error("[gracefulerrors] routingStrategy threw:", err);
|
|
147
|
+
}
|
|
148
|
+
strategyResult = null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (strategyResult != null) {
|
|
152
|
+
return strategyResult;
|
|
153
|
+
}
|
|
154
|
+
if (entry != null) {
|
|
155
|
+
return entry.ui;
|
|
156
|
+
}
|
|
157
|
+
const allowFallback = config.allowFallback !== false;
|
|
158
|
+
if (allowFallback && config.fallback) {
|
|
159
|
+
return config.fallback.ui;
|
|
160
|
+
}
|
|
161
|
+
return "toast";
|
|
162
|
+
}
|
|
163
|
+
return { route };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
exports.createStateManager = createStateManager;
|
|
167
|
+
exports.createUIRouter = createUIRouter;
|
|
168
|
+
//# sourceMappingURL=internal.cjs.map
|
|
169
|
+
//# sourceMappingURL=internal.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/state-manager.ts","../src/registry.ts","../src/router.ts"],"names":[],"mappings":";;;AASO,SAAS,mBACd,MAAA,EAC0B;AAC1B,EAAA,MAAM,aAAA,GAAgB,OAAO,aAAA,IAAiB,CAAA;AAC9C,EAAA,MAAM,WAAW,MAAA,CAAO,QAAA;AACxB,EAAA,MAAM,YAAA,GAAe,OAAO,YAAA,IAAgB,GAAA;AAC5C,EAAA,MAAM,EAAE,WAAU,GAAI,MAAA;AAItB,EAAA,MAAM,WAAA,uBAAkB,GAAA,EAA0B;AAClD,EAAA,MAAM,QAAwB,EAAC;AAC/B,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAoB;AAC1C,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAA2C;AACjE,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAA0B;AAEhD,EAAA,SAAS,OAAO,KAAA,EAAkD;AAChE,IAAA,KAAA,MAAW,YAAY,SAAA,EAAW;AAChC,MAAA,IAAI;AACF,QAAA,QAAA,CAAS,KAAK,CAAA;AAAA,MAChB,SAAS,GAAA,EAAK;AACZ,QAAA,IAAI,OAAA,CAAQ,GAAA,CAAI,UAAU,CAAA,KAAM,YAAA,EAAc;AAC5C,UAAA,OAAA,CAAQ,KAAA,CAAM,iDAAiD,GAAG,CAAA;AAAA,QACpE;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,EAAA,SAAS,gBAAA,GAAyB;AAChC,IAAA,IAAI,KAAA,CAAM,MAAA,GAAS,CAAA,IAAK,WAAA,CAAY,OAAO,aAAA,EAAe;AACxD,MAAA,MAAM,IAAA,GAAO,MAAM,KAAA,EAAM;AACzB,MAAA,YAAA,CAAa,IAAA,EAAM,KAAK,cAA0B,CAAA;AAAA,IACpD;AAAA,EACF;AAEA,EAAA,SAAS,YAAA,CAAa,MAAoB,MAAA,EAAwB;AAChE,IAAA,IAAA,CAAK,KAAA,GAAQ,QAAA;AACb,IAAA,WAAA,CAAY,GAAA,CAAI,IAAA,CAAK,WAAA,EAAa,IAAI,CAAA;AAGtC,IAAA,MAAM,MAAM,IAAA,CAAK,GAAA;AACjB,IAAA,IAAI,GAAA,IAAO,IAAA,IAAQ,GAAA,GAAM,CAAA,EAAG;AAC1B,MAAA,MAAM,KAAA,GAAQ,WAAW,MAAM;AAC7B,QAAA,IAAI,WAAA,CAAY,GAAA,CAAI,IAAA,CAAK,WAAW,CAAA,EAAG;AACrC,UAAA,WAAA,CAAY,MAAA,CAAO,KAAK,WAAW,CAAA;AACnC,UAAA,SAAA,CAAU,MAAA,CAAO,KAAK,WAAW,CAAA;AACjC,UAAA,SAAA,GAAY,IAAA,CAAK,OAAO,aAAa,CAAA;AACrC,UAAA,gBAAA,EAAiB;AAAA,QACnB;AAAA,MACF,GAAG,GAAG,CAAA;AACN,MAAA,SAAA,CAAU,GAAA,CAAI,IAAA,CAAK,WAAA,EAAa,KAAK,CAAA;AAAA,IACvC;AAEA,IAAA,MAAA,CAAO,EAAE,IAAA,EAAM,aAAA,EAAe,OAAO,IAAA,CAAK,KAAA,EAAO,QAAQ,CAAA;AAAA,EAC3D;AAEA,EAAA,SAAS,UAAU,WAAA,EAA8B;AAC/C,IAAA,MAAM,QAAA,GAAW,SAAA,CAAU,GAAA,CAAI,WAAW,CAAA;AAC1C,IAAA,IAAI,QAAA,KAAa,QAAW,OAAO,IAAA;AACnC,IAAA,OAAO,IAAA,CAAK,GAAA,EAAI,GAAI,QAAA,IAAY,YAAA;AAAA,EAClC;AAEA,EAAA,SAAS,QAAQ,IAAA,EAAsD;AACrE,IAAA,IAAI,CAAC,SAAA,CAAU,IAAA,CAAK,WAAW,CAAA,EAAG;AAChC,MAAA,SAAA,GAAY,IAAA,CAAK,OAAO,QAAQ,CAAA;AAChC,MAAA,OAAO,UAAA;AAAA,IACT;AAEA,IAAA,SAAA,CAAU,GAAA,CAAI,IAAA,CAAK,WAAA,EAAa,IAAA,CAAK,KAAK,CAAA;AAE1C,IAAA,IAAI,WAAA,CAAY,OAAO,aAAA,EAAe;AACpC,MAAA,YAAA,CAAa,IAAA,EAAM,KAAK,cAA0B,CAAA;AAClD,MAAA,OAAO,QAAA;AAAA,IACT;AAEA,IAAA,IAAI,QAAA,KAAa,MAAA,IAAa,KAAA,CAAM,MAAA,IAAU,QAAA,EAAU;AACtD,MAAA,SAAA,GAAY,IAAA,CAAK,OAAO,gBAAgB,CAAA;AACxC,MAAA,OAAO,UAAA;AAAA,IACT;AAEA,IAAA,IAAA,CAAK,KAAA,GAAQ,QAAA;AACb,IAAA,KAAA,CAAM,KAAK,IAAI,CAAA;AACf,IAAA,OAAO,QAAA;AAAA,EACT;AAEA,EAAA,SAAS,QAAQ,IAAA,EAAmB;AAElC,IAAA,IAAI,iBAAA;AAEJ,IAAA,KAAA,MAAW,CAAC,WAAA,EAAa,IAAI,CAAA,IAAK,WAAA,EAAa;AAC7C,MAAA,IAAI,IAAA,CAAK,KAAA,CAAM,IAAA,KAAS,IAAA,EAAM;AAC5B,QAAA,iBAAA,GAAoB,WAAA;AACpB,QAAA;AAAA,MACF;AAAA,IACF;AAEA,IAAA,IAAI,sBAAsB,MAAA,EAAW;AACnC,MAAA,MAAM,IAAA,GAAO,WAAA,CAAY,GAAA,CAAI,iBAAiB,CAAA;AAC9C,MAAA,IAAA,CAAK,KAAA,GAAQ,SAAA;AACb,MAAA,WAAA,CAAY,OAAO,iBAAiB,CAAA;AAEpC,MAAA,MAAM,KAAA,GAAQ,SAAA,CAAU,GAAA,CAAI,iBAAiB,CAAA;AAC7C,MAAA,IAAI,UAAU,MAAA,EAAW;AACvB,QAAA,YAAA,CAAa,KAAK,CAAA;AAClB,QAAA,SAAA,CAAU,OAAO,iBAAiB,CAAA;AAAA,MACpC;AAEA,MAAA,MAAA,CAAO,EAAE,IAAA,EAAM,eAAA,EAAiB,IAAA,EAAM,CAAA;AACtC,MAAA,gBAAA,EAAiB;AACjB,MAAA;AAAA,IACF;AAGA,IAAA,MAAM,QAAA,GAAW,MAAM,SAAA,CAAU,CAAC,SAAS,IAAA,CAAK,KAAA,CAAM,SAAS,IAAI,CAAA;AACnE,IAAA,IAAI,aAAa,EAAA,EAAI;AACnB,MAAA,MAAM,IAAA,GAAO,MAAM,QAAQ,CAAA;AAC3B,MAAA,IAAA,CAAK,KAAA,GAAQ,SAAA;AACb,MAAA,KAAA,CAAM,MAAA,CAAO,UAAU,CAAC,CAAA;AACxB,MAAA,MAAA,CAAO,EAAE,IAAA,EAAM,eAAA,EAAiB,IAAA,EAAM,CAAA;AAAA,IACxC;AAAA,EACF;AAEA,EAAA,SAAS,cAAA,GAAqC;AAC5C,IAAA,OAAO,KAAA,CAAM,IAAA,CAAK,WAAA,CAAY,MAAA,EAAQ,CAAA;AAAA,EACxC;AAEA,EAAA,SAAS,cAAA,GAAyB;AAChC,IAAA,OAAO,KAAA,CAAM,MAAA;AAAA,EACf;AAEA,EAAA,SAAS,QAAA,GAAiB;AACxB,IAAA,KAAA,MAAW,KAAA,IAAS,SAAA,CAAU,MAAA,EAAO,EAAG;AACtC,MAAA,YAAA,CAAa,KAAK,CAAA;AAAA,IACpB;AACA,IAAA,SAAA,CAAU,KAAA,EAAM;AAChB,IAAA,WAAA,CAAY,KAAA,EAAM;AAClB,IAAA,KAAA,CAAM,MAAA,GAAS,CAAA;AACf,IAAA,MAAA,CAAO,EAAE,IAAA,EAAM,aAAA,EAAe,CAAA;AAAA,EAChC;AAEA,EAAA,SAAS,UAAU,QAAA,EAA4C;AAC7D,IAAA,SAAA,CAAU,IAAI,QAAQ,CAAA;AACtB,IAAA,OAAO,MAAM;AAAE,MAAA,SAAA,CAAU,OAAO,QAAQ,CAAA;AAAA,IAAE,CAAA;AAAA,EAC5C;AAEA,EAAA,OAAO,EAAE,SAAA,EAAW,OAAA,EAAS,SAAS,cAAA,EAAgB,cAAA,EAAgB,UAAU,SAAA,EAAU;AAC5F;;;ACzJO,SAAS,WAAA,CACd,UACA,IAAA,EAC2C;AAC3C,EAAA,OAAQ,SAAuE,IAAI,CAAA;AACrF;;;ACJO,SAAS,cAAA,GAAgG;AAC9G,EAAA,SAAS,KAAA,CACP,KAAA,EACA,QAAA,EACA,MAAA,EAGiB;AACjB,IAAA,MAAM,KAAA,GAAQ,WAAA,CAAY,QAAA,EAAU,KAAA,CAAM,IAAI,CAAA;AAE9C,IAAA,IAAI,KAAA,IAAS,IAAA,IAAQ,MAAA,CAAO,eAAA,EAAiB;AAC3C,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,mDAAA,EAAsD,KAAA,CAAM,IAAI,CAAA,CAAE,CAAA;AAAA,IACpF;AAEA,IAAA,MAAM,cAAA,GAAkB,OAA6E,cAAA,IAAkB;AAAA,MACrH,WAAA,EAAa,CAAA;AAAA,MACb,WAAA,EAAa;AAAA,KACf;AAEA,IAAA,IAAI,cAAA;AACJ,IAAA,IAAI,OAAO,eAAA,EAAiB;AAC1B,MAAA,IAAI;AACF,QAAA,cAAA,GAAiB,MAAA,CAAO,eAAA,CAAgB,KAAA,EAAO,KAAA,EAAO,cAAc,CAAA;AAAA,MACtE,SAAS,GAAA,EAAK;AACZ,QAAA,IAAI,OAAA,CAAQ,GAAA,CAAI,UAAU,CAAA,KAAM,YAAA,EAAc;AAC5C,UAAA,OAAA,CAAQ,KAAA,CAAM,2CAA2C,GAAG,CAAA;AAAA,QAC9D;AACA,QAAA,cAAA,GAAiB,IAAA;AAAA,MACnB;AAAA,IACF;AAGA,IAAA,IAAI,kBAAkB,IAAA,EAAM;AAC1B,MAAA,OAAO,cAAA;AAAA,IACT;AAGA,IAAA,IAAI,SAAS,IAAA,EAAM;AACjB,MAAA,OAAO,KAAA,CAAM,EAAA;AAAA,IACf;AAIA,IAAA,MAAM,aAAA,GAAgB,OAAO,aAAA,KAAkB,KAAA;AAE/C,IAAA,IAAI,aAAA,IAAiB,OAAO,QAAA,EAAU;AACpC,MAAA,OAAO,OAAO,QAAA,CAAS,EAAA;AAAA,IACzB;AAEA,IAAA,OAAO,OAAA;AAAA,EACT;AAEA,EAAA,OAAO,EAAE,KAAA,EAAM;AACjB","file":"internal.cjs","sourcesContent":["import type { AppError, ErrorSlot, ErrorStateManager, StateListener, UIAction } from './types'\n\nexport interface StateManagerConfig<TCode extends string> {\n maxConcurrent?: number // default: 3\n maxQueue?: number // default: unbounded\n dedupeWindow?: number // ms, default: 300\n onDropped?: (error: AppError<TCode>, reason: 'dedupe' | 'ttl_expired' | 'queue_overflow') => void\n}\n\nexport function createStateManager<TCode extends string>(\n config: StateManagerConfig<TCode>\n): ErrorStateManager<TCode> {\n const maxConcurrent = config.maxConcurrent ?? 3\n const maxQueue = config.maxQueue\n const dedupeWindow = config.dedupeWindow ?? 300\n const { onDropped } = config\n\n type InternalSlot = ErrorSlot<TCode> & { ttl?: number; _pendingAction?: UIAction }\n\n const activeSlots = new Map<string, InternalSlot>()\n const queue: InternalSlot[] = []\n const dedupeMap = new Map<string, number>()\n const ttlTimers = new Map<string, ReturnType<typeof setTimeout>>()\n const listeners = new Set<StateListener<TCode>>()\n\n function notify(event: Parameters<StateListener<TCode>>[0]): void {\n for (const listener of listeners) {\n try {\n listener(event)\n } catch (err) {\n if (process.env['NODE_ENV'] !== 'production') {\n console.error('[gracefulerrors] StateManager listener threw:', err)\n }\n }\n }\n }\n\n function promoteFromQueue(): void {\n if (queue.length > 0 && activeSlots.size < maxConcurrent) {\n const next = queue.shift()!\n activateSlot(next, next._pendingAction as UIAction)\n }\n }\n\n function activateSlot(slot: InternalSlot, action: UIAction): void {\n slot.state = 'ACTIVE'\n activeSlots.set(slot.fingerprint, slot)\n\n // Start TTL timer if configured\n const ttl = slot.ttl\n if (ttl != null && ttl > 0) {\n const timer = setTimeout(() => {\n if (activeSlots.has(slot.fingerprint)) {\n activeSlots.delete(slot.fingerprint)\n ttlTimers.delete(slot.fingerprint)\n onDropped?.(slot.error, 'ttl_expired')\n promoteFromQueue()\n }\n }, ttl)\n ttlTimers.set(slot.fingerprint, timer)\n }\n\n notify({ type: 'ERROR_ADDED', error: slot.error, action })\n }\n\n function canHandle(fingerprint: string): boolean {\n const lastSeen = dedupeMap.get(fingerprint)\n if (lastSeen === undefined) return true\n return Date.now() - lastSeen >= dedupeWindow\n }\n\n function enqueue(slot: InternalSlot): 'active' | 'queued' | 'rejected' {\n if (!canHandle(slot.fingerprint)) {\n onDropped?.(slot.error, 'dedupe')\n return 'rejected'\n }\n\n dedupeMap.set(slot.fingerprint, Date.now())\n\n if (activeSlots.size < maxConcurrent) {\n activateSlot(slot, slot._pendingAction as UIAction)\n return 'active'\n }\n\n if (maxQueue !== undefined && queue.length >= maxQueue) {\n onDropped?.(slot.error, 'queue_overflow')\n return 'rejected'\n }\n\n slot.state = 'QUEUED'\n queue.push(slot)\n return 'queued'\n }\n\n function release(code: TCode): void {\n // Check active slots first\n let targetFingerprint: string | undefined\n\n for (const [fingerprint, slot] of activeSlots) {\n if (slot.error.code === code) {\n targetFingerprint = fingerprint\n break\n }\n }\n\n if (targetFingerprint !== undefined) {\n const slot = activeSlots.get(targetFingerprint)!\n slot.state = 'EXPIRED'\n activeSlots.delete(targetFingerprint)\n\n const timer = ttlTimers.get(targetFingerprint)\n if (timer !== undefined) {\n clearTimeout(timer)\n ttlTimers.delete(targetFingerprint)\n }\n\n notify({ type: 'ERROR_CLEARED', code })\n promoteFromQueue()\n return\n }\n\n // Check queue — spec: QUEUED | clear() → EXPIRED | remove from queue\n const queueIdx = queue.findIndex((slot) => slot.error.code === code)\n if (queueIdx !== -1) {\n const slot = queue[queueIdx]!\n slot.state = 'EXPIRED'\n queue.splice(queueIdx, 1)\n notify({ type: 'ERROR_CLEARED', code })\n }\n }\n\n function getActiveSlots(): ErrorSlot<TCode>[] {\n return Array.from(activeSlots.values())\n }\n\n function getQueueLength(): number {\n return queue.length\n }\n\n function clearAll(): void {\n for (const timer of ttlTimers.values()) {\n clearTimeout(timer)\n }\n ttlTimers.clear()\n activeSlots.clear()\n queue.length = 0\n notify({ type: 'ALL_CLEARED' })\n }\n\n function subscribe(listener: StateListener<TCode>): () => void {\n listeners.add(listener)\n return () => { listeners.delete(listener) }\n }\n\n return { canHandle, enqueue, release, getActiveSlots, getQueueLength, clearAll, subscribe }\n}\n\nexport type { ErrorStateManager, StateListener }\n","import type { AppError, ErrorRegistry, ErrorRegistryEntry, ErrorRegistryEntryFull } from './types'\n\nexport function lookupEntry<TCode extends string>(\n registry: ErrorRegistry<TCode>,\n code: TCode | string\n): ErrorRegistryEntryFull<TCode> | undefined {\n return (registry as Record<string, ErrorRegistryEntryFull<TCode> | undefined>)[code]\n}\n\nexport function resolveMessage<TCode extends string>(\n entry: ErrorRegistryEntry<TCode>,\n error: AppError<TCode>\n): string | undefined {\n if (entry.message === undefined) return undefined\n if (typeof entry.message === 'function') return entry.message(error)\n return entry.message\n}\n\nexport function mergeRegistries<TBase extends string, TExtended extends string>(\n base: ErrorRegistry<TBase>,\n override: ErrorRegistry<TExtended>\n): ErrorRegistry<TBase | TExtended> {\n return { ...base, ...override } as ErrorRegistry<TBase | TExtended>\n}\n\nexport type { ErrorRegistry, ErrorRegistryEntry, ErrorRegistryEntryFull }\n","import type { AppError, ErrorEngineConfig, ErrorRegistry, UIAction, UIRouter } from './types'\nimport { lookupEntry } from './registry'\n\nexport function createUIRouter<TCode extends string, TField extends string = string>(): UIRouter<TCode, TField> {\n function route(\n error: AppError<TCode, TField>,\n registry: ErrorRegistry<TCode>,\n config: Pick<ErrorEngineConfig<TCode, TField>, 'fallback' | 'requireRegistry' | 'allowFallback' | 'routingStrategy'> & {\n routingContext?: { activeCount: number; queueLength: number }\n }\n ): UIAction | null {\n const entry = lookupEntry(registry, error.code)\n\n if (entry == null && config.requireRegistry) {\n throw new Error(`[gracefulerrors] Registry entry required for code: ${error.code}`)\n }\n\n const routingContext = (config as { routingContext?: { activeCount: number; queueLength: number } }).routingContext ?? {\n activeCount: 0,\n queueLength: 0,\n }\n\n let strategyResult: UIAction | null | undefined\n if (config.routingStrategy) {\n try {\n strategyResult = config.routingStrategy(error, entry, routingContext)\n } catch (err) {\n if (process.env['NODE_ENV'] !== 'production') {\n console.error('[gracefulerrors] routingStrategy threw:', err)\n }\n strategyResult = null\n }\n }\n\n // routingStrategy returns non-null → use it (overrides registry)\n if (strategyResult != null) {\n return strategyResult\n }\n\n // Registry entry exists → use its ui\n if (entry != null) {\n return entry.ui\n }\n\n // No match — apply fallback logic\n // allowFallback defaults to true\n const allowFallback = config.allowFallback !== false\n\n if (allowFallback && config.fallback) {\n return config.fallback.ui\n }\n\n return 'toast'\n }\n\n return { route }\n}\n\nexport type { UIRouter }\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { A as AppError, i as ErrorStateManager, j as UIRouter } from './types-CsPmpcbL.cjs';
|
|
2
|
+
export { k as ErrorSlot, l as ErrorState, S as StateListener } from './types-CsPmpcbL.cjs';
|
|
3
|
+
|
|
4
|
+
interface StateManagerConfig<TCode extends string> {
|
|
5
|
+
maxConcurrent?: number;
|
|
6
|
+
maxQueue?: number;
|
|
7
|
+
dedupeWindow?: number;
|
|
8
|
+
onDropped?: (error: AppError<TCode>, reason: 'dedupe' | 'ttl_expired' | 'queue_overflow') => void;
|
|
9
|
+
}
|
|
10
|
+
declare function createStateManager<TCode extends string>(config: StateManagerConfig<TCode>): ErrorStateManager<TCode>;
|
|
11
|
+
|
|
12
|
+
declare function createUIRouter<TCode extends string, TField extends string = string>(): UIRouter<TCode, TField>;
|
|
13
|
+
|
|
14
|
+
export { ErrorStateManager, UIRouter, createStateManager, createUIRouter };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { A as AppError, i as ErrorStateManager, j as UIRouter } from './types-CsPmpcbL.js';
|
|
2
|
+
export { k as ErrorSlot, l as ErrorState, S as StateListener } from './types-CsPmpcbL.js';
|
|
3
|
+
|
|
4
|
+
interface StateManagerConfig<TCode extends string> {
|
|
5
|
+
maxConcurrent?: number;
|
|
6
|
+
maxQueue?: number;
|
|
7
|
+
dedupeWindow?: number;
|
|
8
|
+
onDropped?: (error: AppError<TCode>, reason: 'dedupe' | 'ttl_expired' | 'queue_overflow') => void;
|
|
9
|
+
}
|
|
10
|
+
declare function createStateManager<TCode extends string>(config: StateManagerConfig<TCode>): ErrorStateManager<TCode>;
|
|
11
|
+
|
|
12
|
+
declare function createUIRouter<TCode extends string, TField extends string = string>(): UIRouter<TCode, TField>;
|
|
13
|
+
|
|
14
|
+
export { ErrorStateManager, UIRouter, createStateManager, createUIRouter };
|
package/dist/internal.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// src/state-manager.ts
|
|
2
|
+
function createStateManager(config) {
|
|
3
|
+
const maxConcurrent = config.maxConcurrent ?? 3;
|
|
4
|
+
const maxQueue = config.maxQueue;
|
|
5
|
+
const dedupeWindow = config.dedupeWindow ?? 300;
|
|
6
|
+
const { onDropped } = config;
|
|
7
|
+
const activeSlots = /* @__PURE__ */ new Map();
|
|
8
|
+
const queue = [];
|
|
9
|
+
const dedupeMap = /* @__PURE__ */ new Map();
|
|
10
|
+
const ttlTimers = /* @__PURE__ */ new Map();
|
|
11
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
12
|
+
function notify(event) {
|
|
13
|
+
for (const listener of listeners) {
|
|
14
|
+
try {
|
|
15
|
+
listener(event);
|
|
16
|
+
} catch (err) {
|
|
17
|
+
if (process.env["NODE_ENV"] !== "production") {
|
|
18
|
+
console.error("[gracefulerrors] StateManager listener threw:", err);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function promoteFromQueue() {
|
|
24
|
+
if (queue.length > 0 && activeSlots.size < maxConcurrent) {
|
|
25
|
+
const next = queue.shift();
|
|
26
|
+
activateSlot(next, next._pendingAction);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function activateSlot(slot, action) {
|
|
30
|
+
slot.state = "ACTIVE";
|
|
31
|
+
activeSlots.set(slot.fingerprint, slot);
|
|
32
|
+
const ttl = slot.ttl;
|
|
33
|
+
if (ttl != null && ttl > 0) {
|
|
34
|
+
const timer = setTimeout(() => {
|
|
35
|
+
if (activeSlots.has(slot.fingerprint)) {
|
|
36
|
+
activeSlots.delete(slot.fingerprint);
|
|
37
|
+
ttlTimers.delete(slot.fingerprint);
|
|
38
|
+
onDropped?.(slot.error, "ttl_expired");
|
|
39
|
+
promoteFromQueue();
|
|
40
|
+
}
|
|
41
|
+
}, ttl);
|
|
42
|
+
ttlTimers.set(slot.fingerprint, timer);
|
|
43
|
+
}
|
|
44
|
+
notify({ type: "ERROR_ADDED", error: slot.error, action });
|
|
45
|
+
}
|
|
46
|
+
function canHandle(fingerprint) {
|
|
47
|
+
const lastSeen = dedupeMap.get(fingerprint);
|
|
48
|
+
if (lastSeen === void 0) return true;
|
|
49
|
+
return Date.now() - lastSeen >= dedupeWindow;
|
|
50
|
+
}
|
|
51
|
+
function enqueue(slot) {
|
|
52
|
+
if (!canHandle(slot.fingerprint)) {
|
|
53
|
+
onDropped?.(slot.error, "dedupe");
|
|
54
|
+
return "rejected";
|
|
55
|
+
}
|
|
56
|
+
dedupeMap.set(slot.fingerprint, Date.now());
|
|
57
|
+
if (activeSlots.size < maxConcurrent) {
|
|
58
|
+
activateSlot(slot, slot._pendingAction);
|
|
59
|
+
return "active";
|
|
60
|
+
}
|
|
61
|
+
if (maxQueue !== void 0 && queue.length >= maxQueue) {
|
|
62
|
+
onDropped?.(slot.error, "queue_overflow");
|
|
63
|
+
return "rejected";
|
|
64
|
+
}
|
|
65
|
+
slot.state = "QUEUED";
|
|
66
|
+
queue.push(slot);
|
|
67
|
+
return "queued";
|
|
68
|
+
}
|
|
69
|
+
function release(code) {
|
|
70
|
+
let targetFingerprint;
|
|
71
|
+
for (const [fingerprint, slot] of activeSlots) {
|
|
72
|
+
if (slot.error.code === code) {
|
|
73
|
+
targetFingerprint = fingerprint;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (targetFingerprint !== void 0) {
|
|
78
|
+
const slot = activeSlots.get(targetFingerprint);
|
|
79
|
+
slot.state = "EXPIRED";
|
|
80
|
+
activeSlots.delete(targetFingerprint);
|
|
81
|
+
const timer = ttlTimers.get(targetFingerprint);
|
|
82
|
+
if (timer !== void 0) {
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
ttlTimers.delete(targetFingerprint);
|
|
85
|
+
}
|
|
86
|
+
notify({ type: "ERROR_CLEARED", code });
|
|
87
|
+
promoteFromQueue();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const queueIdx = queue.findIndex((slot) => slot.error.code === code);
|
|
91
|
+
if (queueIdx !== -1) {
|
|
92
|
+
const slot = queue[queueIdx];
|
|
93
|
+
slot.state = "EXPIRED";
|
|
94
|
+
queue.splice(queueIdx, 1);
|
|
95
|
+
notify({ type: "ERROR_CLEARED", code });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function getActiveSlots() {
|
|
99
|
+
return Array.from(activeSlots.values());
|
|
100
|
+
}
|
|
101
|
+
function getQueueLength() {
|
|
102
|
+
return queue.length;
|
|
103
|
+
}
|
|
104
|
+
function clearAll() {
|
|
105
|
+
for (const timer of ttlTimers.values()) {
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
}
|
|
108
|
+
ttlTimers.clear();
|
|
109
|
+
activeSlots.clear();
|
|
110
|
+
queue.length = 0;
|
|
111
|
+
notify({ type: "ALL_CLEARED" });
|
|
112
|
+
}
|
|
113
|
+
function subscribe(listener) {
|
|
114
|
+
listeners.add(listener);
|
|
115
|
+
return () => {
|
|
116
|
+
listeners.delete(listener);
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return { canHandle, enqueue, release, getActiveSlots, getQueueLength, clearAll, subscribe };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/registry.ts
|
|
123
|
+
function lookupEntry(registry, code) {
|
|
124
|
+
return registry[code];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/router.ts
|
|
128
|
+
function createUIRouter() {
|
|
129
|
+
function route(error, registry, config) {
|
|
130
|
+
const entry = lookupEntry(registry, error.code);
|
|
131
|
+
if (entry == null && config.requireRegistry) {
|
|
132
|
+
throw new Error(`[gracefulerrors] Registry entry required for code: ${error.code}`);
|
|
133
|
+
}
|
|
134
|
+
const routingContext = config.routingContext ?? {
|
|
135
|
+
activeCount: 0,
|
|
136
|
+
queueLength: 0
|
|
137
|
+
};
|
|
138
|
+
let strategyResult;
|
|
139
|
+
if (config.routingStrategy) {
|
|
140
|
+
try {
|
|
141
|
+
strategyResult = config.routingStrategy(error, entry, routingContext);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
if (process.env["NODE_ENV"] !== "production") {
|
|
144
|
+
console.error("[gracefulerrors] routingStrategy threw:", err);
|
|
145
|
+
}
|
|
146
|
+
strategyResult = null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (strategyResult != null) {
|
|
150
|
+
return strategyResult;
|
|
151
|
+
}
|
|
152
|
+
if (entry != null) {
|
|
153
|
+
return entry.ui;
|
|
154
|
+
}
|
|
155
|
+
const allowFallback = config.allowFallback !== false;
|
|
156
|
+
if (allowFallback && config.fallback) {
|
|
157
|
+
return config.fallback.ui;
|
|
158
|
+
}
|
|
159
|
+
return "toast";
|
|
160
|
+
}
|
|
161
|
+
return { route };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export { createStateManager, createUIRouter };
|
|
165
|
+
//# sourceMappingURL=internal.js.map
|
|
166
|
+
//# sourceMappingURL=internal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/state-manager.ts","../src/registry.ts","../src/router.ts"],"names":[],"mappings":"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","file":"internal.js","sourcesContent":["import type { AppError, ErrorSlot, ErrorStateManager, StateListener, UIAction } from './types'\n\nexport interface StateManagerConfig<TCode extends string> {\n maxConcurrent?: number // default: 3\n maxQueue?: number // default: unbounded\n dedupeWindow?: number // ms, default: 300\n onDropped?: (error: AppError<TCode>, reason: 'dedupe' | 'ttl_expired' | 'queue_overflow') => void\n}\n\nexport function createStateManager<TCode extends string>(\n config: StateManagerConfig<TCode>\n): ErrorStateManager<TCode> {\n const maxConcurrent = config.maxConcurrent ?? 3\n const maxQueue = config.maxQueue\n const dedupeWindow = config.dedupeWindow ?? 300\n const { onDropped } = config\n\n type InternalSlot = ErrorSlot<TCode> & { ttl?: number; _pendingAction?: UIAction }\n\n const activeSlots = new Map<string, InternalSlot>()\n const queue: InternalSlot[] = []\n const dedupeMap = new Map<string, number>()\n const ttlTimers = new Map<string, ReturnType<typeof setTimeout>>()\n const listeners = new Set<StateListener<TCode>>()\n\n function notify(event: Parameters<StateListener<TCode>>[0]): void {\n for (const listener of listeners) {\n try {\n listener(event)\n } catch (err) {\n if (process.env['NODE_ENV'] !== 'production') {\n console.error('[gracefulerrors] StateManager listener threw:', err)\n }\n }\n }\n }\n\n function promoteFromQueue(): void {\n if (queue.length > 0 && activeSlots.size < maxConcurrent) {\n const next = queue.shift()!\n activateSlot(next, next._pendingAction as UIAction)\n }\n }\n\n function activateSlot(slot: InternalSlot, action: UIAction): void {\n slot.state = 'ACTIVE'\n activeSlots.set(slot.fingerprint, slot)\n\n // Start TTL timer if configured\n const ttl = slot.ttl\n if (ttl != null && ttl > 0) {\n const timer = setTimeout(() => {\n if (activeSlots.has(slot.fingerprint)) {\n activeSlots.delete(slot.fingerprint)\n ttlTimers.delete(slot.fingerprint)\n onDropped?.(slot.error, 'ttl_expired')\n promoteFromQueue()\n }\n }, ttl)\n ttlTimers.set(slot.fingerprint, timer)\n }\n\n notify({ type: 'ERROR_ADDED', error: slot.error, action })\n }\n\n function canHandle(fingerprint: string): boolean {\n const lastSeen = dedupeMap.get(fingerprint)\n if (lastSeen === undefined) return true\n return Date.now() - lastSeen >= dedupeWindow\n }\n\n function enqueue(slot: InternalSlot): 'active' | 'queued' | 'rejected' {\n if (!canHandle(slot.fingerprint)) {\n onDropped?.(slot.error, 'dedupe')\n return 'rejected'\n }\n\n dedupeMap.set(slot.fingerprint, Date.now())\n\n if (activeSlots.size < maxConcurrent) {\n activateSlot(slot, slot._pendingAction as UIAction)\n return 'active'\n }\n\n if (maxQueue !== undefined && queue.length >= maxQueue) {\n onDropped?.(slot.error, 'queue_overflow')\n return 'rejected'\n }\n\n slot.state = 'QUEUED'\n queue.push(slot)\n return 'queued'\n }\n\n function release(code: TCode): void {\n // Check active slots first\n let targetFingerprint: string | undefined\n\n for (const [fingerprint, slot] of activeSlots) {\n if (slot.error.code === code) {\n targetFingerprint = fingerprint\n break\n }\n }\n\n if (targetFingerprint !== undefined) {\n const slot = activeSlots.get(targetFingerprint)!\n slot.state = 'EXPIRED'\n activeSlots.delete(targetFingerprint)\n\n const timer = ttlTimers.get(targetFingerprint)\n if (timer !== undefined) {\n clearTimeout(timer)\n ttlTimers.delete(targetFingerprint)\n }\n\n notify({ type: 'ERROR_CLEARED', code })\n promoteFromQueue()\n return\n }\n\n // Check queue — spec: QUEUED | clear() → EXPIRED | remove from queue\n const queueIdx = queue.findIndex((slot) => slot.error.code === code)\n if (queueIdx !== -1) {\n const slot = queue[queueIdx]!\n slot.state = 'EXPIRED'\n queue.splice(queueIdx, 1)\n notify({ type: 'ERROR_CLEARED', code })\n }\n }\n\n function getActiveSlots(): ErrorSlot<TCode>[] {\n return Array.from(activeSlots.values())\n }\n\n function getQueueLength(): number {\n return queue.length\n }\n\n function clearAll(): void {\n for (const timer of ttlTimers.values()) {\n clearTimeout(timer)\n }\n ttlTimers.clear()\n activeSlots.clear()\n queue.length = 0\n notify({ type: 'ALL_CLEARED' })\n }\n\n function subscribe(listener: StateListener<TCode>): () => void {\n listeners.add(listener)\n return () => { listeners.delete(listener) }\n }\n\n return { canHandle, enqueue, release, getActiveSlots, getQueueLength, clearAll, subscribe }\n}\n\nexport type { ErrorStateManager, StateListener }\n","import type { AppError, ErrorRegistry, ErrorRegistryEntry, ErrorRegistryEntryFull } from './types'\n\nexport function lookupEntry<TCode extends string>(\n registry: ErrorRegistry<TCode>,\n code: TCode | string\n): ErrorRegistryEntryFull<TCode> | undefined {\n return (registry as Record<string, ErrorRegistryEntryFull<TCode> | undefined>)[code]\n}\n\nexport function resolveMessage<TCode extends string>(\n entry: ErrorRegistryEntry<TCode>,\n error: AppError<TCode>\n): string | undefined {\n if (entry.message === undefined) return undefined\n if (typeof entry.message === 'function') return entry.message(error)\n return entry.message\n}\n\nexport function mergeRegistries<TBase extends string, TExtended extends string>(\n base: ErrorRegistry<TBase>,\n override: ErrorRegistry<TExtended>\n): ErrorRegistry<TBase | TExtended> {\n return { ...base, ...override } as ErrorRegistry<TBase | TExtended>\n}\n\nexport type { ErrorRegistry, ErrorRegistryEntry, ErrorRegistryEntryFull }\n","import type { AppError, ErrorEngineConfig, ErrorRegistry, UIAction, UIRouter } from './types'\nimport { lookupEntry } from './registry'\n\nexport function createUIRouter<TCode extends string, TField extends string = string>(): UIRouter<TCode, TField> {\n function route(\n error: AppError<TCode, TField>,\n registry: ErrorRegistry<TCode>,\n config: Pick<ErrorEngineConfig<TCode, TField>, 'fallback' | 'requireRegistry' | 'allowFallback' | 'routingStrategy'> & {\n routingContext?: { activeCount: number; queueLength: number }\n }\n ): UIAction | null {\n const entry = lookupEntry(registry, error.code)\n\n if (entry == null && config.requireRegistry) {\n throw new Error(`[gracefulerrors] Registry entry required for code: ${error.code}`)\n }\n\n const routingContext = (config as { routingContext?: { activeCount: number; queueLength: number } }).routingContext ?? {\n activeCount: 0,\n queueLength: 0,\n }\n\n let strategyResult: UIAction | null | undefined\n if (config.routingStrategy) {\n try {\n strategyResult = config.routingStrategy(error, entry, routingContext)\n } catch (err) {\n if (process.env['NODE_ENV'] !== 'production') {\n console.error('[gracefulerrors] routingStrategy threw:', err)\n }\n strategyResult = null\n }\n }\n\n // routingStrategy returns non-null → use it (overrides registry)\n if (strategyResult != null) {\n return strategyResult\n }\n\n // Registry entry exists → use its ui\n if (entry != null) {\n return entry.ui\n }\n\n // No match — apply fallback logic\n // allowFallback defaults to true\n const allowFallback = config.allowFallback !== false\n\n if (allowFallback && config.fallback) {\n return config.fallback.ui\n }\n\n return 'toast'\n }\n\n return { route }\n}\n\nexport type { UIRouter }\n"]}
|
package/dist/react.cjs
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var React = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
|
|
6
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
+
|
|
8
|
+
var React__default = /*#__PURE__*/_interopDefault(React);
|
|
9
|
+
|
|
10
|
+
var ErrorEngineContext = React.createContext(null);
|
|
11
|
+
function ErrorEngineProvider({
|
|
12
|
+
engine,
|
|
13
|
+
children
|
|
14
|
+
}) {
|
|
15
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ErrorEngineContext.Provider, { value: { engine }, children });
|
|
16
|
+
}
|
|
17
|
+
function useErrorEngine() {
|
|
18
|
+
const ctx = React.useContext(ErrorEngineContext);
|
|
19
|
+
if (!ctx) {
|
|
20
|
+
if (process.env["NODE_ENV"] === "development") {
|
|
21
|
+
console.error("[gracefulerrors] useErrorEngine called outside of ErrorEngineProvider.");
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return ctx.engine;
|
|
26
|
+
}
|
|
27
|
+
function useFieldError(field) {
|
|
28
|
+
const engine = useErrorEngine();
|
|
29
|
+
const [error, setError] = React.useState(null);
|
|
30
|
+
React.useEffect(() => {
|
|
31
|
+
if (!engine) return;
|
|
32
|
+
const unsubscribe = engine.subscribe((event) => {
|
|
33
|
+
if (event.type === "ERROR_ADDED" && event.action === "inline") {
|
|
34
|
+
if (event.error.context?.field === field) {
|
|
35
|
+
setError(event.error);
|
|
36
|
+
}
|
|
37
|
+
} else if (event.type === "ERROR_CLEARED") {
|
|
38
|
+
setError((prev) => prev?.code === event.code ? null : prev);
|
|
39
|
+
} else if (event.type === "ALL_CLEARED") {
|
|
40
|
+
setError(null);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return unsubscribe;
|
|
44
|
+
}, [engine, field]);
|
|
45
|
+
return { error };
|
|
46
|
+
}
|
|
47
|
+
var ErrorBoundaryWithEngine = class extends React__default.default.Component {
|
|
48
|
+
constructor() {
|
|
49
|
+
super(...arguments);
|
|
50
|
+
this.state = { hasError: false };
|
|
51
|
+
}
|
|
52
|
+
static getDerivedStateFromError() {
|
|
53
|
+
return { hasError: true };
|
|
54
|
+
}
|
|
55
|
+
componentDidCatch(error) {
|
|
56
|
+
this.context?.engine?.handle(error);
|
|
57
|
+
}
|
|
58
|
+
render() {
|
|
59
|
+
if (this.state.hasError) return this.props.fallback;
|
|
60
|
+
return this.props.children;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
ErrorBoundaryWithEngine.contextType = ErrorEngineContext;
|
|
64
|
+
|
|
65
|
+
exports.ErrorBoundaryWithEngine = ErrorBoundaryWithEngine;
|
|
66
|
+
exports.ErrorEngineContext = ErrorEngineContext;
|
|
67
|
+
exports.ErrorEngineProvider = ErrorEngineProvider;
|
|
68
|
+
exports.useErrorEngine = useErrorEngine;
|
|
69
|
+
exports.useFieldError = useFieldError;
|
|
70
|
+
//# sourceMappingURL=react.cjs.map
|
|
71
|
+
//# sourceMappingURL=react.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/react.tsx"],"names":["createContext","jsx","useContext","useState","useEffect","React"],"mappings":";;;;;;;;;AAKO,IAAM,kBAAA,GAAqBA,oBAA8C,IAAI;AAE7E,SAAS,mBAAA,CAAmD;AAAA,EACjE,MAAA;AAAA,EACA;AACF,CAAA,EAGgB;AACd,EAAA,uBACEC,cAAA,CAAC,mBAAmB,QAAA,EAAnB,EAA4B,OAAO,EAAE,MAAA,IACnC,QAAA,EACH,CAAA;AAEJ;AAEO,SAAS,cAAA,GAA2E;AACzF,EAAA,MAAM,GAAA,GAAMC,iBAAW,kBAAkB,CAAA;AACzC,EAAA,IAAI,CAAC,GAAA,EAAK;AACR,IAAA,IAAI,OAAA,CAAQ,GAAA,CAAI,UAAU,CAAA,KAAM,aAAA,EAAe;AAC7C,MAAA,OAAA,CAAQ,MAAM,wEAAwE,CAAA;AAAA,IACxF;AACA,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,OAAO,GAAA,CAAI,MAAA;AACb;AAEO,SAAS,cACd,KAAA,EAC4C;AAC5C,EAAA,MAAM,SAAS,cAAA,EAAe;AAC9B,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAIC,eAA0C,IAAI,CAAA;AAExE,EAAAC,eAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,MAAA,EAAQ;AAEb,IAAA,MAAM,WAAA,GAAc,MAAA,CAAO,SAAA,CAAU,CAAC,KAAA,KAAwC;AAC5E,MAAA,IAAI,KAAA,CAAM,IAAA,KAAS,aAAA,IAAiB,KAAA,CAAM,WAAW,QAAA,EAAU;AAC7D,QAAA,IAAI,KAAA,CAAM,KAAA,CAAM,OAAA,EAAS,KAAA,KAAU,KAAA,EAAO;AACxC,UAAA,QAAA,CAAS,MAAM,KAAiC,CAAA;AAAA,QAClD;AAAA,MACF,CAAA,MAAA,IAAW,KAAA,CAAM,IAAA,KAAS,eAAA,EAAiB;AACzC,QAAA,QAAA,CAAS,UAAQ,IAAA,EAAM,IAAA,KAAS,KAAA,CAAM,IAAA,GAAO,OAAO,IAAI,CAAA;AAAA,MAC1D,CAAA,MAAA,IAAW,KAAA,CAAM,IAAA,KAAS,aAAA,EAAe;AACvC,QAAA,QAAA,CAAS,IAAI,CAAA;AAAA,MACf;AAAA,IACF,CAAC,CAAA;AAED,IAAA,OAAO,WAAA;AAAA,EACT,CAAA,EAAG,CAAC,MAAA,EAAQ,KAAK,CAAC,CAAA;AAElB,EAAA,OAAO,EAAE,KAAA,EAAM;AACjB;AAEO,IAAM,uBAAA,GAAN,cAAsCC,sBAAA,CAAM,SAAA,CAGjD;AAAA,EAHK,WAAA,GAAA;AAAA,IAAA,KAAA,CAAA,GAAA,SAAA,CAAA;AAOL,IAAA,IAAA,CAAA,KAAA,GAAQ,EAAE,UAAU,KAAA,EAAM;AAAA,EAAA;AAAA,EAE1B,OAAO,wBAAA,GAA2B;AAChC,IAAA,OAAO,EAAE,UAAU,IAAA,EAAK;AAAA,EAC1B;AAAA,EAEA,kBAAkB,KAAA,EAAc;AAC9B,IAAA,IAAA,CAAK,OAAA,EAAS,MAAA,EAAQ,MAAA,CAAO,KAAK,CAAA;AAAA,EACpC;AAAA,EAEA,MAAA,GAAS;AACP,IAAA,IAAI,IAAA,CAAK,KAAA,CAAM,QAAA,EAAU,OAAO,KAAK,KAAA,CAAM,QAAA;AAC3C,IAAA,OAAO,KAAK,KAAA,CAAM,QAAA;AAAA,EACpB;AACF;AArBa,uBAAA,CAIJ,WAAA,GAAc,kBAAA","file":"react.cjs","sourcesContent":["'use client'\n\nimport React, { createContext, useContext, useEffect, useState } from 'react'\nimport type { ErrorEngine, AppError, StateListener } from './types'\n\nexport const ErrorEngineContext = createContext<{ engine: ErrorEngine } | null>(null)\n\nexport function ErrorEngineProvider<TCode extends string = string>({\n engine,\n children,\n}: {\n engine: ErrorEngine<TCode>\n children: React.ReactNode\n}): JSX.Element {\n return (\n <ErrorEngineContext.Provider value={{ engine }}>\n {children}\n </ErrorEngineContext.Provider>\n )\n}\n\nexport function useErrorEngine<TCode extends string = string>(): ErrorEngine<TCode> | null {\n const ctx = useContext(ErrorEngineContext)\n if (!ctx) {\n if (process.env['NODE_ENV'] === 'development') {\n console.error('[gracefulerrors] useErrorEngine called outside of ErrorEngineProvider.')\n }\n return null\n }\n return ctx.engine as ErrorEngine<TCode>\n}\n\nexport function useFieldError<TField extends string = string>(\n field: TField\n): { error: AppError<string, TField> | null } {\n const engine = useErrorEngine()\n const [error, setError] = useState<AppError<string, TField> | null>(null)\n\n useEffect(() => {\n if (!engine) return\n\n const unsubscribe = engine.subscribe((event: Parameters<StateListener>[0]) => {\n if (event.type === 'ERROR_ADDED' && event.action === 'inline') {\n if (event.error.context?.field === field) {\n setError(event.error as AppError<string, TField>)\n }\n } else if (event.type === 'ERROR_CLEARED') {\n setError(prev => prev?.code === event.code ? null : prev)\n } else if (event.type === 'ALL_CLEARED') {\n setError(null)\n }\n })\n\n return unsubscribe\n }, [engine, field])\n\n return { error }\n}\n\nexport class ErrorBoundaryWithEngine extends React.Component<\n { children: React.ReactNode; fallback: React.ReactNode },\n { hasError: boolean }\n> {\n static contextType = ErrorEngineContext\n declare context: React.ContextType<typeof ErrorEngineContext>\n\n state = { hasError: false }\n\n static getDerivedStateFromError() {\n return { hasError: true }\n }\n\n componentDidCatch(error: Error) {\n this.context?.engine?.handle(error)\n }\n\n render() {\n if (this.state.hasError) return this.props.fallback\n return this.props.children\n }\n}\n"]}
|
package/dist/react.d.cts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { a as ErrorEngine, A as AppError } from './types-CsPmpcbL.cjs';
|
|
3
|
+
|
|
4
|
+
declare const ErrorEngineContext: React.Context<{
|
|
5
|
+
engine: ErrorEngine;
|
|
6
|
+
} | null>;
|
|
7
|
+
declare function ErrorEngineProvider<TCode extends string = string>({ engine, children, }: {
|
|
8
|
+
engine: ErrorEngine<TCode>;
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
}): JSX.Element;
|
|
11
|
+
declare function useErrorEngine<TCode extends string = string>(): ErrorEngine<TCode> | null;
|
|
12
|
+
declare function useFieldError<TField extends string = string>(field: TField): {
|
|
13
|
+
error: AppError<string, TField> | null;
|
|
14
|
+
};
|
|
15
|
+
declare class ErrorBoundaryWithEngine extends React.Component<{
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
fallback: React.ReactNode;
|
|
18
|
+
}, {
|
|
19
|
+
hasError: boolean;
|
|
20
|
+
}> {
|
|
21
|
+
static contextType: React.Context<{
|
|
22
|
+
engine: ErrorEngine;
|
|
23
|
+
} | null>;
|
|
24
|
+
context: React.ContextType<typeof ErrorEngineContext>;
|
|
25
|
+
state: {
|
|
26
|
+
hasError: boolean;
|
|
27
|
+
};
|
|
28
|
+
static getDerivedStateFromError(): {
|
|
29
|
+
hasError: boolean;
|
|
30
|
+
};
|
|
31
|
+
componentDidCatch(error: Error): void;
|
|
32
|
+
render(): React.ReactNode;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { ErrorBoundaryWithEngine, ErrorEngineContext, ErrorEngineProvider, useErrorEngine, useFieldError };
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { a as ErrorEngine, A as AppError } from './types-CsPmpcbL.js';
|
|
3
|
+
|
|
4
|
+
declare const ErrorEngineContext: React.Context<{
|
|
5
|
+
engine: ErrorEngine;
|
|
6
|
+
} | null>;
|
|
7
|
+
declare function ErrorEngineProvider<TCode extends string = string>({ engine, children, }: {
|
|
8
|
+
engine: ErrorEngine<TCode>;
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
}): JSX.Element;
|
|
11
|
+
declare function useErrorEngine<TCode extends string = string>(): ErrorEngine<TCode> | null;
|
|
12
|
+
declare function useFieldError<TField extends string = string>(field: TField): {
|
|
13
|
+
error: AppError<string, TField> | null;
|
|
14
|
+
};
|
|
15
|
+
declare class ErrorBoundaryWithEngine extends React.Component<{
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
fallback: React.ReactNode;
|
|
18
|
+
}, {
|
|
19
|
+
hasError: boolean;
|
|
20
|
+
}> {
|
|
21
|
+
static contextType: React.Context<{
|
|
22
|
+
engine: ErrorEngine;
|
|
23
|
+
} | null>;
|
|
24
|
+
context: React.ContextType<typeof ErrorEngineContext>;
|
|
25
|
+
state: {
|
|
26
|
+
hasError: boolean;
|
|
27
|
+
};
|
|
28
|
+
static getDerivedStateFromError(): {
|
|
29
|
+
hasError: boolean;
|
|
30
|
+
};
|
|
31
|
+
componentDidCatch(error: Error): void;
|
|
32
|
+
render(): React.ReactNode;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { ErrorBoundaryWithEngine, ErrorEngineContext, ErrorEngineProvider, useErrorEngine, useFieldError };
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, useEffect } from 'react';
|
|
2
|
+
import { jsx } from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
var ErrorEngineContext = createContext(null);
|
|
5
|
+
function ErrorEngineProvider({
|
|
6
|
+
engine,
|
|
7
|
+
children
|
|
8
|
+
}) {
|
|
9
|
+
return /* @__PURE__ */ jsx(ErrorEngineContext.Provider, { value: { engine }, children });
|
|
10
|
+
}
|
|
11
|
+
function useErrorEngine() {
|
|
12
|
+
const ctx = useContext(ErrorEngineContext);
|
|
13
|
+
if (!ctx) {
|
|
14
|
+
if (process.env["NODE_ENV"] === "development") {
|
|
15
|
+
console.error("[gracefulerrors] useErrorEngine called outside of ErrorEngineProvider.");
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return ctx.engine;
|
|
20
|
+
}
|
|
21
|
+
function useFieldError(field) {
|
|
22
|
+
const engine = useErrorEngine();
|
|
23
|
+
const [error, setError] = useState(null);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!engine) return;
|
|
26
|
+
const unsubscribe = engine.subscribe((event) => {
|
|
27
|
+
if (event.type === "ERROR_ADDED" && event.action === "inline") {
|
|
28
|
+
if (event.error.context?.field === field) {
|
|
29
|
+
setError(event.error);
|
|
30
|
+
}
|
|
31
|
+
} else if (event.type === "ERROR_CLEARED") {
|
|
32
|
+
setError((prev) => prev?.code === event.code ? null : prev);
|
|
33
|
+
} else if (event.type === "ALL_CLEARED") {
|
|
34
|
+
setError(null);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return unsubscribe;
|
|
38
|
+
}, [engine, field]);
|
|
39
|
+
return { error };
|
|
40
|
+
}
|
|
41
|
+
var ErrorBoundaryWithEngine = class extends React.Component {
|
|
42
|
+
constructor() {
|
|
43
|
+
super(...arguments);
|
|
44
|
+
this.state = { hasError: false };
|
|
45
|
+
}
|
|
46
|
+
static getDerivedStateFromError() {
|
|
47
|
+
return { hasError: true };
|
|
48
|
+
}
|
|
49
|
+
componentDidCatch(error) {
|
|
50
|
+
this.context?.engine?.handle(error);
|
|
51
|
+
}
|
|
52
|
+
render() {
|
|
53
|
+
if (this.state.hasError) return this.props.fallback;
|
|
54
|
+
return this.props.children;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
ErrorBoundaryWithEngine.contextType = ErrorEngineContext;
|
|
58
|
+
|
|
59
|
+
export { ErrorBoundaryWithEngine, ErrorEngineContext, ErrorEngineProvider, useErrorEngine, useFieldError };
|
|
60
|
+
//# sourceMappingURL=react.js.map
|
|
61
|
+
//# sourceMappingURL=react.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/react.tsx"],"names":[],"mappings":";;;AAKO,IAAM,kBAAA,GAAqB,cAA8C,IAAI;AAE7E,SAAS,mBAAA,CAAmD;AAAA,EACjE,MAAA;AAAA,EACA;AACF,CAAA,EAGgB;AACd,EAAA,uBACE,GAAA,CAAC,mBAAmB,QAAA,EAAnB,EAA4B,OAAO,EAAE,MAAA,IACnC,QAAA,EACH,CAAA;AAEJ;AAEO,SAAS,cAAA,GAA2E;AACzF,EAAA,MAAM,GAAA,GAAM,WAAW,kBAAkB,CAAA;AACzC,EAAA,IAAI,CAAC,GAAA,EAAK;AACR,IAAA,IAAI,OAAA,CAAQ,GAAA,CAAI,UAAU,CAAA,KAAM,aAAA,EAAe;AAC7C,MAAA,OAAA,CAAQ,MAAM,wEAAwE,CAAA;AAAA,IACxF;AACA,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,OAAO,GAAA,CAAI,MAAA;AACb;AAEO,SAAS,cACd,KAAA,EAC4C;AAC5C,EAAA,MAAM,SAAS,cAAA,EAAe;AAC9B,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAI,SAA0C,IAAI,CAAA;AAExE,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,MAAA,EAAQ;AAEb,IAAA,MAAM,WAAA,GAAc,MAAA,CAAO,SAAA,CAAU,CAAC,KAAA,KAAwC;AAC5E,MAAA,IAAI,KAAA,CAAM,IAAA,KAAS,aAAA,IAAiB,KAAA,CAAM,WAAW,QAAA,EAAU;AAC7D,QAAA,IAAI,KAAA,CAAM,KAAA,CAAM,OAAA,EAAS,KAAA,KAAU,KAAA,EAAO;AACxC,UAAA,QAAA,CAAS,MAAM,KAAiC,CAAA;AAAA,QAClD;AAAA,MACF,CAAA,MAAA,IAAW,KAAA,CAAM,IAAA,KAAS,eAAA,EAAiB;AACzC,QAAA,QAAA,CAAS,UAAQ,IAAA,EAAM,IAAA,KAAS,KAAA,CAAM,IAAA,GAAO,OAAO,IAAI,CAAA;AAAA,MAC1D,CAAA,MAAA,IAAW,KAAA,CAAM,IAAA,KAAS,aAAA,EAAe;AACvC,QAAA,QAAA,CAAS,IAAI,CAAA;AAAA,MACf;AAAA,IACF,CAAC,CAAA;AAED,IAAA,OAAO,WAAA;AAAA,EACT,CAAA,EAAG,CAAC,MAAA,EAAQ,KAAK,CAAC,CAAA;AAElB,EAAA,OAAO,EAAE,KAAA,EAAM;AACjB;AAEO,IAAM,uBAAA,GAAN,cAAsC,KAAA,CAAM,SAAA,CAGjD;AAAA,EAHK,WAAA,GAAA;AAAA,IAAA,KAAA,CAAA,GAAA,SAAA,CAAA;AAOL,IAAA,IAAA,CAAA,KAAA,GAAQ,EAAE,UAAU,KAAA,EAAM;AAAA,EAAA;AAAA,EAE1B,OAAO,wBAAA,GAA2B;AAChC,IAAA,OAAO,EAAE,UAAU,IAAA,EAAK;AAAA,EAC1B;AAAA,EAEA,kBAAkB,KAAA,EAAc;AAC9B,IAAA,IAAA,CAAK,OAAA,EAAS,MAAA,EAAQ,MAAA,CAAO,KAAK,CAAA;AAAA,EACpC;AAAA,EAEA,MAAA,GAAS;AACP,IAAA,IAAI,IAAA,CAAK,KAAA,CAAM,QAAA,EAAU,OAAO,KAAK,KAAA,CAAM,QAAA;AAC3C,IAAA,OAAO,KAAK,KAAA,CAAM,QAAA;AAAA,EACpB;AACF;AArBa,uBAAA,CAIJ,WAAA,GAAc,kBAAA","file":"react.js","sourcesContent":["'use client'\n\nimport React, { createContext, useContext, useEffect, useState } from 'react'\nimport type { ErrorEngine, AppError, StateListener } from './types'\n\nexport const ErrorEngineContext = createContext<{ engine: ErrorEngine } | null>(null)\n\nexport function ErrorEngineProvider<TCode extends string = string>({\n engine,\n children,\n}: {\n engine: ErrorEngine<TCode>\n children: React.ReactNode\n}): JSX.Element {\n return (\n <ErrorEngineContext.Provider value={{ engine }}>\n {children}\n </ErrorEngineContext.Provider>\n )\n}\n\nexport function useErrorEngine<TCode extends string = string>(): ErrorEngine<TCode> | null {\n const ctx = useContext(ErrorEngineContext)\n if (!ctx) {\n if (process.env['NODE_ENV'] === 'development') {\n console.error('[gracefulerrors] useErrorEngine called outside of ErrorEngineProvider.')\n }\n return null\n }\n return ctx.engine as ErrorEngine<TCode>\n}\n\nexport function useFieldError<TField extends string = string>(\n field: TField\n): { error: AppError<string, TField> | null } {\n const engine = useErrorEngine()\n const [error, setError] = useState<AppError<string, TField> | null>(null)\n\n useEffect(() => {\n if (!engine) return\n\n const unsubscribe = engine.subscribe((event: Parameters<StateListener>[0]) => {\n if (event.type === 'ERROR_ADDED' && event.action === 'inline') {\n if (event.error.context?.field === field) {\n setError(event.error as AppError<string, TField>)\n }\n } else if (event.type === 'ERROR_CLEARED') {\n setError(prev => prev?.code === event.code ? null : prev)\n } else if (event.type === 'ALL_CLEARED') {\n setError(null)\n }\n })\n\n return unsubscribe\n }, [engine, field])\n\n return { error }\n}\n\nexport class ErrorBoundaryWithEngine extends React.Component<\n { children: React.ReactNode; fallback: React.ReactNode },\n { hasError: boolean }\n> {\n static contextType = ErrorEngineContext\n declare context: React.ContextType<typeof ErrorEngineContext>\n\n state = { hasError: false }\n\n static getDerivedStateFromError() {\n return { hasError: true }\n }\n\n componentDidCatch(error: Error) {\n this.context?.engine?.handle(error)\n }\n\n render() {\n if (this.state.hasError) return this.props.fallback\n return this.props.children\n }\n}\n"]}
|