@almadar/ui 1.0.0 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-4FRUCUO5.js +14 -0
- package/dist/chunk-7NEWMNNU.js +147 -0
- package/dist/chunk-AQREMI4N.js +426 -0
- package/dist/chunk-BCERHHKU.js +2834 -0
- package/dist/chunk-I5RSZIOE.js +190 -0
- package/dist/chunk-KKCVDUK7.js +104 -0
- package/dist/chunk-N7MVUW4R.js +194 -0
- package/dist/chunk-S7EYY36U.js +13 -0
- package/dist/chunk-TTXKOHDO.js +270 -0
- package/dist/chunk-XSEDIUM6.js +93 -0
- package/dist/cn-mqkxz8Sd.d.ts +9 -0
- package/dist/components/index.d.ts +175 -4
- package/dist/components/index.js +701 -7595
- package/dist/context/index.js +6 -342
- package/dist/hooks/index.d.ts +70 -1
- package/dist/hooks/index.js +6 -2262
- package/dist/lib/index.d.ts +180 -8
- package/dist/lib/index.js +685 -171
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.js +8 -905
- package/dist/renderer/index.d.ts +144 -1
- package/dist/renderer/index.js +209 -402
- package/dist/stores/index.js +2 -196
- package/package.json +11 -13
- package/themes/almadar.css +8 -0
- package/themes/index.css +1 -0
- package/themes/minimalist.css +8 -0
- package/themes/trait-wars.css +176 -0
- package/themes/wireframe.css +8 -0
- package/dist/components/index.js.map +0 -1
- package/dist/context/index.js.map +0 -1
- package/dist/hooks/index.js.map +0 -1
- package/dist/lib/index.js.map +0 -1
- package/dist/providers/index.js.map +0 -1
- package/dist/renderer/index.js.map +0 -1
- package/dist/stores/index.js.map +0 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ThemeProvider, useTheme } from './chunk-I5RSZIOE.js';
|
|
2
|
+
|
|
3
|
+
// context/DesignThemeContext.tsx
|
|
4
|
+
var DesignThemeProvider = ThemeProvider;
|
|
5
|
+
function useDesignTheme() {
|
|
6
|
+
const { theme, setTheme, availableThemes } = useTheme();
|
|
7
|
+
return {
|
|
8
|
+
designTheme: theme,
|
|
9
|
+
setDesignTheme: setTheme,
|
|
10
|
+
availableThemes: availableThemes.map((t) => t.name)
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export { DesignThemeProvider, useDesignTheme };
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
// hooks/useUISlots.ts
|
|
4
|
+
var DEFAULT_SLOTS = {
|
|
5
|
+
main: null,
|
|
6
|
+
sidebar: null,
|
|
7
|
+
modal: null,
|
|
8
|
+
drawer: null,
|
|
9
|
+
overlay: null,
|
|
10
|
+
center: null,
|
|
11
|
+
toast: null,
|
|
12
|
+
"hud-top": null,
|
|
13
|
+
"hud-bottom": null,
|
|
14
|
+
floating: null
|
|
15
|
+
};
|
|
16
|
+
var idCounter = 0;
|
|
17
|
+
function generateId() {
|
|
18
|
+
return `slot-content-${++idCounter}-${Date.now()}`;
|
|
19
|
+
}
|
|
20
|
+
function useUISlotManager() {
|
|
21
|
+
const [slots, setSlots] = useState(DEFAULT_SLOTS);
|
|
22
|
+
const subscribersRef = useRef(/* @__PURE__ */ new Set());
|
|
23
|
+
const timersRef = useRef(/* @__PURE__ */ new Map());
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
return () => {
|
|
26
|
+
timersRef.current.forEach((timer) => clearTimeout(timer));
|
|
27
|
+
timersRef.current.clear();
|
|
28
|
+
};
|
|
29
|
+
}, []);
|
|
30
|
+
const notifySubscribers = useCallback((slot, content) => {
|
|
31
|
+
subscribersRef.current.forEach((callback) => {
|
|
32
|
+
try {
|
|
33
|
+
callback(slot, content);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error("[UISlots] Subscriber error:", error);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}, []);
|
|
39
|
+
const render = useCallback((config) => {
|
|
40
|
+
const id = generateId();
|
|
41
|
+
const content = {
|
|
42
|
+
id,
|
|
43
|
+
pattern: config.pattern,
|
|
44
|
+
props: config.props ?? {},
|
|
45
|
+
priority: config.priority ?? 0,
|
|
46
|
+
animation: config.animation ?? "fade",
|
|
47
|
+
onDismiss: config.onDismiss,
|
|
48
|
+
sourceTrait: config.sourceTrait
|
|
49
|
+
};
|
|
50
|
+
if (config.autoDismissMs && config.autoDismissMs > 0) {
|
|
51
|
+
content.autoDismissAt = Date.now() + config.autoDismissMs;
|
|
52
|
+
const timer = setTimeout(() => {
|
|
53
|
+
setSlots((prev) => {
|
|
54
|
+
if (prev[config.target]?.id === id) {
|
|
55
|
+
content.onDismiss?.();
|
|
56
|
+
notifySubscribers(config.target, null);
|
|
57
|
+
return { ...prev, [config.target]: null };
|
|
58
|
+
}
|
|
59
|
+
return prev;
|
|
60
|
+
});
|
|
61
|
+
timersRef.current.delete(id);
|
|
62
|
+
}, config.autoDismissMs);
|
|
63
|
+
timersRef.current.set(id, timer);
|
|
64
|
+
}
|
|
65
|
+
setSlots((prev) => {
|
|
66
|
+
const existing = prev[config.target];
|
|
67
|
+
if (existing && existing.priority > content.priority) {
|
|
68
|
+
console.warn(
|
|
69
|
+
`[UISlots] Slot "${config.target}" already has higher priority content (${existing.priority} > ${content.priority})`
|
|
70
|
+
);
|
|
71
|
+
return prev;
|
|
72
|
+
}
|
|
73
|
+
notifySubscribers(config.target, content);
|
|
74
|
+
return { ...prev, [config.target]: content };
|
|
75
|
+
});
|
|
76
|
+
return id;
|
|
77
|
+
}, [notifySubscribers]);
|
|
78
|
+
const clear = useCallback((slot) => {
|
|
79
|
+
setSlots((prev) => {
|
|
80
|
+
const content = prev[slot];
|
|
81
|
+
if (content) {
|
|
82
|
+
const timer = timersRef.current.get(content.id);
|
|
83
|
+
if (timer) {
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
timersRef.current.delete(content.id);
|
|
86
|
+
}
|
|
87
|
+
content.onDismiss?.();
|
|
88
|
+
notifySubscribers(slot, null);
|
|
89
|
+
}
|
|
90
|
+
return { ...prev, [slot]: null };
|
|
91
|
+
});
|
|
92
|
+
}, [notifySubscribers]);
|
|
93
|
+
const clearById = useCallback((id) => {
|
|
94
|
+
setSlots((prev) => {
|
|
95
|
+
const entry = Object.entries(prev).find(([, content]) => content?.id === id);
|
|
96
|
+
if (entry) {
|
|
97
|
+
const [slot, content] = entry;
|
|
98
|
+
const timer = timersRef.current.get(id);
|
|
99
|
+
if (timer) {
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
timersRef.current.delete(id);
|
|
102
|
+
}
|
|
103
|
+
content.onDismiss?.();
|
|
104
|
+
notifySubscribers(slot, null);
|
|
105
|
+
return { ...prev, [slot]: null };
|
|
106
|
+
}
|
|
107
|
+
return prev;
|
|
108
|
+
});
|
|
109
|
+
}, [notifySubscribers]);
|
|
110
|
+
const clearAll = useCallback(() => {
|
|
111
|
+
timersRef.current.forEach((timer) => clearTimeout(timer));
|
|
112
|
+
timersRef.current.clear();
|
|
113
|
+
setSlots((prev) => {
|
|
114
|
+
Object.entries(prev).forEach(([slot, content]) => {
|
|
115
|
+
if (content) {
|
|
116
|
+
content.onDismiss?.();
|
|
117
|
+
notifySubscribers(slot, null);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
return DEFAULT_SLOTS;
|
|
121
|
+
});
|
|
122
|
+
}, [notifySubscribers]);
|
|
123
|
+
const subscribe = useCallback((callback) => {
|
|
124
|
+
subscribersRef.current.add(callback);
|
|
125
|
+
return () => {
|
|
126
|
+
subscribersRef.current.delete(callback);
|
|
127
|
+
};
|
|
128
|
+
}, []);
|
|
129
|
+
const hasContent = useCallback((slot) => {
|
|
130
|
+
return slots[slot] !== null;
|
|
131
|
+
}, [slots]);
|
|
132
|
+
const getContent = useCallback((slot) => {
|
|
133
|
+
return slots[slot];
|
|
134
|
+
}, [slots]);
|
|
135
|
+
return {
|
|
136
|
+
slots,
|
|
137
|
+
render,
|
|
138
|
+
clear,
|
|
139
|
+
clearById,
|
|
140
|
+
clearAll,
|
|
141
|
+
subscribe,
|
|
142
|
+
hasContent,
|
|
143
|
+
getContent
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export { DEFAULT_SLOTS, useUISlotManager };
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { __publicField } from './chunk-S7EYY36U.js';
|
|
2
|
+
import { useRef, useState, useEffect, useCallback } from 'react';
|
|
3
|
+
|
|
4
|
+
// renderer/client-effect-executor.ts
|
|
5
|
+
function executeClientEffects(effects, config) {
|
|
6
|
+
if (!effects || effects.length === 0) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
for (const effect of effects) {
|
|
10
|
+
try {
|
|
11
|
+
executeEffect(effect, config);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.error(
|
|
14
|
+
`[ClientEffectExecutor] Error executing effect:`,
|
|
15
|
+
effect,
|
|
16
|
+
error
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
config.onComplete?.();
|
|
21
|
+
}
|
|
22
|
+
function executeEffect(effect, config) {
|
|
23
|
+
const [effectType, ...args] = effect;
|
|
24
|
+
switch (effectType) {
|
|
25
|
+
case "render-ui": {
|
|
26
|
+
const [slot, patternConfig] = args;
|
|
27
|
+
executeRenderUI(slot, patternConfig, config);
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
case "navigate": {
|
|
31
|
+
const [path, params] = args;
|
|
32
|
+
executeNavigate(path, params, config);
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
case "notify": {
|
|
36
|
+
const [message, options] = args;
|
|
37
|
+
executeNotify(message, options, config);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
case "emit": {
|
|
41
|
+
const [event, payload] = args;
|
|
42
|
+
executeEmit(event, payload, config);
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
default:
|
|
46
|
+
console.warn(`[ClientEffectExecutor] Unknown effect type: ${effectType}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function executeRenderUI(slot, patternConfig, config) {
|
|
50
|
+
config.renderToSlot(slot, patternConfig);
|
|
51
|
+
}
|
|
52
|
+
function executeNavigate(path, params, config) {
|
|
53
|
+
config.navigate(path, params);
|
|
54
|
+
}
|
|
55
|
+
function executeNotify(message, options, config) {
|
|
56
|
+
config.notify(message, options);
|
|
57
|
+
}
|
|
58
|
+
function executeEmit(event, payload, config) {
|
|
59
|
+
config.eventBus.emit(event, payload);
|
|
60
|
+
}
|
|
61
|
+
function parseClientEffect(raw) {
|
|
62
|
+
if (!Array.isArray(raw) || raw.length < 1) {
|
|
63
|
+
console.warn("[ClientEffectExecutor] Invalid effect format:", raw);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const [type, ...args] = raw;
|
|
67
|
+
if (typeof type !== "string") {
|
|
68
|
+
console.warn("[ClientEffectExecutor] Effect type must be string:", raw);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
switch (type) {
|
|
72
|
+
case "render-ui":
|
|
73
|
+
return ["render-ui", args[0], args[1]];
|
|
74
|
+
case "navigate":
|
|
75
|
+
return ["navigate", args[0], args[1]];
|
|
76
|
+
case "notify":
|
|
77
|
+
return ["notify", args[0], args[1]];
|
|
78
|
+
case "emit":
|
|
79
|
+
return ["emit", args[0], args[1]];
|
|
80
|
+
default:
|
|
81
|
+
console.warn(`[ClientEffectExecutor] Unknown effect type: ${type}`);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function parseClientEffects(raw) {
|
|
86
|
+
if (!raw || !Array.isArray(raw)) {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
return raw.map((effect) => parseClientEffect(effect)).filter((effect) => effect !== null);
|
|
90
|
+
}
|
|
91
|
+
function filterEffectsByType(effects, type) {
|
|
92
|
+
return effects.filter(
|
|
93
|
+
(effect) => effect[0] === type
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
function getRenderUIEffects(effects) {
|
|
97
|
+
return filterEffectsByType(effects, "render-ui");
|
|
98
|
+
}
|
|
99
|
+
function getNavigateEffects(effects) {
|
|
100
|
+
return filterEffectsByType(effects, "navigate");
|
|
101
|
+
}
|
|
102
|
+
function getNotifyEffects(effects) {
|
|
103
|
+
return filterEffectsByType(effects, "notify");
|
|
104
|
+
}
|
|
105
|
+
function getEmitEffects(effects) {
|
|
106
|
+
return filterEffectsByType(effects, "emit");
|
|
107
|
+
}
|
|
108
|
+
var effectIdCounter = 0;
|
|
109
|
+
function generateEffectId() {
|
|
110
|
+
return `offline-effect-${++effectIdCounter}-${Date.now()}`;
|
|
111
|
+
}
|
|
112
|
+
var OfflineExecutor = class {
|
|
113
|
+
constructor(config) {
|
|
114
|
+
__publicField(this, "config");
|
|
115
|
+
__publicField(this, "state");
|
|
116
|
+
__publicField(this, "storage");
|
|
117
|
+
/**
|
|
118
|
+
* Handle going online
|
|
119
|
+
*/
|
|
120
|
+
__publicField(this, "handleOnline", () => {
|
|
121
|
+
this.state.isOffline = false;
|
|
122
|
+
});
|
|
123
|
+
/**
|
|
124
|
+
* Handle going offline
|
|
125
|
+
*/
|
|
126
|
+
__publicField(this, "handleOffline", () => {
|
|
127
|
+
this.state.isOffline = true;
|
|
128
|
+
});
|
|
129
|
+
this.config = {
|
|
130
|
+
enableSyncQueue: true,
|
|
131
|
+
maxQueueSize: 100,
|
|
132
|
+
...config
|
|
133
|
+
};
|
|
134
|
+
this.state = {
|
|
135
|
+
isOffline: !this.checkOnline(),
|
|
136
|
+
syncQueue: [],
|
|
137
|
+
localEffectsProcessed: 0,
|
|
138
|
+
effectsSynced: 0
|
|
139
|
+
};
|
|
140
|
+
this.storage = typeof localStorage !== "undefined" ? localStorage : null;
|
|
141
|
+
this.loadSyncQueue();
|
|
142
|
+
if (typeof window !== "undefined") {
|
|
143
|
+
window.addEventListener("online", this.handleOnline);
|
|
144
|
+
window.addEventListener("offline", this.handleOffline);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Check if we're online (browser API)
|
|
149
|
+
*/
|
|
150
|
+
checkOnline() {
|
|
151
|
+
return typeof navigator !== "undefined" ? navigator.onLine : true;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Load sync queue from localStorage
|
|
155
|
+
*/
|
|
156
|
+
loadSyncQueue() {
|
|
157
|
+
if (!this.storage) return;
|
|
158
|
+
try {
|
|
159
|
+
const stored = this.storage.getItem("orbital-offline-queue");
|
|
160
|
+
if (stored) {
|
|
161
|
+
this.state.syncQueue = JSON.parse(stored);
|
|
162
|
+
}
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.warn("[OfflineExecutor] Failed to load sync queue:", error);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Save sync queue to localStorage
|
|
169
|
+
*/
|
|
170
|
+
saveSyncQueue() {
|
|
171
|
+
if (!this.storage) return;
|
|
172
|
+
try {
|
|
173
|
+
this.storage.setItem("orbital-offline-queue", JSON.stringify(this.state.syncQueue));
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.warn("[OfflineExecutor] Failed to save sync queue:", error);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Add an effect to the sync queue
|
|
180
|
+
*/
|
|
181
|
+
queueForSync(type, payload) {
|
|
182
|
+
if (!this.config.enableSyncQueue) return;
|
|
183
|
+
const effect = {
|
|
184
|
+
id: generateEffectId(),
|
|
185
|
+
timestamp: Date.now(),
|
|
186
|
+
type,
|
|
187
|
+
payload,
|
|
188
|
+
retries: 0,
|
|
189
|
+
maxRetries: 3
|
|
190
|
+
};
|
|
191
|
+
this.state.syncQueue.push(effect);
|
|
192
|
+
if (this.state.syncQueue.length > (this.config.maxQueueSize ?? 100)) {
|
|
193
|
+
this.state.syncQueue.shift();
|
|
194
|
+
}
|
|
195
|
+
this.saveSyncQueue();
|
|
196
|
+
this.config.onEffectQueued?.(effect);
|
|
197
|
+
this.config.onQueueChange?.(this.state.syncQueue);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Execute client effects immediately.
|
|
201
|
+
*/
|
|
202
|
+
executeClientEffects(effects) {
|
|
203
|
+
if (effects.length === 0) return;
|
|
204
|
+
executeClientEffects(effects, this.config);
|
|
205
|
+
this.state.localEffectsProcessed += effects.length;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Process an event in offline mode.
|
|
209
|
+
*
|
|
210
|
+
* Returns a simulated EventResponse with mock data.
|
|
211
|
+
* Client effects are executed immediately.
|
|
212
|
+
* Server effects are queued for sync.
|
|
213
|
+
*/
|
|
214
|
+
processEventOffline(event, payload, effects) {
|
|
215
|
+
const clientEffects = [];
|
|
216
|
+
const fetchedData = {};
|
|
217
|
+
if (effects) {
|
|
218
|
+
for (const effect of effects) {
|
|
219
|
+
if (!Array.isArray(effect) || effect.length < 1) continue;
|
|
220
|
+
const [type, ...args] = effect;
|
|
221
|
+
switch (type) {
|
|
222
|
+
// Client effects - execute immediately
|
|
223
|
+
case "render-ui":
|
|
224
|
+
case "navigate":
|
|
225
|
+
case "notify":
|
|
226
|
+
case "emit":
|
|
227
|
+
clientEffects.push(effect);
|
|
228
|
+
break;
|
|
229
|
+
// Fetch effect - use mock data
|
|
230
|
+
case "fetch": {
|
|
231
|
+
const [entityName, _query] = args;
|
|
232
|
+
if (typeof entityName === "string" && this.config.mockDataProvider) {
|
|
233
|
+
fetchedData[entityName] = this.config.mockDataProvider(entityName);
|
|
234
|
+
}
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
// Server effects - queue for sync
|
|
238
|
+
case "persist":
|
|
239
|
+
case "call-service":
|
|
240
|
+
case "spawn":
|
|
241
|
+
case "despawn":
|
|
242
|
+
this.queueForSync(type, { args, event, payload });
|
|
243
|
+
break;
|
|
244
|
+
default:
|
|
245
|
+
console.warn(`[OfflineExecutor] Unknown effect type: ${type}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (clientEffects.length > 0) {
|
|
250
|
+
this.executeClientEffects(clientEffects);
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
success: true,
|
|
254
|
+
data: Object.keys(fetchedData).length > 0 ? fetchedData : void 0,
|
|
255
|
+
clientEffects: clientEffects.length > 0 ? clientEffects : void 0
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Sync pending effects to server.
|
|
260
|
+
*
|
|
261
|
+
* @param serverUrl - Base URL for the orbital server
|
|
262
|
+
* @param authToken - Optional auth token for requests
|
|
263
|
+
* @returns Number of successfully synced effects
|
|
264
|
+
*/
|
|
265
|
+
async syncPendingEffects(serverUrl, authToken) {
|
|
266
|
+
if (this.state.syncQueue.length === 0) {
|
|
267
|
+
return 0;
|
|
268
|
+
}
|
|
269
|
+
this.state.lastSyncAttempt = Date.now();
|
|
270
|
+
let syncedCount = 0;
|
|
271
|
+
const failedEffects = [];
|
|
272
|
+
const headers = {
|
|
273
|
+
"Content-Type": "application/json"
|
|
274
|
+
};
|
|
275
|
+
if (authToken) {
|
|
276
|
+
headers["Authorization"] = `Bearer ${authToken}`;
|
|
277
|
+
}
|
|
278
|
+
for (const effect of this.state.syncQueue) {
|
|
279
|
+
try {
|
|
280
|
+
const response = await fetch(`${serverUrl}/sync-effect`, {
|
|
281
|
+
method: "POST",
|
|
282
|
+
headers,
|
|
283
|
+
body: JSON.stringify({
|
|
284
|
+
type: effect.type,
|
|
285
|
+
payload: effect.payload,
|
|
286
|
+
offlineId: effect.id,
|
|
287
|
+
offlineTimestamp: effect.timestamp
|
|
288
|
+
})
|
|
289
|
+
});
|
|
290
|
+
if (response.ok) {
|
|
291
|
+
syncedCount++;
|
|
292
|
+
} else {
|
|
293
|
+
effect.retries++;
|
|
294
|
+
if (effect.retries < effect.maxRetries) {
|
|
295
|
+
failedEffects.push(effect);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
} catch (error) {
|
|
299
|
+
effect.retries++;
|
|
300
|
+
if (effect.retries < effect.maxRetries) {
|
|
301
|
+
failedEffects.push(effect);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
this.state.syncQueue = failedEffects;
|
|
306
|
+
this.state.effectsSynced += syncedCount;
|
|
307
|
+
if (syncedCount > 0) {
|
|
308
|
+
this.state.lastSuccessfulSync = Date.now();
|
|
309
|
+
}
|
|
310
|
+
this.saveSyncQueue();
|
|
311
|
+
this.config.onQueueChange?.(this.state.syncQueue);
|
|
312
|
+
return syncedCount;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Get current executor state
|
|
316
|
+
*/
|
|
317
|
+
getState() {
|
|
318
|
+
return { ...this.state };
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Get number of pending effects
|
|
322
|
+
*/
|
|
323
|
+
getPendingCount() {
|
|
324
|
+
return this.state.syncQueue.length;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Clear the sync queue
|
|
328
|
+
*/
|
|
329
|
+
clearQueue() {
|
|
330
|
+
this.state.syncQueue = [];
|
|
331
|
+
this.saveSyncQueue();
|
|
332
|
+
this.config.onQueueChange?.(this.state.syncQueue);
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Dispose the executor and clean up listeners
|
|
336
|
+
*/
|
|
337
|
+
dispose() {
|
|
338
|
+
if (typeof window !== "undefined") {
|
|
339
|
+
window.removeEventListener("online", this.handleOnline);
|
|
340
|
+
window.removeEventListener("offline", this.handleOffline);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
function createOfflineExecutor(config) {
|
|
345
|
+
return new OfflineExecutor(config);
|
|
346
|
+
}
|
|
347
|
+
function useOfflineExecutor(options) {
|
|
348
|
+
const executorRef = useRef(null);
|
|
349
|
+
const [state, setState] = useState({
|
|
350
|
+
isOffline: false,
|
|
351
|
+
syncQueue: [],
|
|
352
|
+
localEffectsProcessed: 0,
|
|
353
|
+
effectsSynced: 0
|
|
354
|
+
});
|
|
355
|
+
useEffect(() => {
|
|
356
|
+
const executor = new OfflineExecutor({
|
|
357
|
+
...options,
|
|
358
|
+
onQueueChange: (queue) => {
|
|
359
|
+
setState(executor.getState());
|
|
360
|
+
options.onQueueChange?.(queue);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
executorRef.current = executor;
|
|
364
|
+
setState(executor.getState());
|
|
365
|
+
return () => {
|
|
366
|
+
executor.dispose();
|
|
367
|
+
executorRef.current = null;
|
|
368
|
+
};
|
|
369
|
+
}, []);
|
|
370
|
+
useEffect(() => {
|
|
371
|
+
if (!options.autoSync || !options.serverUrl) return;
|
|
372
|
+
const handleOnline = async () => {
|
|
373
|
+
if (executorRef.current) {
|
|
374
|
+
await executorRef.current.syncPendingEffects(
|
|
375
|
+
options.serverUrl,
|
|
376
|
+
options.authToken
|
|
377
|
+
);
|
|
378
|
+
setState(executorRef.current.getState());
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
window.addEventListener("online", handleOnline);
|
|
382
|
+
return () => window.removeEventListener("online", handleOnline);
|
|
383
|
+
}, [options.autoSync, options.serverUrl, options.authToken]);
|
|
384
|
+
const executeEffects = useCallback((effects) => {
|
|
385
|
+
executorRef.current?.executeClientEffects(effects);
|
|
386
|
+
if (executorRef.current) {
|
|
387
|
+
setState(executorRef.current.getState());
|
|
388
|
+
}
|
|
389
|
+
}, []);
|
|
390
|
+
const processEventOffline = useCallback(
|
|
391
|
+
(event, payload, effects) => {
|
|
392
|
+
const result = executorRef.current?.processEventOffline(event, payload, effects);
|
|
393
|
+
if (executorRef.current) {
|
|
394
|
+
setState(executorRef.current.getState());
|
|
395
|
+
}
|
|
396
|
+
return result ?? { success: false, error: "Executor not initialized" };
|
|
397
|
+
},
|
|
398
|
+
[]
|
|
399
|
+
);
|
|
400
|
+
const sync = useCallback(async () => {
|
|
401
|
+
if (!executorRef.current || !options.serverUrl) return 0;
|
|
402
|
+
const count = await executorRef.current.syncPendingEffects(
|
|
403
|
+
options.serverUrl,
|
|
404
|
+
options.authToken
|
|
405
|
+
);
|
|
406
|
+
setState(executorRef.current.getState());
|
|
407
|
+
return count;
|
|
408
|
+
}, [options.serverUrl, options.authToken]);
|
|
409
|
+
const clearQueue = useCallback(() => {
|
|
410
|
+
executorRef.current?.clearQueue();
|
|
411
|
+
if (executorRef.current) {
|
|
412
|
+
setState(executorRef.current.getState());
|
|
413
|
+
}
|
|
414
|
+
}, []);
|
|
415
|
+
return {
|
|
416
|
+
state,
|
|
417
|
+
isOffline: state.isOffline,
|
|
418
|
+
pendingCount: state.syncQueue.length,
|
|
419
|
+
executeClientEffects: executeEffects,
|
|
420
|
+
processEventOffline,
|
|
421
|
+
sync,
|
|
422
|
+
clearQueue
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export { OfflineExecutor, createOfflineExecutor, executeClientEffects, filterEffectsByType, getEmitEffects, getNavigateEffects, getNotifyEffects, getRenderUIEffects, parseClientEffect, parseClientEffects, useOfflineExecutor };
|