@ai4b-team/fsaos-gateway-sdk 1.0.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/CHANGELOG.md +56 -0
- package/README.md +157 -0
- package/dist/iife/gateway.js +12 -0
- package/dist/iife/gateway.js.map +7 -0
- package/dist/iife/ui.js +3 -0
- package/dist/iife/ui.js.map +7 -0
- package/dist/index.cjs +3220 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1806 -0
- package/dist/index.d.ts +1806 -0
- package/dist/index.js +3079 -0
- package/dist/index.js.map +1 -0
- package/dist/ui.cjs +201 -0
- package/dist/ui.cjs.map +1 -0
- package/dist/ui.d.cts +243 -0
- package/dist/ui.d.ts +243 -0
- package/dist/ui.js +180 -0
- package/dist/ui.js.map +1 -0
- package/package.json +92 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3220 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var supabaseJs = require('@supabase/supabase-js');
|
|
4
|
+
var reactQuery = require('@tanstack/react-query');
|
|
5
|
+
var React = require('react');
|
|
6
|
+
var client = require('react-dom/client');
|
|
7
|
+
var ReactDOM = require('react-dom');
|
|
8
|
+
var JsxRuntime = require('react/jsx-runtime');
|
|
9
|
+
|
|
10
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
|
+
|
|
12
|
+
function _interopNamespace(e) {
|
|
13
|
+
if (e && e.__esModule) return e;
|
|
14
|
+
var n = Object.create(null);
|
|
15
|
+
if (e) {
|
|
16
|
+
Object.keys(e).forEach(function (k) {
|
|
17
|
+
if (k !== 'default') {
|
|
18
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
19
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
20
|
+
enumerable: true,
|
|
21
|
+
get: function () { return e[k]; }
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
n.default = e;
|
|
27
|
+
return Object.freeze(n);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
var React__default = /*#__PURE__*/_interopDefault(React);
|
|
31
|
+
var ReactDOM__namespace = /*#__PURE__*/_interopNamespace(ReactDOM);
|
|
32
|
+
var JsxRuntime__namespace = /*#__PURE__*/_interopNamespace(JsxRuntime);
|
|
33
|
+
|
|
34
|
+
var __defProp = Object.defineProperty;
|
|
35
|
+
var __export = (target, all) => {
|
|
36
|
+
for (var name in all)
|
|
37
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/index.ts
|
|
41
|
+
var src_exports = {};
|
|
42
|
+
__export(src_exports, {
|
|
43
|
+
ComponentRenderer: () => ComponentRenderer,
|
|
44
|
+
EnforcementDeniedError: () => EnforcementDeniedError,
|
|
45
|
+
KernelProvider: () => KernelProvider,
|
|
46
|
+
QueryClientProvider: () => reactQuery.QueryClientProvider,
|
|
47
|
+
auth: () => auth,
|
|
48
|
+
awaitScopeReady: () => awaitScopeReady,
|
|
49
|
+
callTool: () => callTool,
|
|
50
|
+
clearCachedToken: () => clearCachedToken,
|
|
51
|
+
clearSession: () => clearSession,
|
|
52
|
+
createItem: () => createItem,
|
|
53
|
+
disconnectSSE: () => disconnectSSE,
|
|
54
|
+
disposeVfsRealtimeWs: () => disposeVfsRealtimeWs,
|
|
55
|
+
emitEvent: () => emitEvent,
|
|
56
|
+
fetchChannelMessages: () => fetchChannelMessages,
|
|
57
|
+
fetchChannelMessagesPage: () => fetchChannelMessagesPage,
|
|
58
|
+
fetchEdgesForItem: () => fetchEdgesForItem,
|
|
59
|
+
fetchItemHistory: () => fetchItemHistory,
|
|
60
|
+
fetchItems: () => fetchItems,
|
|
61
|
+
fetchMemberFocus: () => fetchMemberFocus,
|
|
62
|
+
fetchOpenEnvelope: () => fetchOpenEnvelope,
|
|
63
|
+
fetchRecentActivity: () => fetchRecentActivity,
|
|
64
|
+
fetchTypeDefinitions: () => fetchTypeDefinitions,
|
|
65
|
+
fetchVfsChildren: () => fetchVfsChildren,
|
|
66
|
+
fetchVfsChildrenPage: () => fetchVfsChildrenPage,
|
|
67
|
+
fetchVfsItem: () => fetchVfsItem,
|
|
68
|
+
fetchVfsItemById: () => fetchVfsItemById,
|
|
69
|
+
fetchVfsTree: () => fetchVfsTree,
|
|
70
|
+
gatewayCall: () => gatewayCall,
|
|
71
|
+
getAccessToken: () => getAccessToken,
|
|
72
|
+
getAssetUrl: () => getAssetUrl,
|
|
73
|
+
getDefaultSort: () => getDefaultSort,
|
|
74
|
+
getRequiredFormFields: () => getRequiredFormFields,
|
|
75
|
+
getScope: () => getScope,
|
|
76
|
+
getScopeVersion: () => getScopeVersion,
|
|
77
|
+
getSessionEntry: () => getSessionEntry,
|
|
78
|
+
initSession: () => initSession,
|
|
79
|
+
initVfsRealtimeWs: () => initVfsRealtimeWs,
|
|
80
|
+
interpretSchema: () => interpretSchema,
|
|
81
|
+
invalidateAllVfs: () => invalidateAllVfs,
|
|
82
|
+
invalidateChannelMessages: () => invalidateChannelMessages,
|
|
83
|
+
invalidateChildren: () => invalidateChildren,
|
|
84
|
+
invalidateItem: () => invalidateItem,
|
|
85
|
+
invalidateOpenEnvelope: () => invalidateOpenEnvelope,
|
|
86
|
+
invalidatePathAndParent: () => invalidatePathAndParent,
|
|
87
|
+
invalidateSubtree: () => invalidateSubtree,
|
|
88
|
+
invalidateTypes: () => invalidateTypes,
|
|
89
|
+
isScopeReady: () => isScopeReady,
|
|
90
|
+
listChildren: () => listChildren,
|
|
91
|
+
mount: () => mount,
|
|
92
|
+
mountRealtimeScope: () => mountRealtimeScope,
|
|
93
|
+
normalizeItem: () => normalizeItem,
|
|
94
|
+
ping: () => ping,
|
|
95
|
+
pushChanges: () => pushChanges,
|
|
96
|
+
queryClient: () => queryClient,
|
|
97
|
+
readItem: () => readItem,
|
|
98
|
+
registerCleanup: () => registerCleanup,
|
|
99
|
+
resetAllSdkState: () => resetAllSdkState,
|
|
100
|
+
resolvePrincipalId: () => resolvePrincipalId,
|
|
101
|
+
setCachedToken: () => setCachedToken,
|
|
102
|
+
setScope: () => setScope,
|
|
103
|
+
setupRequireShim: () => setupRequireShim,
|
|
104
|
+
signal: () => signal,
|
|
105
|
+
subscribeScope: () => subscribeScope,
|
|
106
|
+
subscribeToEvents: () => subscribeToEvents,
|
|
107
|
+
subscribeToPath: () => subscribeToPath,
|
|
108
|
+
subscribeToScope: () => subscribeToScope,
|
|
109
|
+
unmountRealtimeScope: () => unmountRealtimeScope,
|
|
110
|
+
updateItem: () => updateItem,
|
|
111
|
+
uploadFile: () => uploadFile,
|
|
112
|
+
useAccounts: () => useAccounts,
|
|
113
|
+
useAllChannels: () => useAllChannels,
|
|
114
|
+
useAsset: () => useAsset,
|
|
115
|
+
useAuth: () => useAuth,
|
|
116
|
+
useChannelMessages: () => useChannelMessages,
|
|
117
|
+
useChannels: () => useChannels,
|
|
118
|
+
useChildren: () => useChildren,
|
|
119
|
+
useComponent: () => useComponent,
|
|
120
|
+
useCreate: () => useCreate,
|
|
121
|
+
useDelete: () => useDelete,
|
|
122
|
+
useDmChannels: () => useDmChannels,
|
|
123
|
+
useEdges: () => useEdges,
|
|
124
|
+
useEventStream: () => useEventStream,
|
|
125
|
+
useFileUpload: () => useFileUpload,
|
|
126
|
+
useInfiniteChannelMessages: () => useInfiniteChannelMessages,
|
|
127
|
+
useInfiniteChildren: () => useInfiniteChildren,
|
|
128
|
+
useInfiniteItems: () => useInfiniteItems,
|
|
129
|
+
useItem: () => useItem,
|
|
130
|
+
useItemById: () => useItemById,
|
|
131
|
+
useItemHistory: () => useItemHistory,
|
|
132
|
+
useItems: () => useItems,
|
|
133
|
+
useLink: () => useLink,
|
|
134
|
+
useList: () => useList,
|
|
135
|
+
useMemberFocus: () => useMemberFocus,
|
|
136
|
+
useMove: () => useMove,
|
|
137
|
+
useMutation: () => useMutation,
|
|
138
|
+
useNotifications: () => useNotifications,
|
|
139
|
+
useOpen: () => useOpen,
|
|
140
|
+
usePermission: () => usePermission,
|
|
141
|
+
usePermissions: () => usePermissions,
|
|
142
|
+
usePrincipal: () => usePrincipal,
|
|
143
|
+
useRealtimeQuery: () => useRealtimeQuery,
|
|
144
|
+
useRecentActivity: () => useRecentActivity,
|
|
145
|
+
useScope: () => useScope,
|
|
146
|
+
useScopeReady: () => useScopeReady,
|
|
147
|
+
useSearch: () => useSearch,
|
|
148
|
+
useTheme: () => useTheme,
|
|
149
|
+
useTool: () => useTool,
|
|
150
|
+
useTree: () => useTree,
|
|
151
|
+
useType: () => useType,
|
|
152
|
+
useTypeHelpers: () => useTypeHelpers,
|
|
153
|
+
useTypes: () => useTypes,
|
|
154
|
+
useUnreadCounts: () => useUnreadCounts,
|
|
155
|
+
useUpdate: () => useUpdate,
|
|
156
|
+
vfsKeys: () => vfsKeys
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// src/enforcement.ts
|
|
160
|
+
var EnforcementDeniedError = class _EnforcementDeniedError extends Error {
|
|
161
|
+
name = "EnforcementDeniedError";
|
|
162
|
+
deniedBy;
|
|
163
|
+
ruleKey;
|
|
164
|
+
displayName;
|
|
165
|
+
constructor(payload) {
|
|
166
|
+
const message = payload.message || _EnforcementDeniedError.fallbackMessage(payload);
|
|
167
|
+
super(message);
|
|
168
|
+
this.deniedBy = payload.denied_by || payload.error_type || payload.error || "unknown";
|
|
169
|
+
const enforcement = payload.enforcement;
|
|
170
|
+
this.ruleKey = enforcement?.rule_key ?? void 0;
|
|
171
|
+
this.displayName = enforcement?.display_name ?? void 0;
|
|
172
|
+
}
|
|
173
|
+
static fallbackMessage(payload) {
|
|
174
|
+
const err = payload.error || "";
|
|
175
|
+
const deniedBy = payload.denied_by || "";
|
|
176
|
+
if (deniedBy === "rule" || err === "RULE_DENIED") {
|
|
177
|
+
return "This feature is not enabled for this workspace.";
|
|
178
|
+
}
|
|
179
|
+
if (deniedBy === "entitlement" || err === "ENTITLEMENT_REQUIRED") {
|
|
180
|
+
return "This feature requires a plan upgrade.";
|
|
181
|
+
}
|
|
182
|
+
if (deniedBy === "access" || err === "ACCESS_DENIED" || err === "PERMISSION_DENIED") {
|
|
183
|
+
return "You don't have permission to do this.";
|
|
184
|
+
}
|
|
185
|
+
return "This action is not allowed.";
|
|
186
|
+
}
|
|
187
|
+
get isRuleDenial() {
|
|
188
|
+
return this.deniedBy === "rule" || this.deniedBy === "RULE_DENIED";
|
|
189
|
+
}
|
|
190
|
+
get isAccessDenial() {
|
|
191
|
+
return this.deniedBy === "access" || this.deniedBy === "ACCESS_DENIED";
|
|
192
|
+
}
|
|
193
|
+
get isEntitlementDenial() {
|
|
194
|
+
return this.deniedBy === "entitlement" || this.deniedBy === "ENTITLEMENT_REQUIRED";
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
var CONFIG = typeof window !== "undefined" && window.__FSAOS_CONFIG__ || {};
|
|
198
|
+
var SUPABASE_URL = CONFIG.supabaseUrl || "https://vahbmsslxuustnlvsrkg.supabase.co";
|
|
199
|
+
var SUPABASE_ANON_KEY = CONFIG.supabaseAnonKey || "sb_publishable_ZjosozAjfpZ4InMNElTr6Q_hHB-f5nc";
|
|
200
|
+
var GATEWAY_URL = CONFIG.gatewayUrl || "https://fsaos-mcp-gw-rust.fly.dev";
|
|
201
|
+
var supabase = supabaseJs.createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
|
202
|
+
auth: {
|
|
203
|
+
persistSession: true,
|
|
204
|
+
autoRefreshToken: true,
|
|
205
|
+
detectSessionInUrl: false
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// src/session.ts
|
|
210
|
+
var cachedToken = null;
|
|
211
|
+
var tokenReady;
|
|
212
|
+
var resolveTokenReady;
|
|
213
|
+
var tokenResolved = false;
|
|
214
|
+
tokenReady = new Promise((resolve) => {
|
|
215
|
+
resolveTokenReady = resolve;
|
|
216
|
+
});
|
|
217
|
+
supabase.auth.onAuthStateChange((_event, session) => {
|
|
218
|
+
if (session?.access_token) {
|
|
219
|
+
cachedToken = session.access_token;
|
|
220
|
+
session.expires_at ?? Math.floor(Date.now() / 1e3) + 3600;
|
|
221
|
+
} else {
|
|
222
|
+
cachedToken = null;
|
|
223
|
+
}
|
|
224
|
+
if (!tokenResolved) {
|
|
225
|
+
tokenResolved = true;
|
|
226
|
+
resolveTokenReady();
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
setTimeout(() => {
|
|
230
|
+
if (!tokenResolved) {
|
|
231
|
+
tokenResolved = true;
|
|
232
|
+
resolveTokenReady();
|
|
233
|
+
}
|
|
234
|
+
}, 5e3);
|
|
235
|
+
function setCachedToken(token, expiresAt) {
|
|
236
|
+
cachedToken = token;
|
|
237
|
+
}
|
|
238
|
+
function clearCachedToken() {
|
|
239
|
+
cachedToken = null;
|
|
240
|
+
}
|
|
241
|
+
async function getAccessToken() {
|
|
242
|
+
await tokenReady;
|
|
243
|
+
return cachedToken;
|
|
244
|
+
}
|
|
245
|
+
var currentScopePath = null;
|
|
246
|
+
var scopeVersion = 0;
|
|
247
|
+
var scopeListeners = /* @__PURE__ */ new Set();
|
|
248
|
+
var scopeReady;
|
|
249
|
+
var resolveScopeReady;
|
|
250
|
+
var scopeResolved = false;
|
|
251
|
+
scopeReady = new Promise((resolve) => {
|
|
252
|
+
resolveScopeReady = resolve;
|
|
253
|
+
});
|
|
254
|
+
function setScope(scopePath) {
|
|
255
|
+
const changed = currentScopePath !== scopePath;
|
|
256
|
+
currentScopePath = scopePath;
|
|
257
|
+
if (!scopeResolved) {
|
|
258
|
+
scopeResolved = true;
|
|
259
|
+
resolveScopeReady();
|
|
260
|
+
}
|
|
261
|
+
if (changed) {
|
|
262
|
+
scopeVersion++;
|
|
263
|
+
scopeListeners.forEach((listener) => {
|
|
264
|
+
try {
|
|
265
|
+
listener();
|
|
266
|
+
} catch (e) {
|
|
267
|
+
console.warn("[SDK] Scope listener error:", e);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function getScope() {
|
|
273
|
+
return currentScopePath;
|
|
274
|
+
}
|
|
275
|
+
function getScopeVersion() {
|
|
276
|
+
return scopeVersion;
|
|
277
|
+
}
|
|
278
|
+
function subscribeScope(listener) {
|
|
279
|
+
scopeListeners.add(listener);
|
|
280
|
+
return () => {
|
|
281
|
+
scopeListeners.delete(listener);
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function awaitScopeReady() {
|
|
285
|
+
return scopeReady;
|
|
286
|
+
}
|
|
287
|
+
function isScopeReady() {
|
|
288
|
+
return scopeResolved;
|
|
289
|
+
}
|
|
290
|
+
var sessionEntry = null;
|
|
291
|
+
var sessionPromise = null;
|
|
292
|
+
async function initSession() {
|
|
293
|
+
if (sessionEntry) return sessionEntry;
|
|
294
|
+
if (sessionPromise) return sessionPromise;
|
|
295
|
+
sessionPromise = (async () => {
|
|
296
|
+
const hostname = getHostname();
|
|
297
|
+
const token = await getAccessToken();
|
|
298
|
+
const headers = {};
|
|
299
|
+
if (token) {
|
|
300
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
301
|
+
}
|
|
302
|
+
const response = await fetch(`${GATEWAY_URL}/d/${hostname}/init`, {
|
|
303
|
+
method: "GET",
|
|
304
|
+
headers
|
|
305
|
+
});
|
|
306
|
+
if (!response.ok) {
|
|
307
|
+
throw new Error(`Session init failed: ${response.status} ${response.statusText}`);
|
|
308
|
+
}
|
|
309
|
+
const json = await response.json();
|
|
310
|
+
const entry = json.entry || json;
|
|
311
|
+
sessionEntry = {
|
|
312
|
+
scope_id: entry.scope_id,
|
|
313
|
+
scope_path: entry.scope_path,
|
|
314
|
+
instance_path: entry.instance_path || "/root",
|
|
315
|
+
fractal_id: entry.fractal_id || null,
|
|
316
|
+
instance_name: entry.instance_name || null,
|
|
317
|
+
display_name: entry.scope_display_name || entry.display_name || null
|
|
318
|
+
};
|
|
319
|
+
return sessionEntry;
|
|
320
|
+
})();
|
|
321
|
+
return sessionPromise;
|
|
322
|
+
}
|
|
323
|
+
function getSessionEntry() {
|
|
324
|
+
return sessionEntry;
|
|
325
|
+
}
|
|
326
|
+
function clearSession() {
|
|
327
|
+
sessionEntry = null;
|
|
328
|
+
sessionPromise = null;
|
|
329
|
+
currentScopePath = null;
|
|
330
|
+
}
|
|
331
|
+
var cleanupRegistry = [];
|
|
332
|
+
function registerCleanup(fn) {
|
|
333
|
+
cleanupRegistry.push(fn);
|
|
334
|
+
}
|
|
335
|
+
function resetAllSdkState() {
|
|
336
|
+
clearSession();
|
|
337
|
+
clearCachedToken();
|
|
338
|
+
scopeResolved = false;
|
|
339
|
+
scopeReady = new Promise((resolve) => {
|
|
340
|
+
resolveScopeReady = resolve;
|
|
341
|
+
});
|
|
342
|
+
scopeVersion++;
|
|
343
|
+
scopeListeners.forEach((listener) => {
|
|
344
|
+
try {
|
|
345
|
+
listener();
|
|
346
|
+
} catch (e) {
|
|
347
|
+
console.warn("[SDK] Scope listener error:", e);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
for (const fn of cleanupRegistry) {
|
|
351
|
+
try {
|
|
352
|
+
fn();
|
|
353
|
+
} catch (e) {
|
|
354
|
+
console.warn("[SDK] Cleanup function failed:", e);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function getHostname() {
|
|
359
|
+
if (typeof window !== "undefined") {
|
|
360
|
+
const cfg = window.__FSAOS_CONFIG__;
|
|
361
|
+
if (cfg?.hostname) return cfg.hostname;
|
|
362
|
+
return window.location.hostname;
|
|
363
|
+
}
|
|
364
|
+
return "localhost";
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/client.ts
|
|
368
|
+
var SCOPE_INDEPENDENT_METHODS = /* @__PURE__ */ new Set([
|
|
369
|
+
"list-memberships",
|
|
370
|
+
"init"
|
|
371
|
+
]);
|
|
372
|
+
async function gatewayCall(method, params = {}) {
|
|
373
|
+
const hostname = getHostname();
|
|
374
|
+
const url = `${GATEWAY_URL}/d/${hostname}`;
|
|
375
|
+
const headers = {
|
|
376
|
+
"Content-Type": "application/json"
|
|
377
|
+
};
|
|
378
|
+
const token = await getAccessToken();
|
|
379
|
+
if (token) {
|
|
380
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
381
|
+
}
|
|
382
|
+
if (!SCOPE_INDEPENDENT_METHODS.has(method) && !params._scope_path) {
|
|
383
|
+
await awaitScopeReady();
|
|
384
|
+
}
|
|
385
|
+
const enrichedParams = { ...params };
|
|
386
|
+
const session = getSessionEntry();
|
|
387
|
+
if (!enrichedParams.domain_scope_id && session?.scope_id) {
|
|
388
|
+
enrichedParams.domain_scope_id = session.scope_id;
|
|
389
|
+
}
|
|
390
|
+
const scopePath = getScope();
|
|
391
|
+
if (!enrichedParams._scope_path && scopePath) {
|
|
392
|
+
enrichedParams._scope_path = scopePath;
|
|
393
|
+
}
|
|
394
|
+
const response = await fetch(url, {
|
|
395
|
+
method: "POST",
|
|
396
|
+
headers,
|
|
397
|
+
body: JSON.stringify({ method, params: enrichedParams })
|
|
398
|
+
});
|
|
399
|
+
if (!response.ok) {
|
|
400
|
+
throw new Error(`Gateway HTTP error: ${response.status} ${response.statusText}`);
|
|
401
|
+
}
|
|
402
|
+
const json = await response.json();
|
|
403
|
+
if (json.success === false) {
|
|
404
|
+
const err = json.error || "";
|
|
405
|
+
const deniedBy = json.denied_by || json.error_type || "";
|
|
406
|
+
if (deniedBy === "rule" || deniedBy === "access" || deniedBy === "entitlement" || err === "RULE_DENIED" || err === "ACCESS_DENIED" || err === "PERMISSION_DENIED" || err === "ENTITLEMENT_REQUIRED") {
|
|
407
|
+
throw new EnforcementDeniedError(json);
|
|
408
|
+
}
|
|
409
|
+
throw new Error(
|
|
410
|
+
json.message || json.error || `Gateway call ${method} failed`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
return json;
|
|
414
|
+
}
|
|
415
|
+
var queryClient = new reactQuery.QueryClient({
|
|
416
|
+
defaultOptions: {
|
|
417
|
+
queries: {
|
|
418
|
+
staleTime: 1e3 * 60 * 2,
|
|
419
|
+
// 2 minutes
|
|
420
|
+
gcTime: 1e3 * 60 * 10,
|
|
421
|
+
// 10 minutes
|
|
422
|
+
refetchOnWindowFocus: false,
|
|
423
|
+
retry: 1
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// src/vfs-keys.ts
|
|
429
|
+
function sk() {
|
|
430
|
+
return getScope() ?? "__unscoped__";
|
|
431
|
+
}
|
|
432
|
+
var vfsKeys = {
|
|
433
|
+
/** Root key for all VFS queries (scope-aware). */
|
|
434
|
+
all: () => ["vfs", sk()],
|
|
435
|
+
/** Single item by path (scope-aware — relative paths resolve per-scope). */
|
|
436
|
+
item: (path) => ["vfs", sk(), "item", path],
|
|
437
|
+
/** Single item by UUID (scope-independent — UUIDs are globally unique). */
|
|
438
|
+
itemById: (id) => ["vfs", "item-by-id", id],
|
|
439
|
+
/** Children of a path (scope-aware). */
|
|
440
|
+
children: (path) => ["vfs", sk(), "children", path],
|
|
441
|
+
/** All children queries for current scope (for broad invalidation). */
|
|
442
|
+
allChildren: () => ["vfs", sk(), "children"],
|
|
443
|
+
/** Type definitions for a scope (or default). */
|
|
444
|
+
types: (scopeId) => ["vfs", "types", scopeId ?? "default"],
|
|
445
|
+
/** Edges for an item (scope-independent — edges are by item UUID). */
|
|
446
|
+
edges: (itemId) => ["vfs", "edges", itemId],
|
|
447
|
+
/** Search results (scope-aware — search is scoped). */
|
|
448
|
+
search: (query, types) => ["vfs", sk(), "search", query, ...types ?? []],
|
|
449
|
+
/** Items query (type-based listing with filters, scope-aware). */
|
|
450
|
+
items: (type, filters) => ["vfs", sk(), "items", type, JSON.stringify(filters ?? {})],
|
|
451
|
+
/** All items queries for current scope (for broad invalidation). */
|
|
452
|
+
allItems: () => ["vfs", sk(), "items"],
|
|
453
|
+
/** Recursive tree at a path + depth (scope-aware). */
|
|
454
|
+
tree: (path, depth) => ["vfs", sk(), "tree", path, depth ?? 1],
|
|
455
|
+
/** Open envelope for a path (scope-aware). */
|
|
456
|
+
openEnvelope: (path, strategy) => ["vfs", sk(), "open", path, strategy ?? "__default__"],
|
|
457
|
+
/** All open-envelope queries for current scope (for broad invalidation). */
|
|
458
|
+
allOpenEnvelopes: () => ["vfs", sk(), "open"],
|
|
459
|
+
/** Member focus for a scope. */
|
|
460
|
+
memberFocus: (scopeId) => ["vfs", "member-focus", scopeId],
|
|
461
|
+
/** Fractal instances (scope-independent). */
|
|
462
|
+
fractalInstances: () => ["vfs", "fractal-instances"],
|
|
463
|
+
/** Recent activity (scope-aware). */
|
|
464
|
+
recentActivity: (a, b, c) => ["vfs", sk(), "recent-activity", a, b, c],
|
|
465
|
+
/** Item history (scope-independent — by item UUID). */
|
|
466
|
+
itemHistory: (a, b, c) => ["vfs", "item-history", a, b, c],
|
|
467
|
+
/** Channel messages (scope-aware — channels are scoped). */
|
|
468
|
+
channelMessages: (channelPath, parentMessageId) => ["vfs", sk(), "channel-messages", channelPath, parentMessageId ?? "__top__"],
|
|
469
|
+
/** All channel-messages queries for current scope (for broad invalidation). */
|
|
470
|
+
allChannelMessages: () => ["vfs", sk(), "channel-messages"],
|
|
471
|
+
/** Channels listing (scope-aware). */
|
|
472
|
+
channels: (filter) => ["vfs", sk(), "channels", filter ?? "all"],
|
|
473
|
+
/** All channels queries for current scope (for broad invalidation). */
|
|
474
|
+
allChannels: () => ["vfs", sk(), "channels"],
|
|
475
|
+
/** Notifications listing (scope-aware). */
|
|
476
|
+
notifications: (filter, limit) => ["vfs", sk(), "notifications", filter ?? "all", limit ?? 200],
|
|
477
|
+
/** All notification queries for current scope (for broad invalidation). */
|
|
478
|
+
allNotifications: () => ["vfs", sk(), "notifications"],
|
|
479
|
+
/** Unread counts (scope-aware). */
|
|
480
|
+
unreadCounts: () => ["vfs", sk(), "unread-counts"],
|
|
481
|
+
/** Membership graph for a principal (scope-independent). */
|
|
482
|
+
memberships: (principalId) => ["memberships", principalId]
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
// src/vfs.ts
|
|
486
|
+
function nowISO() {
|
|
487
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
488
|
+
}
|
|
489
|
+
function randomId() {
|
|
490
|
+
return crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
491
|
+
}
|
|
492
|
+
function normalizeItem(raw) {
|
|
493
|
+
const path = raw.path || raw.item_path || "";
|
|
494
|
+
const segments = path.split("/");
|
|
495
|
+
segments.pop();
|
|
496
|
+
const parentPath = segments.join("/");
|
|
497
|
+
const typeData = raw.type_data || {};
|
|
498
|
+
if (typeData.allowed_parent_types && !typeData.accepts) {
|
|
499
|
+
typeData.accepts = typeData.allowed_parent_types;
|
|
500
|
+
}
|
|
501
|
+
delete typeData.allowed_parent_types;
|
|
502
|
+
return {
|
|
503
|
+
id: raw.id || raw.item_id || "",
|
|
504
|
+
name: raw.name || raw.item_name || "",
|
|
505
|
+
item_type: raw.item_type || "unknown",
|
|
506
|
+
path,
|
|
507
|
+
parent_path: parentPath,
|
|
508
|
+
is_active: raw.is_active !== false,
|
|
509
|
+
has_children: raw.has_children ?? false,
|
|
510
|
+
created_at: raw.created_at || nowISO(),
|
|
511
|
+
updated_at: raw.updated_at || nowISO(),
|
|
512
|
+
visibility: raw.visibility,
|
|
513
|
+
type_data: typeData,
|
|
514
|
+
scope_item_id: raw.scope_item_id,
|
|
515
|
+
fractal_id: raw.fractal_id,
|
|
516
|
+
parent_instance_id: raw.parent_instance_id,
|
|
517
|
+
owner_principal_id: raw.owner_principal_id,
|
|
518
|
+
created_by_principal_id: raw.created_by_principal_id,
|
|
519
|
+
parent_id: raw.parent_id
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
function extractItems(response) {
|
|
523
|
+
return (response.content?.items || response.items || []).map(normalizeItem);
|
|
524
|
+
}
|
|
525
|
+
async function fetchVfsItemById(id) {
|
|
526
|
+
try {
|
|
527
|
+
const response = await gatewayCall("read", { id });
|
|
528
|
+
if (response.item_name || response.item_path) {
|
|
529
|
+
const item2 = normalizeItem(response);
|
|
530
|
+
if (item2.path) {
|
|
531
|
+
queryClient.setQueryData(vfsKeys.item(item2.path), item2);
|
|
532
|
+
}
|
|
533
|
+
return item2;
|
|
534
|
+
}
|
|
535
|
+
const item = response.item;
|
|
536
|
+
if (item) {
|
|
537
|
+
const normalized = normalizeItem(item);
|
|
538
|
+
if (normalized.path) {
|
|
539
|
+
queryClient.setQueryData(vfsKeys.item(normalized.path), normalized);
|
|
540
|
+
}
|
|
541
|
+
return normalized;
|
|
542
|
+
}
|
|
543
|
+
return null;
|
|
544
|
+
} catch {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
async function fetchVfsItem(path) {
|
|
549
|
+
try {
|
|
550
|
+
const response = await gatewayCall("read", { path });
|
|
551
|
+
if (response.item_name || response.item_path) {
|
|
552
|
+
return normalizeItem(response);
|
|
553
|
+
}
|
|
554
|
+
const item = response.item;
|
|
555
|
+
return item ? normalizeItem(item) : null;
|
|
556
|
+
} catch {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
async function fetchVfsChildren(path) {
|
|
561
|
+
try {
|
|
562
|
+
const response = await gatewayCall("list", { path, limit: 200 });
|
|
563
|
+
const items = extractItems(response);
|
|
564
|
+
for (const item of items) {
|
|
565
|
+
queryClient.setQueryData(vfsKeys.item(item.path), item);
|
|
566
|
+
}
|
|
567
|
+
return items;
|
|
568
|
+
} catch (err) {
|
|
569
|
+
console.error(`[gateway] Failed to fetch children of ${path}:`, err);
|
|
570
|
+
return [];
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
async function fetchVfsChildrenPage(path, limit, offset) {
|
|
574
|
+
try {
|
|
575
|
+
const response = await gatewayCall("list", { path, limit, offset });
|
|
576
|
+
const items = extractItems(response);
|
|
577
|
+
for (const item of items) {
|
|
578
|
+
queryClient.setQueryData(vfsKeys.item(item.path), item);
|
|
579
|
+
}
|
|
580
|
+
return { items, total_hint: response.total ?? response.content?.total ?? items.length };
|
|
581
|
+
} catch (err) {
|
|
582
|
+
console.error(`[gateway] Failed to fetch children page of ${path}:`, err);
|
|
583
|
+
return { items: [], total_hint: 0 };
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
async function fetchTypeDefinitions(scopeId) {
|
|
587
|
+
const params = scopeId ? { scope_id: scopeId } : {};
|
|
588
|
+
const response = await gatewayCall("types-list", params);
|
|
589
|
+
const rawItems = response.content?.items || response.items || [];
|
|
590
|
+
const types = /* @__PURE__ */ new Map();
|
|
591
|
+
for (const raw of rawItems) {
|
|
592
|
+
const name = raw.name;
|
|
593
|
+
const td = raw.type_data;
|
|
594
|
+
if (!name || !td) continue;
|
|
595
|
+
types.set(name, {
|
|
596
|
+
type_key: name,
|
|
597
|
+
display_name: td.display_name || name,
|
|
598
|
+
display_name_plural: td.display_name_plural || name + "s",
|
|
599
|
+
icon: td.icon || "File",
|
|
600
|
+
color: td.color || "gray",
|
|
601
|
+
description: td.description || "",
|
|
602
|
+
input_schema: td.input_schema || {},
|
|
603
|
+
system_schema: td.system_schema || {},
|
|
604
|
+
json_schema: td.input_schema || td.json_schema || {},
|
|
605
|
+
default_data: td.default_data || {},
|
|
606
|
+
field_defaults: td.field_defaults || {},
|
|
607
|
+
renderer_config: td.renderer_config || {},
|
|
608
|
+
is_system: td.is_system ?? false,
|
|
609
|
+
is_active: td.is_active ?? true,
|
|
610
|
+
is_container: td.is_container ?? false,
|
|
611
|
+
is_scope: td.is_scope ?? false,
|
|
612
|
+
render_mode: td.render_mode || "none",
|
|
613
|
+
placement_mode: td.placement_mode,
|
|
614
|
+
direct_parent_types: td.direct_parent_types,
|
|
615
|
+
allowed_parent_types: td.allowed_parent_types,
|
|
616
|
+
governed_create: td.governed_create,
|
|
617
|
+
create_method: td.create_method,
|
|
618
|
+
dedup: td.dedup,
|
|
619
|
+
edges: td.edges,
|
|
620
|
+
events: td.events
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
return types;
|
|
624
|
+
}
|
|
625
|
+
async function fetchEdgesForItem(itemId) {
|
|
626
|
+
try {
|
|
627
|
+
const response = await gatewayCall("edges", { item_id: itemId });
|
|
628
|
+
const outgoing = response.outgoing || [];
|
|
629
|
+
const incoming = response.incoming || [];
|
|
630
|
+
const edges = [];
|
|
631
|
+
for (const edge of outgoing) {
|
|
632
|
+
edges.push({
|
|
633
|
+
id: edge.edge_id || randomId(),
|
|
634
|
+
source_item_id: itemId,
|
|
635
|
+
target_item_id: edge.target_id || "",
|
|
636
|
+
edge_type: edge.edge_type || "",
|
|
637
|
+
weight: edge.weight ?? 1,
|
|
638
|
+
context: edge.context || {},
|
|
639
|
+
is_active: true,
|
|
640
|
+
is_bidirectional: edge.is_bidirectional
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
for (const edge of incoming) {
|
|
644
|
+
edges.push({
|
|
645
|
+
id: edge.edge_id || randomId(),
|
|
646
|
+
source_item_id: edge.source_id || "",
|
|
647
|
+
target_item_id: itemId,
|
|
648
|
+
edge_type: edge.edge_type || "",
|
|
649
|
+
weight: edge.weight ?? 1,
|
|
650
|
+
context: edge.context || {},
|
|
651
|
+
is_active: true,
|
|
652
|
+
is_bidirectional: edge.is_bidirectional
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
return edges;
|
|
656
|
+
} catch (err) {
|
|
657
|
+
console.warn(`[gateway] Failed to fetch edges for ${itemId}:`, err);
|
|
658
|
+
return [];
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
async function fetchVfsTree(path, maxDepth = 1, itemTypes, limit) {
|
|
662
|
+
try {
|
|
663
|
+
const params = {
|
|
664
|
+
path,
|
|
665
|
+
max_depth: maxDepth > 0 ? maxDepth : 999
|
|
666
|
+
};
|
|
667
|
+
if (itemTypes?.length) params.item_types = itemTypes;
|
|
668
|
+
if (limit) params.limit = limit;
|
|
669
|
+
const response = await gatewayCall("tree", params);
|
|
670
|
+
const items = extractItems(response);
|
|
671
|
+
for (const item of items) {
|
|
672
|
+
queryClient.setQueryData(vfsKeys.item(item.path), item);
|
|
673
|
+
}
|
|
674
|
+
return items;
|
|
675
|
+
} catch (err) {
|
|
676
|
+
console.error(`[gateway] Failed to fetch tree at ${path}:`, err);
|
|
677
|
+
return [];
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
async function fetchRecentActivity(scopePath, limit = 50, offset = 0) {
|
|
681
|
+
try {
|
|
682
|
+
const response = await gatewayCall("recent-activity", {
|
|
683
|
+
scope_path: scopePath,
|
|
684
|
+
limit,
|
|
685
|
+
offset
|
|
686
|
+
});
|
|
687
|
+
return response.content?.items || response.items || response.entries || [];
|
|
688
|
+
} catch (err) {
|
|
689
|
+
console.error(`[gateway] Failed to fetch recent activity for ${scopePath}:`, err);
|
|
690
|
+
return [];
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
async function fetchItemHistory(itemId, limit = 50, offset = 0) {
|
|
694
|
+
try {
|
|
695
|
+
const response = await gatewayCall("item-history", {
|
|
696
|
+
item_id: itemId,
|
|
697
|
+
limit,
|
|
698
|
+
offset
|
|
699
|
+
});
|
|
700
|
+
return {
|
|
701
|
+
versions: response.content?.items || response.versions || [],
|
|
702
|
+
total: response.total ?? 0
|
|
703
|
+
};
|
|
704
|
+
} catch (err) {
|
|
705
|
+
console.error(`[gateway] Failed to fetch item history for ${itemId}:`, err);
|
|
706
|
+
return { versions: [], total: 0 };
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
async function fetchMemberFocus(scopeId, itemTypes, limit) {
|
|
710
|
+
try {
|
|
711
|
+
const params = { scope_id: scopeId };
|
|
712
|
+
if (itemTypes?.length) params.item_types = itemTypes;
|
|
713
|
+
if (limit) params.limit = limit;
|
|
714
|
+
const response = await gatewayCall("member-focus", params);
|
|
715
|
+
const items = extractItems(response);
|
|
716
|
+
for (const item of items) {
|
|
717
|
+
queryClient.setQueryData(vfsKeys.item(item.path), item);
|
|
718
|
+
}
|
|
719
|
+
return items;
|
|
720
|
+
} catch (err) {
|
|
721
|
+
console.error(`[gateway] Failed to fetch member focus for scope ${scopeId}:`, err);
|
|
722
|
+
return [];
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
async function fetchOpenEnvelope(path, options) {
|
|
726
|
+
const params = { path };
|
|
727
|
+
if (options?.mode) params.mode = options.mode;
|
|
728
|
+
if (options?.strategy) params.strategy = options.strategy;
|
|
729
|
+
if (options?.arguments) params.arguments = options.arguments;
|
|
730
|
+
const response = await gatewayCall("open", params);
|
|
731
|
+
const r = response;
|
|
732
|
+
return {
|
|
733
|
+
item_id: r.item_id,
|
|
734
|
+
item_path: r.item_path,
|
|
735
|
+
item_name: r.item_name,
|
|
736
|
+
item_type: r.item_type,
|
|
737
|
+
render: r.render,
|
|
738
|
+
compatible_components: r.compatible_components ?? [],
|
|
739
|
+
content: r.content,
|
|
740
|
+
metadata: r.metadata,
|
|
741
|
+
instructions: r.instructions
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
async function fetchItems(filter) {
|
|
745
|
+
const params = {
|
|
746
|
+
item_type: filter.type
|
|
747
|
+
};
|
|
748
|
+
if (filter.scope) {
|
|
749
|
+
params.scope_path = filter.scope;
|
|
750
|
+
}
|
|
751
|
+
const filters = {};
|
|
752
|
+
if (filter.name) filters.name = filter.name;
|
|
753
|
+
if (filter.tag) filters.tag = filter.tag;
|
|
754
|
+
if (filter.parent_id) filters.parent_id = filter.parent_id;
|
|
755
|
+
if (filter.sort_by) filters.sort_by = filter.sort_by;
|
|
756
|
+
if (filter.sort_dir) filters.sort_dir = filter.sort_dir;
|
|
757
|
+
if (filter.offset != null) filters.offset = filter.offset;
|
|
758
|
+
if (filter.fields) {
|
|
759
|
+
for (const [key, value] of Object.entries(filter.fields)) {
|
|
760
|
+
filters[`field.${key}`] = String(value);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (Object.keys(filters).length > 0) {
|
|
764
|
+
params.filters = filters;
|
|
765
|
+
}
|
|
766
|
+
if (filter.limit != null) {
|
|
767
|
+
params.limit = filter.limit;
|
|
768
|
+
}
|
|
769
|
+
try {
|
|
770
|
+
const response = await gatewayCall("search", params);
|
|
771
|
+
const items = extractItems(response);
|
|
772
|
+
for (const item of items) {
|
|
773
|
+
queryClient.setQueryData(vfsKeys.item(item.path), item);
|
|
774
|
+
}
|
|
775
|
+
return { items, total_hint: response.total ?? response.content?.total ?? items.length };
|
|
776
|
+
} catch (err) {
|
|
777
|
+
console.error(`[gateway] Failed to fetch items of type ${filter.type}:`, err);
|
|
778
|
+
return { items: [], total_hint: 0 };
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
function parseChannelMessages(rawItems) {
|
|
782
|
+
return rawItems.filter((i) => i.item_type === "message").map((i) => {
|
|
783
|
+
const td = i.type_data || {};
|
|
784
|
+
let role = "user";
|
|
785
|
+
if (td.role === "assistant") role = "assistant";
|
|
786
|
+
else if (td.role === "system") role = "system";
|
|
787
|
+
const metadata = td.metadata || {};
|
|
788
|
+
return {
|
|
789
|
+
id: i.id || "",
|
|
790
|
+
path: i.path || "",
|
|
791
|
+
content: td.message || td.content || "",
|
|
792
|
+
created_at: i.created_at || "",
|
|
793
|
+
role,
|
|
794
|
+
seq: td.seq || 0,
|
|
795
|
+
principal_id: td.principal_id,
|
|
796
|
+
reply_count: td.reply_count || 0,
|
|
797
|
+
parent_message_id: td.parent_message_id,
|
|
798
|
+
intent_card: metadata.intent_card,
|
|
799
|
+
thread_summary: Array.isArray(td.thread_summary) && td.thread_summary.length > 0 ? td.thread_summary : void 0
|
|
800
|
+
};
|
|
801
|
+
}).sort((a, b) => a.seq - b.seq);
|
|
802
|
+
}
|
|
803
|
+
async function fetchChannelMessages(channelPath, parentMessageId) {
|
|
804
|
+
try {
|
|
805
|
+
const params = { channel_path: channelPath };
|
|
806
|
+
if (parentMessageId) params.parent_message_id = parentMessageId;
|
|
807
|
+
const response = await gatewayCall("get-channel-messages", params);
|
|
808
|
+
const raw = response;
|
|
809
|
+
const items = raw.messages || raw.content?.items || raw.items || [];
|
|
810
|
+
return parseChannelMessages(items);
|
|
811
|
+
} catch (err) {
|
|
812
|
+
console.error(
|
|
813
|
+
`[gateway] Failed to fetch channel messages for ${channelPath}:`,
|
|
814
|
+
err
|
|
815
|
+
);
|
|
816
|
+
return [];
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
async function fetchChannelMessagesPage(channelPath, options) {
|
|
820
|
+
try {
|
|
821
|
+
const params = { channel_path: channelPath };
|
|
822
|
+
if (options?.parentMessageId) params.parent_message_id = options.parentMessageId;
|
|
823
|
+
if (options?.limit) params.limit = options.limit;
|
|
824
|
+
if (options?.before_seq != null) params.before_seq = options.before_seq;
|
|
825
|
+
if (options?.after_seq != null) params.after_seq = options.after_seq;
|
|
826
|
+
const response = await gatewayCall("get-channel-messages", params);
|
|
827
|
+
const raw = response;
|
|
828
|
+
const items = raw.messages || [];
|
|
829
|
+
return {
|
|
830
|
+
messages: parseChannelMessages(items),
|
|
831
|
+
mode: raw.mode || "top_level"
|
|
832
|
+
};
|
|
833
|
+
} catch (err) {
|
|
834
|
+
console.error(
|
|
835
|
+
`[gateway] Failed to fetch channel messages page for ${channelPath}:`,
|
|
836
|
+
err
|
|
837
|
+
);
|
|
838
|
+
return { messages: [], mode: "top_level" };
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
function invalidateChildren(path) {
|
|
842
|
+
queryClient.invalidateQueries({ queryKey: vfsKeys.children(path) });
|
|
843
|
+
}
|
|
844
|
+
function invalidateItem(path) {
|
|
845
|
+
queryClient.invalidateQueries({ queryKey: vfsKeys.item(path) });
|
|
846
|
+
}
|
|
847
|
+
function invalidatePathAndParent(path, fallbackParent) {
|
|
848
|
+
invalidateItem(path);
|
|
849
|
+
invalidateChildren(path);
|
|
850
|
+
const segments = path.split("/");
|
|
851
|
+
segments.pop();
|
|
852
|
+
const parentPath = segments.join("/") || fallbackParent;
|
|
853
|
+
if (parentPath) {
|
|
854
|
+
invalidateChildren(parentPath);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
function invalidateSubtree(path) {
|
|
858
|
+
const prefix = path + "/";
|
|
859
|
+
queryClient.invalidateQueries({
|
|
860
|
+
predicate: (query) => {
|
|
861
|
+
const key = query.queryKey;
|
|
862
|
+
if (key[0] !== "vfs") return false;
|
|
863
|
+
return key.some(
|
|
864
|
+
(segment) => typeof segment === "string" && (segment === path || segment.startsWith(prefix))
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
function invalidateAllVfs() {
|
|
870
|
+
queryClient.invalidateQueries({ queryKey: vfsKeys.all() });
|
|
871
|
+
}
|
|
872
|
+
function invalidateTypes(scopeId) {
|
|
873
|
+
queryClient.invalidateQueries({ queryKey: vfsKeys.types(scopeId) });
|
|
874
|
+
}
|
|
875
|
+
function invalidateChannelMessages(channelPath, parentMessageId) {
|
|
876
|
+
if (channelPath) {
|
|
877
|
+
queryClient.invalidateQueries({ queryKey: vfsKeys.channelMessages(channelPath, parentMessageId) });
|
|
878
|
+
} else {
|
|
879
|
+
queryClient.invalidateQueries({ queryKey: vfsKeys.allChannelMessages() });
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
function invalidateOpenEnvelope(path, strategy) {
|
|
883
|
+
if (path && strategy) {
|
|
884
|
+
queryClient.invalidateQueries({ queryKey: vfsKeys.openEnvelope(path, strategy) });
|
|
885
|
+
} else if (path) {
|
|
886
|
+
queryClient.invalidateQueries({
|
|
887
|
+
predicate: (query) => {
|
|
888
|
+
const key = query.queryKey;
|
|
889
|
+
return key[0] === "vfs" && key[2] === "open" && key[3] === path;
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
} else {
|
|
893
|
+
queryClient.invalidateQueries({ queryKey: vfsKeys.allOpenEnvelopes() });
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// src/ws.ts
|
|
898
|
+
var DEBUG = typeof window !== "undefined" && (window.location?.hostname === "localhost" || window.location?.hostname === "127.0.0.1" || window.__FSAOS_WS_DEBUG__);
|
|
899
|
+
function log(...args) {
|
|
900
|
+
if (DEBUG) console.log("[WS]", ...args);
|
|
901
|
+
}
|
|
902
|
+
var ws = null;
|
|
903
|
+
var connectionInFlight = null;
|
|
904
|
+
var reconnectTimer = null;
|
|
905
|
+
var reconnectAttempts = 0;
|
|
906
|
+
var MAX_RECONNECT_DELAY = 3e4;
|
|
907
|
+
var CONNECTION_TIMEOUT_MS = 1e4;
|
|
908
|
+
var intentionalClose = false;
|
|
909
|
+
var PING_INTERVAL_MS = 3e4;
|
|
910
|
+
var pingInterval = null;
|
|
911
|
+
var visibilityListenerAttached = false;
|
|
912
|
+
function startPing() {
|
|
913
|
+
stopPing();
|
|
914
|
+
pingInterval = setInterval(() => {
|
|
915
|
+
sendMessage({ type: "ping" });
|
|
916
|
+
}, PING_INTERVAL_MS);
|
|
917
|
+
}
|
|
918
|
+
function stopPing() {
|
|
919
|
+
if (pingInterval) {
|
|
920
|
+
clearInterval(pingInterval);
|
|
921
|
+
pingInterval = null;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
function handleVisibilityChange() {
|
|
925
|
+
if (typeof document === "undefined") return;
|
|
926
|
+
if (document.hidden) {
|
|
927
|
+
stopPing();
|
|
928
|
+
} else {
|
|
929
|
+
if (hasActiveSubscriptions()) {
|
|
930
|
+
log("Tab visible, reconnecting...");
|
|
931
|
+
ensureConnection();
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
function attachVisibilityListener() {
|
|
936
|
+
if (visibilityListenerAttached) return;
|
|
937
|
+
if (typeof document === "undefined") return;
|
|
938
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
939
|
+
visibilityListenerAttached = true;
|
|
940
|
+
}
|
|
941
|
+
var pathListeners = {};
|
|
942
|
+
var typedListeners = [];
|
|
943
|
+
var activeScopes = /* @__PURE__ */ new Map();
|
|
944
|
+
var activePaths = /* @__PURE__ */ new Map();
|
|
945
|
+
function hasListeners() {
|
|
946
|
+
const hasPath = Object.keys(pathListeners).some(
|
|
947
|
+
(k) => pathListeners[k] && pathListeners[k].length > 0
|
|
948
|
+
);
|
|
949
|
+
return hasPath || typedListeners.length > 0;
|
|
950
|
+
}
|
|
951
|
+
function hasActiveSubscriptions() {
|
|
952
|
+
return activeScopes.size > 0 || activePaths.size > 0;
|
|
953
|
+
}
|
|
954
|
+
async function ensureConnection() {
|
|
955
|
+
if (!hasActiveSubscriptions()) {
|
|
956
|
+
log("ensureConnection: no active subscriptions, skipping");
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
960
|
+
log("ensureConnection: already open");
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
if (connectionInFlight) {
|
|
964
|
+
log("ensureConnection: connection already in flight, waiting...");
|
|
965
|
+
return connectionInFlight;
|
|
966
|
+
}
|
|
967
|
+
if (ws) {
|
|
968
|
+
log("ensureConnection: killing stale WS in state", ws.readyState);
|
|
969
|
+
try {
|
|
970
|
+
ws.close();
|
|
971
|
+
} catch (_e) {
|
|
972
|
+
}
|
|
973
|
+
ws = null;
|
|
974
|
+
}
|
|
975
|
+
intentionalClose = false;
|
|
976
|
+
connectionInFlight = (async () => {
|
|
977
|
+
try {
|
|
978
|
+
const config = typeof window !== "undefined" && window.__FSAOS_CONFIG__ || {};
|
|
979
|
+
const gatewayUrl = GATEWAY_URL;
|
|
980
|
+
const embedToken = config.embedToken || null;
|
|
981
|
+
let token = embedToken;
|
|
982
|
+
if (!token) {
|
|
983
|
+
token = await getAccessToken();
|
|
984
|
+
}
|
|
985
|
+
if (!token) {
|
|
986
|
+
log("ensureConnection: no token available, will retry");
|
|
987
|
+
scheduleReconnect();
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
const wsProtocol = gatewayUrl.startsWith("https") ? "wss" : "ws";
|
|
991
|
+
const wsHost = gatewayUrl.replace(/^https?:\/\//, "");
|
|
992
|
+
let wsUrl = `${wsProtocol}://${wsHost}/ws`;
|
|
993
|
+
wsUrl += `?token=${encodeURIComponent(token)}`;
|
|
994
|
+
log("ensureConnection: opening WS to", wsUrl.substring(0, 50) + "...");
|
|
995
|
+
ws = new WebSocket(wsUrl);
|
|
996
|
+
await new Promise((resolve, reject) => {
|
|
997
|
+
if (!ws) return reject(new Error("WebSocket not created"));
|
|
998
|
+
const timeout = setTimeout(() => {
|
|
999
|
+
log("ensureConnection: TIMEOUT after", CONNECTION_TIMEOUT_MS, "ms");
|
|
1000
|
+
if (ws && ws.readyState !== WebSocket.OPEN) {
|
|
1001
|
+
try {
|
|
1002
|
+
ws.close();
|
|
1003
|
+
} catch (_e) {
|
|
1004
|
+
}
|
|
1005
|
+
ws = null;
|
|
1006
|
+
}
|
|
1007
|
+
reject(new Error("WebSocket connection timeout"));
|
|
1008
|
+
}, CONNECTION_TIMEOUT_MS);
|
|
1009
|
+
ws.onopen = () => {
|
|
1010
|
+
clearTimeout(timeout);
|
|
1011
|
+
log("ensureConnection: CONNECTED");
|
|
1012
|
+
reconnectAttempts = 0;
|
|
1013
|
+
resubscribeAll();
|
|
1014
|
+
if (typeof document === "undefined" || !document.hidden) {
|
|
1015
|
+
startPing();
|
|
1016
|
+
}
|
|
1017
|
+
attachVisibilityListener();
|
|
1018
|
+
resolve();
|
|
1019
|
+
};
|
|
1020
|
+
ws.onmessage = (event) => {
|
|
1021
|
+
handleMessage(event.data);
|
|
1022
|
+
};
|
|
1023
|
+
ws.onerror = () => {
|
|
1024
|
+
log("ensureConnection: WS error event");
|
|
1025
|
+
};
|
|
1026
|
+
ws.onclose = (event) => {
|
|
1027
|
+
clearTimeout(timeout);
|
|
1028
|
+
log("ensureConnection: WS closed, code:", event.code, "reason:", event.reason);
|
|
1029
|
+
ws = null;
|
|
1030
|
+
stopPing();
|
|
1031
|
+
if (!intentionalClose && hasActiveSubscriptions()) {
|
|
1032
|
+
scheduleReconnect();
|
|
1033
|
+
}
|
|
1034
|
+
reject(new Error(`WebSocket closed: ${event.code}`));
|
|
1035
|
+
};
|
|
1036
|
+
});
|
|
1037
|
+
} catch (e) {
|
|
1038
|
+
log("ensureConnection: failed -", e?.message || e);
|
|
1039
|
+
} finally {
|
|
1040
|
+
connectionInFlight = null;
|
|
1041
|
+
}
|
|
1042
|
+
})();
|
|
1043
|
+
return connectionInFlight;
|
|
1044
|
+
}
|
|
1045
|
+
function scheduleReconnect() {
|
|
1046
|
+
if (reconnectTimer) return;
|
|
1047
|
+
if (typeof document !== "undefined" && document.hidden) return;
|
|
1048
|
+
const delay = Math.min(
|
|
1049
|
+
1e3 * Math.pow(2, reconnectAttempts) + Math.random() * 1e3,
|
|
1050
|
+
MAX_RECONNECT_DELAY
|
|
1051
|
+
);
|
|
1052
|
+
reconnectAttempts++;
|
|
1053
|
+
log("scheduleReconnect: attempt", reconnectAttempts, "in", Math.round(delay), "ms");
|
|
1054
|
+
reconnectTimer = setTimeout(() => {
|
|
1055
|
+
reconnectTimer = null;
|
|
1056
|
+
if (hasActiveSubscriptions()) {
|
|
1057
|
+
ensureConnection();
|
|
1058
|
+
}
|
|
1059
|
+
}, delay);
|
|
1060
|
+
}
|
|
1061
|
+
function resubscribeAll() {
|
|
1062
|
+
for (const sub of activeScopes.values()) {
|
|
1063
|
+
log("resubscribeAll: scope", sub.scope_id);
|
|
1064
|
+
sendMessage({ type: "subscribe", scope_id: sub.scope_id, event_types: sub.event_types });
|
|
1065
|
+
}
|
|
1066
|
+
for (const sub of activePaths.values()) {
|
|
1067
|
+
log("resubscribeAll: path", sub.path);
|
|
1068
|
+
sendMessage({ type: "subscribe_path", path: sub.path, event_types: sub.event_types });
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
function sendMessage(msg) {
|
|
1072
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
1073
|
+
ws.send(JSON.stringify(msg));
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
function handleMessage(raw) {
|
|
1077
|
+
try {
|
|
1078
|
+
const msg = JSON.parse(raw);
|
|
1079
|
+
if (msg.type === "subscribed" || msg.type === "subscribed_path" || msg.type === "unsubscribed" || msg.type === "unsubscribed_path" || msg.type === "pong") {
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
if (msg.type === "error") {
|
|
1083
|
+
console.warn("[WS] Server error:", msg.message);
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
if (msg.type === "event" || msg.event_type) {
|
|
1087
|
+
const eventType = msg.event_type || "message";
|
|
1088
|
+
const data = msg.data || msg;
|
|
1089
|
+
if (data.path && pathListeners[data.path]) {
|
|
1090
|
+
pathListeners[data.path].forEach((cb) => cb(data));
|
|
1091
|
+
}
|
|
1092
|
+
if (pathListeners["*"]) {
|
|
1093
|
+
pathListeners["*"].forEach((cb) => cb(data));
|
|
1094
|
+
}
|
|
1095
|
+
for (const entry of typedListeners) {
|
|
1096
|
+
if (entry.eventType !== "*" && entry.eventType !== eventType) continue;
|
|
1097
|
+
if (entry.filter && !entry.filter(data)) continue;
|
|
1098
|
+
entry.callback(data);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
} catch (_e) {
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
function maybeDisconnect() {
|
|
1105
|
+
if (!hasListeners() && activeScopes.size === 0 && activePaths.size === 0 && ws) {
|
|
1106
|
+
intentionalClose = true;
|
|
1107
|
+
stopPing();
|
|
1108
|
+
ws.close();
|
|
1109
|
+
ws = null;
|
|
1110
|
+
intentionalClose = false;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
function subscribeToPath(path, callback) {
|
|
1114
|
+
if (!pathListeners[path]) {
|
|
1115
|
+
pathListeners[path] = [];
|
|
1116
|
+
}
|
|
1117
|
+
pathListeners[path].push(callback);
|
|
1118
|
+
if (!activePaths.has(path)) {
|
|
1119
|
+
activePaths.set(path, { path, event_types: ["token_stream"] });
|
|
1120
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
1121
|
+
sendMessage({ type: "subscribe_path", path, event_types: ["token_stream"] });
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
ensureConnection();
|
|
1125
|
+
return () => {
|
|
1126
|
+
const listeners = pathListeners[path];
|
|
1127
|
+
if (listeners) {
|
|
1128
|
+
const idx = listeners.indexOf(callback);
|
|
1129
|
+
if (idx !== -1) listeners.splice(idx, 1);
|
|
1130
|
+
if (listeners.length === 0) {
|
|
1131
|
+
delete pathListeners[path];
|
|
1132
|
+
activePaths.delete(path);
|
|
1133
|
+
sendMessage({ type: "unsubscribe_path", path });
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
maybeDisconnect();
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
function subscribeToEvents(eventType, callback, filter) {
|
|
1140
|
+
const types = Array.isArray(eventType) ? eventType : [eventType];
|
|
1141
|
+
const entries = types.map((t) => ({ eventType: t, callback, filter }));
|
|
1142
|
+
typedListeners.push(...entries);
|
|
1143
|
+
if (hasActiveSubscriptions()) {
|
|
1144
|
+
ensureConnection();
|
|
1145
|
+
}
|
|
1146
|
+
return () => {
|
|
1147
|
+
for (const entry of entries) {
|
|
1148
|
+
const idx = typedListeners.indexOf(entry);
|
|
1149
|
+
if (idx !== -1) typedListeners.splice(idx, 1);
|
|
1150
|
+
}
|
|
1151
|
+
maybeDisconnect();
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
function subscribeToScope(scopeId, eventTypes = ["vfs_change", "ccm_change"]) {
|
|
1155
|
+
log("subscribeToScope:", scopeId, eventTypes);
|
|
1156
|
+
activeScopes.set(scopeId, { scope_id: scopeId, event_types: eventTypes });
|
|
1157
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
1158
|
+
sendMessage({ type: "subscribe", scope_id: scopeId, event_types: eventTypes });
|
|
1159
|
+
}
|
|
1160
|
+
ensureConnection();
|
|
1161
|
+
return () => {
|
|
1162
|
+
activeScopes.delete(scopeId);
|
|
1163
|
+
sendMessage({ type: "unsubscribe", scope_id: scopeId });
|
|
1164
|
+
maybeDisconnect();
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
function emitEvent(scopeId, eventType, payload = {}) {
|
|
1168
|
+
sendMessage({ type: "emit", scope_id: scopeId, event_type: eventType, payload });
|
|
1169
|
+
}
|
|
1170
|
+
function disconnectSSE() {
|
|
1171
|
+
log("disconnectSSE: tearing down");
|
|
1172
|
+
intentionalClose = true;
|
|
1173
|
+
stopPing();
|
|
1174
|
+
if (ws) {
|
|
1175
|
+
ws.close();
|
|
1176
|
+
ws = null;
|
|
1177
|
+
}
|
|
1178
|
+
if (reconnectTimer) {
|
|
1179
|
+
clearTimeout(reconnectTimer);
|
|
1180
|
+
reconnectTimer = null;
|
|
1181
|
+
}
|
|
1182
|
+
connectionInFlight = null;
|
|
1183
|
+
intentionalClose = false;
|
|
1184
|
+
reconnectAttempts = 0;
|
|
1185
|
+
for (const key of Object.keys(pathListeners)) {
|
|
1186
|
+
delete pathListeners[key];
|
|
1187
|
+
}
|
|
1188
|
+
typedListeners.length = 0;
|
|
1189
|
+
activeScopes.clear();
|
|
1190
|
+
activePaths.clear();
|
|
1191
|
+
}
|
|
1192
|
+
function ping() {
|
|
1193
|
+
sendMessage({ type: "ping" });
|
|
1194
|
+
}
|
|
1195
|
+
registerCleanup(disconnectSSE);
|
|
1196
|
+
|
|
1197
|
+
// src/vfs-realtime-ws.ts
|
|
1198
|
+
var activeScopeSubs = /* @__PURE__ */ new Map();
|
|
1199
|
+
var unsubEvents = null;
|
|
1200
|
+
var debounceTimers = /* @__PURE__ */ new Map();
|
|
1201
|
+
var DEBOUNCE_MS = 250;
|
|
1202
|
+
function initVfsRealtimeWs(accountId) {
|
|
1203
|
+
disposeVfsRealtimeWs();
|
|
1204
|
+
mountRealtimeScope(accountId);
|
|
1205
|
+
unsubEvents = subscribeToEvents(
|
|
1206
|
+
"vfs_change",
|
|
1207
|
+
(data) => handleVfsChange(data)
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
function mountRealtimeScope(scopeId) {
|
|
1211
|
+
if (activeScopeSubs.has(scopeId)) return;
|
|
1212
|
+
const unsub = subscribeToScope(scopeId, ["vfs_change", "ccm_change"]);
|
|
1213
|
+
activeScopeSubs.set(scopeId, unsub);
|
|
1214
|
+
}
|
|
1215
|
+
function unmountRealtimeScope(scopeId) {
|
|
1216
|
+
const unsub = activeScopeSubs.get(scopeId);
|
|
1217
|
+
if (unsub) {
|
|
1218
|
+
unsub();
|
|
1219
|
+
activeScopeSubs.delete(scopeId);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
function disposeVfsRealtimeWs() {
|
|
1223
|
+
for (const [, unsub] of activeScopeSubs) {
|
|
1224
|
+
unsub();
|
|
1225
|
+
}
|
|
1226
|
+
activeScopeSubs.clear();
|
|
1227
|
+
if (unsubEvents) {
|
|
1228
|
+
unsubEvents();
|
|
1229
|
+
unsubEvents = null;
|
|
1230
|
+
}
|
|
1231
|
+
debounceTimers.forEach((timer) => clearTimeout(timer));
|
|
1232
|
+
debounceTimers.clear();
|
|
1233
|
+
}
|
|
1234
|
+
registerCleanup(disposeVfsRealtimeWs);
|
|
1235
|
+
function handleVfsChange(data) {
|
|
1236
|
+
const path = data.path;
|
|
1237
|
+
const action = data.action;
|
|
1238
|
+
if (!path) return;
|
|
1239
|
+
const segments = path.split("/");
|
|
1240
|
+
segments.pop();
|
|
1241
|
+
const parentPath = segments.join("/") || "/root";
|
|
1242
|
+
const existing = debounceTimers.get(parentPath);
|
|
1243
|
+
if (existing) clearTimeout(existing);
|
|
1244
|
+
const timer = setTimeout(() => {
|
|
1245
|
+
debounceTimers.delete(parentPath);
|
|
1246
|
+
applyInvalidation(action, path, data, parentPath);
|
|
1247
|
+
}, DEBOUNCE_MS);
|
|
1248
|
+
debounceTimers.set(parentPath, timer);
|
|
1249
|
+
}
|
|
1250
|
+
function applyInvalidation(action, path, data, parentPath) {
|
|
1251
|
+
const upperAction = (action || "").toUpperCase();
|
|
1252
|
+
if (upperAction === "DELETE") {
|
|
1253
|
+
queryClient.removeQueries({ queryKey: vfsKeys.item(path) });
|
|
1254
|
+
queryClient.removeQueries({ queryKey: vfsKeys.children(path) });
|
|
1255
|
+
queryClient.removeQueries({ queryKey: vfsKeys.tree(path) });
|
|
1256
|
+
} else if (data.item_id) {
|
|
1257
|
+
try {
|
|
1258
|
+
const record = {
|
|
1259
|
+
id: data.item_id,
|
|
1260
|
+
path: data.path,
|
|
1261
|
+
name: data.name,
|
|
1262
|
+
item_type: data.item_type,
|
|
1263
|
+
parent_id: data.parent_id,
|
|
1264
|
+
scope_item_id: data.scope_item_id
|
|
1265
|
+
};
|
|
1266
|
+
const normalized = normalizeItem(record);
|
|
1267
|
+
if (normalized && normalized.id) {
|
|
1268
|
+
queryClient.setQueryData(vfsKeys.item(path), normalized);
|
|
1269
|
+
}
|
|
1270
|
+
} catch (_e) {
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
queryClient.invalidateQueries({ queryKey: vfsKeys.children(parentPath) });
|
|
1274
|
+
queryClient.invalidateQueries({
|
|
1275
|
+
predicate: (query) => {
|
|
1276
|
+
const key = query.queryKey;
|
|
1277
|
+
if (key[0] !== "vfs" || key[1] !== "tree") return false;
|
|
1278
|
+
const treePath = key[2];
|
|
1279
|
+
return path.startsWith(treePath + "/") || path === treePath;
|
|
1280
|
+
}
|
|
1281
|
+
});
|
|
1282
|
+
if (data.item_id) {
|
|
1283
|
+
queryClient.invalidateQueries({ queryKey: vfsKeys.itemById(data.item_id) });
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
function readItem(path) {
|
|
1287
|
+
return gatewayCall("read", { path });
|
|
1288
|
+
}
|
|
1289
|
+
function listChildren(path, options) {
|
|
1290
|
+
return gatewayCall("list", { path, ...options });
|
|
1291
|
+
}
|
|
1292
|
+
function createItem(params) {
|
|
1293
|
+
return gatewayCall("create", params);
|
|
1294
|
+
}
|
|
1295
|
+
function updateItem(params) {
|
|
1296
|
+
return gatewayCall("update", params);
|
|
1297
|
+
}
|
|
1298
|
+
function pushChanges(params) {
|
|
1299
|
+
return gatewayCall("create", params);
|
|
1300
|
+
}
|
|
1301
|
+
function callTool(toolName, params) {
|
|
1302
|
+
const config = typeof window !== "undefined" && window.__FSAOS_CONFIG__ || {};
|
|
1303
|
+
const scopePath = config.scopePath || "";
|
|
1304
|
+
return gatewayCall("tools/call", {
|
|
1305
|
+
name: toolName,
|
|
1306
|
+
instance_path: scopePath,
|
|
1307
|
+
...params || {}
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
function signal(targetId, eventName, payload) {
|
|
1311
|
+
return gatewayCall("signal", { target_id: targetId, event_name: eventName, payload: payload || {} });
|
|
1312
|
+
}
|
|
1313
|
+
function getAssetUrl(path) {
|
|
1314
|
+
const config = typeof window !== "undefined" && window.__FSAOS_CONFIG__ || {};
|
|
1315
|
+
return (config.edgeBaseUrl || "") + path;
|
|
1316
|
+
}
|
|
1317
|
+
var auth = {
|
|
1318
|
+
signIn: (opts) => {
|
|
1319
|
+
if (opts.provider) {
|
|
1320
|
+
return supabase.auth.signInWithOAuth({
|
|
1321
|
+
provider: opts.provider,
|
|
1322
|
+
options: { redirectTo: opts.redirectTo || window.location.href }
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
return supabase.auth.signInWithPassword({
|
|
1326
|
+
email: opts.email,
|
|
1327
|
+
password: opts.password
|
|
1328
|
+
});
|
|
1329
|
+
},
|
|
1330
|
+
signUp: (opts) => {
|
|
1331
|
+
return supabase.auth.signUp({
|
|
1332
|
+
email: opts.email,
|
|
1333
|
+
password: opts.password,
|
|
1334
|
+
options: { data: opts.metadata || {} }
|
|
1335
|
+
});
|
|
1336
|
+
},
|
|
1337
|
+
signOut: async () => {
|
|
1338
|
+
await supabase.auth.signOut();
|
|
1339
|
+
resetAllSdkState();
|
|
1340
|
+
queryClient.clear();
|
|
1341
|
+
},
|
|
1342
|
+
getSession: () => supabase.auth.getSession().then((r) => r.data.session),
|
|
1343
|
+
getUser: () => supabase.auth.getUser().then((r) => r.data.user),
|
|
1344
|
+
onAuthStateChange: (callback) => {
|
|
1345
|
+
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
|
1346
|
+
(event, session) => {
|
|
1347
|
+
callback({ event, session, user: session?.user ?? null });
|
|
1348
|
+
}
|
|
1349
|
+
);
|
|
1350
|
+
return () => subscription?.unsubscribe();
|
|
1351
|
+
},
|
|
1352
|
+
resetPassword: (email) => supabase.auth.resetPasswordForEmail(email),
|
|
1353
|
+
signInWithMagicLink: (email, redirectTo) => {
|
|
1354
|
+
return supabase.auth.signInWithOtp({
|
|
1355
|
+
email,
|
|
1356
|
+
options: { emailRedirectTo: redirectTo || window.location.href }
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
function mount() {
|
|
1361
|
+
const rootEl = document.getElementById("root");
|
|
1362
|
+
if (!rootEl) return;
|
|
1363
|
+
try {
|
|
1364
|
+
const comp = window.__FSAOS_COMPONENT__;
|
|
1365
|
+
if (!comp) {
|
|
1366
|
+
rootEl.innerHTML = '<div style="padding:2rem;color:#ef4444;font-family:system-ui"><h2>Component Error</h2><pre>No component found. The bundle may have failed to load.</pre></div>';
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
const RootComponent = comp.default || comp;
|
|
1370
|
+
const itemData = window.__FSAOS_ITEM_DATA__ || null;
|
|
1371
|
+
const props = itemData ? { item: itemData, ...itemData } : {};
|
|
1372
|
+
const root = client.createRoot(rootEl);
|
|
1373
|
+
root.render(
|
|
1374
|
+
React.createElement(
|
|
1375
|
+
reactQuery.QueryClientProvider,
|
|
1376
|
+
{ client: queryClient },
|
|
1377
|
+
React.createElement(RootComponent, props)
|
|
1378
|
+
)
|
|
1379
|
+
);
|
|
1380
|
+
} catch (err) {
|
|
1381
|
+
console.error("[FSAOS] Failed to mount root component:", err);
|
|
1382
|
+
rootEl.innerHTML = '<div style="padding:2rem;color:#ef4444;font-family:system-ui"><h2>Component Error</h2><pre>' + (err.message || err) + "</pre></div>";
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
function setupRequireShim() {
|
|
1386
|
+
const R = window.React;
|
|
1387
|
+
const RD = window.ReactDOM;
|
|
1388
|
+
const gateway = window.__FSAOS_GATEWAY__;
|
|
1389
|
+
const ui = window.__FSAOS_UI__;
|
|
1390
|
+
if (!gateway) {
|
|
1391
|
+
console.error("[FSAOS] Cannot set up require shim: __FSAOS_GATEWAY__ not found");
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
const modules = {
|
|
1395
|
+
"react": R,
|
|
1396
|
+
"react-dom": RD,
|
|
1397
|
+
"react-dom/client": RD,
|
|
1398
|
+
"react/jsx-runtime": {
|
|
1399
|
+
jsx: R?.createElement,
|
|
1400
|
+
jsxs: R?.createElement,
|
|
1401
|
+
Fragment: R?.Fragment
|
|
1402
|
+
},
|
|
1403
|
+
"@fsaos/react": gateway,
|
|
1404
|
+
"@fsaos/gateway": gateway,
|
|
1405
|
+
"@fsaos/ui": ui || {}
|
|
1406
|
+
};
|
|
1407
|
+
window.require = function fsaosRequire(name) {
|
|
1408
|
+
if (modules[name]) return modules[name];
|
|
1409
|
+
throw new Error("[FSAOS] Module not found: " + name);
|
|
1410
|
+
};
|
|
1411
|
+
window.__FSAOS_MOUNT__ = gateway.mount;
|
|
1412
|
+
}
|
|
1413
|
+
function useScopeKey() {
|
|
1414
|
+
return React.useSyncExternalStore(subscribeScope, getScopeVersion, getScopeVersion);
|
|
1415
|
+
}
|
|
1416
|
+
function useScopeReady() {
|
|
1417
|
+
useScopeKey();
|
|
1418
|
+
return isScopeReady();
|
|
1419
|
+
}
|
|
1420
|
+
function useItem(path) {
|
|
1421
|
+
useScopeKey();
|
|
1422
|
+
return reactQuery.useQuery({
|
|
1423
|
+
queryKey: vfsKeys.item(path),
|
|
1424
|
+
queryFn: () => fetchVfsItem(path),
|
|
1425
|
+
enabled: !!path
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
function useItemById(id) {
|
|
1429
|
+
useScopeKey();
|
|
1430
|
+
return reactQuery.useQuery({
|
|
1431
|
+
queryKey: vfsKeys.itemById(id),
|
|
1432
|
+
queryFn: () => fetchVfsItemById(id),
|
|
1433
|
+
enabled: !!id
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
function useList(path) {
|
|
1437
|
+
useScopeKey();
|
|
1438
|
+
return reactQuery.useQuery({
|
|
1439
|
+
queryKey: vfsKeys.children(path),
|
|
1440
|
+
queryFn: () => fetchVfsChildren(path),
|
|
1441
|
+
enabled: !!path
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
function useChildren(path) {
|
|
1445
|
+
const query = useList(path);
|
|
1446
|
+
return {
|
|
1447
|
+
...query,
|
|
1448
|
+
children: query.data ?? []
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
function useTree(path, depth = 1) {
|
|
1452
|
+
useScopeKey();
|
|
1453
|
+
return reactQuery.useQuery({
|
|
1454
|
+
queryKey: vfsKeys.tree(path, depth),
|
|
1455
|
+
queryFn: () => fetchVfsTree(path, depth),
|
|
1456
|
+
enabled: !!path
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
function useSearch(query, itemTypes) {
|
|
1460
|
+
useScopeKey();
|
|
1461
|
+
return reactQuery.useQuery({
|
|
1462
|
+
queryKey: vfsKeys.search(query, itemTypes),
|
|
1463
|
+
queryFn: async () => {
|
|
1464
|
+
const params = { query };
|
|
1465
|
+
if (itemTypes?.length) params.item_types = itemTypes;
|
|
1466
|
+
const response = await gatewayCall("search", params);
|
|
1467
|
+
return (response.content?.items || response.items || []).map(normalizeItem);
|
|
1468
|
+
},
|
|
1469
|
+
enabled: !!query && query.length > 0
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
function useItems(filter) {
|
|
1473
|
+
useScopeKey();
|
|
1474
|
+
const stableFilter = React.useMemo(() => filter, [JSON.stringify(filter)]);
|
|
1475
|
+
return reactQuery.useQuery({
|
|
1476
|
+
queryKey: vfsKeys.items(
|
|
1477
|
+
stableFilter?.type ?? "",
|
|
1478
|
+
stableFilter
|
|
1479
|
+
),
|
|
1480
|
+
queryFn: () => fetchItems(stableFilter),
|
|
1481
|
+
enabled: !!stableFilter?.type,
|
|
1482
|
+
select: (result) => result.items
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
function useInfiniteItems(filter) {
|
|
1486
|
+
useScopeKey();
|
|
1487
|
+
const PAGE_SIZE = filter?.limit ?? 20;
|
|
1488
|
+
const stableFilter = React.useMemo(() => filter, [JSON.stringify(filter)]);
|
|
1489
|
+
const query = reactQuery.useInfiniteQuery({
|
|
1490
|
+
queryKey: ["vfs", getScope() ?? "__unscoped__", "infinite-items", stableFilter?.type ?? "", JSON.stringify(stableFilter ?? {})],
|
|
1491
|
+
queryFn: async ({ pageParam = 0 }) => {
|
|
1492
|
+
return fetchItems({
|
|
1493
|
+
...stableFilter,
|
|
1494
|
+
limit: PAGE_SIZE,
|
|
1495
|
+
offset: pageParam
|
|
1496
|
+
});
|
|
1497
|
+
},
|
|
1498
|
+
initialPageParam: 0,
|
|
1499
|
+
getNextPageParam: (lastPage, _allPages, lastPageParam) => {
|
|
1500
|
+
if (lastPage.items.length < PAGE_SIZE) return void 0;
|
|
1501
|
+
return lastPageParam + lastPage.items.length;
|
|
1502
|
+
},
|
|
1503
|
+
enabled: !!stableFilter?.type
|
|
1504
|
+
});
|
|
1505
|
+
const allItems = React.useMemo(() => {
|
|
1506
|
+
if (!query.data?.pages) return [];
|
|
1507
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1508
|
+
const flat = [];
|
|
1509
|
+
for (const page of query.data.pages) {
|
|
1510
|
+
for (const item of page.items) {
|
|
1511
|
+
if (item.id && !seen.has(item.id)) {
|
|
1512
|
+
seen.add(item.id);
|
|
1513
|
+
flat.push(item);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
return flat;
|
|
1518
|
+
}, [query.data?.pages]);
|
|
1519
|
+
return {
|
|
1520
|
+
allItems,
|
|
1521
|
+
fetchNextPage: query.fetchNextPage,
|
|
1522
|
+
isFetchingNextPage: query.isFetchingNextPage,
|
|
1523
|
+
isExhausted: !query.hasNextPage,
|
|
1524
|
+
isLoading: query.isLoading,
|
|
1525
|
+
isFetching: query.isFetching,
|
|
1526
|
+
error: query.error,
|
|
1527
|
+
totalHint: query.data?.pages?.[0]?.total_hint
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
function useInfiniteChildren(path) {
|
|
1531
|
+
useScopeKey();
|
|
1532
|
+
const PAGE_SIZE = 20;
|
|
1533
|
+
const query = reactQuery.useInfiniteQuery({
|
|
1534
|
+
queryKey: ["vfs", getScope() ?? "__unscoped__", "infinite-children", path ?? ""],
|
|
1535
|
+
queryFn: async ({ pageParam = 0 }) => {
|
|
1536
|
+
return fetchVfsChildrenPage(path, PAGE_SIZE, pageParam);
|
|
1537
|
+
},
|
|
1538
|
+
initialPageParam: 0,
|
|
1539
|
+
getNextPageParam: (lastPage, _allPages, lastPageParam) => {
|
|
1540
|
+
if (lastPage.items.length < PAGE_SIZE) return void 0;
|
|
1541
|
+
return lastPageParam + lastPage.items.length;
|
|
1542
|
+
},
|
|
1543
|
+
enabled: !!path
|
|
1544
|
+
});
|
|
1545
|
+
const allItems = React.useMemo(() => {
|
|
1546
|
+
if (!query.data?.pages) return [];
|
|
1547
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1548
|
+
const flat = [];
|
|
1549
|
+
for (const page of query.data.pages) {
|
|
1550
|
+
for (const item of page.items) {
|
|
1551
|
+
if (item.id && !seen.has(item.id)) {
|
|
1552
|
+
seen.add(item.id);
|
|
1553
|
+
flat.push(item);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
return flat;
|
|
1558
|
+
}, [query.data?.pages]);
|
|
1559
|
+
return {
|
|
1560
|
+
allItems,
|
|
1561
|
+
fetchNextPage: query.fetchNextPage,
|
|
1562
|
+
isFetchingNextPage: query.isFetchingNextPage,
|
|
1563
|
+
isExhausted: !query.hasNextPage,
|
|
1564
|
+
isLoading: query.isLoading,
|
|
1565
|
+
isFetching: query.isFetching,
|
|
1566
|
+
error: query.error,
|
|
1567
|
+
totalHint: query.data?.pages?.[0]?.total_hint
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
function useEdges(itemId) {
|
|
1571
|
+
useScopeKey();
|
|
1572
|
+
return reactQuery.useQuery({
|
|
1573
|
+
queryKey: vfsKeys.edges(itemId),
|
|
1574
|
+
queryFn: () => fetchEdgesForItem(itemId),
|
|
1575
|
+
enabled: !!itemId
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
function useTypes(scopeId) {
|
|
1579
|
+
useScopeKey();
|
|
1580
|
+
return reactQuery.useQuery({
|
|
1581
|
+
queryKey: vfsKeys.types(scopeId),
|
|
1582
|
+
queryFn: () => fetchTypeDefinitions(scopeId)
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
function useType(typeKey, scopeId) {
|
|
1586
|
+
const typesQuery = useTypes(scopeId);
|
|
1587
|
+
return {
|
|
1588
|
+
...typesQuery,
|
|
1589
|
+
data: typesQuery.data?.get(typeKey) ?? void 0
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
function useOpen(path, options) {
|
|
1593
|
+
useScopeKey();
|
|
1594
|
+
const stableArgs = React.useMemo(
|
|
1595
|
+
() => options?.arguments,
|
|
1596
|
+
[JSON.stringify(options?.arguments)]
|
|
1597
|
+
);
|
|
1598
|
+
const stableMode = options?.mode;
|
|
1599
|
+
const stableStrategy = options?.strategy;
|
|
1600
|
+
return reactQuery.useQuery({
|
|
1601
|
+
queryKey: vfsKeys.openEnvelope(path, stableStrategy),
|
|
1602
|
+
queryFn: () => fetchOpenEnvelope(path, {
|
|
1603
|
+
mode: stableMode,
|
|
1604
|
+
strategy: stableStrategy,
|
|
1605
|
+
arguments: stableArgs
|
|
1606
|
+
}),
|
|
1607
|
+
enabled: !!path
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
var EDGE_TYPE_LABELS = {
|
|
1611
|
+
related_to: "Related To",
|
|
1612
|
+
depends_on: "Depends On",
|
|
1613
|
+
blocks: "Blocks",
|
|
1614
|
+
parent_of: "Parent Of",
|
|
1615
|
+
child_of: "Child Of",
|
|
1616
|
+
references: "References",
|
|
1617
|
+
implements: "Implements",
|
|
1618
|
+
extends: "Extends",
|
|
1619
|
+
contains: "Contains",
|
|
1620
|
+
belongs_to: "Belongs To",
|
|
1621
|
+
created_by: "Created By",
|
|
1622
|
+
assigned_to: "Assigned To",
|
|
1623
|
+
tagged_with: "Tagged With",
|
|
1624
|
+
linked_to: "Linked To"
|
|
1625
|
+
};
|
|
1626
|
+
function useTypeHelpers(scopeId) {
|
|
1627
|
+
const typesQuery = useTypes(scopeId);
|
|
1628
|
+
const types = typesQuery.data ?? /* @__PURE__ */ new Map();
|
|
1629
|
+
return React.useMemo(() => {
|
|
1630
|
+
const getTypeDefinition = (typeKey) => types.get(typeKey);
|
|
1631
|
+
const canHaveChildren = (item) => {
|
|
1632
|
+
const td = types.get(item.item_type);
|
|
1633
|
+
return td?.is_container ?? false;
|
|
1634
|
+
};
|
|
1635
|
+
const isScope = (item) => {
|
|
1636
|
+
const td = types.get(item.item_type);
|
|
1637
|
+
return td?.is_scope ?? false;
|
|
1638
|
+
};
|
|
1639
|
+
const isScopeType = (typeKey) => {
|
|
1640
|
+
const td = types.get(typeKey);
|
|
1641
|
+
return td?.is_scope ?? false;
|
|
1642
|
+
};
|
|
1643
|
+
const isContainerType = (typeKey) => {
|
|
1644
|
+
const td = types.get(typeKey);
|
|
1645
|
+
return td?.is_container ?? false;
|
|
1646
|
+
};
|
|
1647
|
+
const getTypeColor = (typeKey) => {
|
|
1648
|
+
const td = types.get(typeKey);
|
|
1649
|
+
return td?.color ?? "gray";
|
|
1650
|
+
};
|
|
1651
|
+
const getTypeIcon = (typeKey) => {
|
|
1652
|
+
const td = types.get(typeKey);
|
|
1653
|
+
return td?.icon ?? "File";
|
|
1654
|
+
};
|
|
1655
|
+
const getTypeDisplayName = (typeKey) => {
|
|
1656
|
+
const td = types.get(typeKey);
|
|
1657
|
+
return td?.display_name ?? typeKey;
|
|
1658
|
+
};
|
|
1659
|
+
const getKnownEdgeTypes = () => Object.entries(EDGE_TYPE_LABELS).map(([key, label]) => ({ key, label }));
|
|
1660
|
+
const getEdgeTypeLabel = (edgeType) => EDGE_TYPE_LABELS[edgeType] ?? edgeType.replace(/_/g, " ");
|
|
1661
|
+
return {
|
|
1662
|
+
canHaveChildren,
|
|
1663
|
+
isScope,
|
|
1664
|
+
isScopeType,
|
|
1665
|
+
isContainerType,
|
|
1666
|
+
getTypeDefinition,
|
|
1667
|
+
getTypeColor,
|
|
1668
|
+
getTypeIcon,
|
|
1669
|
+
getTypeDisplayName,
|
|
1670
|
+
getKnownEdgeTypes,
|
|
1671
|
+
getEdgeTypeLabel,
|
|
1672
|
+
allTypes: types,
|
|
1673
|
+
loading: typesQuery.isLoading
|
|
1674
|
+
};
|
|
1675
|
+
}, [types, typesQuery.isLoading]);
|
|
1676
|
+
}
|
|
1677
|
+
function useMemberFocus(scopeId, itemTypes, limit) {
|
|
1678
|
+
useScopeKey();
|
|
1679
|
+
return reactQuery.useQuery({
|
|
1680
|
+
queryKey: vfsKeys.memberFocus(scopeId),
|
|
1681
|
+
queryFn: () => fetchMemberFocus(scopeId, itemTypes, limit),
|
|
1682
|
+
enabled: !!scopeId
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
function useItemHistory(itemId, limit = 50, offset = 0) {
|
|
1686
|
+
return reactQuery.useQuery({
|
|
1687
|
+
queryKey: vfsKeys.itemHistory(itemId, String(limit), String(offset)),
|
|
1688
|
+
queryFn: () => fetchItemHistory(itemId, limit, offset),
|
|
1689
|
+
enabled: !!itemId
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
function useRecentActivity(scopePath, limit = 50, offset = 0) {
|
|
1693
|
+
useScopeKey();
|
|
1694
|
+
return reactQuery.useQuery({
|
|
1695
|
+
queryKey: vfsKeys.recentActivity(scopePath, String(limit), String(offset)),
|
|
1696
|
+
queryFn: () => fetchRecentActivity(scopePath, limit, offset),
|
|
1697
|
+
enabled: !!scopePath
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
function useScope() {
|
|
1701
|
+
const [data, setData] = React.useState(null);
|
|
1702
|
+
const [loading, setLoading] = React.useState(true);
|
|
1703
|
+
const [error, setError] = React.useState(null);
|
|
1704
|
+
React.useEffect(() => {
|
|
1705
|
+
let cancelled = false;
|
|
1706
|
+
initSession().then((entry) => {
|
|
1707
|
+
if (!cancelled) {
|
|
1708
|
+
const config = typeof window !== "undefined" && window.__FSAOS_CONFIG__ || {};
|
|
1709
|
+
setData({
|
|
1710
|
+
path: entry.scope_path,
|
|
1711
|
+
scope_id: entry.scope_id,
|
|
1712
|
+
fractal_id: entry.fractal_id,
|
|
1713
|
+
instance_name: entry.instance_name,
|
|
1714
|
+
display_name: entry.display_name,
|
|
1715
|
+
componentPath: config.componentPath || "",
|
|
1716
|
+
isEmbed: !!config.embedToken
|
|
1717
|
+
});
|
|
1718
|
+
setLoading(false);
|
|
1719
|
+
}
|
|
1720
|
+
}).catch((err) => {
|
|
1721
|
+
if (!cancelled) {
|
|
1722
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
1723
|
+
setLoading(false);
|
|
1724
|
+
}
|
|
1725
|
+
});
|
|
1726
|
+
return () => {
|
|
1727
|
+
cancelled = true;
|
|
1728
|
+
};
|
|
1729
|
+
}, []);
|
|
1730
|
+
return { data, loading, error };
|
|
1731
|
+
}
|
|
1732
|
+
function useAccounts() {
|
|
1733
|
+
const { user } = useAuth();
|
|
1734
|
+
const [currentAccount, setCurrentAccount] = React.useState(null);
|
|
1735
|
+
const [mountedPath, setMountedPath] = React.useState(null);
|
|
1736
|
+
const autoSelectedRef = React.useRef(false);
|
|
1737
|
+
const query = reactQuery.useQuery({
|
|
1738
|
+
queryKey: vfsKeys.memberships(user?.id ?? "anonymous"),
|
|
1739
|
+
queryFn: async () => {
|
|
1740
|
+
const result = await gatewayCall("list-memberships", {});
|
|
1741
|
+
const memberships = result.memberships || [];
|
|
1742
|
+
return memberships.map((m) => ({
|
|
1743
|
+
accountId: m.account_id,
|
|
1744
|
+
path: m.account_path,
|
|
1745
|
+
name: m.account_name,
|
|
1746
|
+
role: m.role || "member",
|
|
1747
|
+
typeData: m.account_type_data || {},
|
|
1748
|
+
joinedAt: m.joined_at,
|
|
1749
|
+
via: m.via,
|
|
1750
|
+
placement: m.placement,
|
|
1751
|
+
subordinateTo: m.subordinate_to ?? null,
|
|
1752
|
+
cascadeAnchorId: m.cascade_anchor_id ?? null,
|
|
1753
|
+
isAccount: m.is_account ?? void 0,
|
|
1754
|
+
isSpace: m.is_space ?? void 0,
|
|
1755
|
+
isOwner: m.is_owner ?? void 0,
|
|
1756
|
+
billingResponsible: m.billing_responsible ?? void 0,
|
|
1757
|
+
grantedVia: m.granted_via ?? null,
|
|
1758
|
+
grantedUnderAccountId: m.granted_under_account_id ?? null,
|
|
1759
|
+
personalSpaceId: m.personal_space_id ?? null,
|
|
1760
|
+
personalSpacePath: m.personal_space_path ?? null
|
|
1761
|
+
}));
|
|
1762
|
+
},
|
|
1763
|
+
enabled: !!user?.id,
|
|
1764
|
+
staleTime: 1e3 * 60 * 2
|
|
1765
|
+
});
|
|
1766
|
+
const allMemberships = query.data ?? [];
|
|
1767
|
+
const switcherAccounts = React.useMemo(() => {
|
|
1768
|
+
return allMemberships.filter((a) => {
|
|
1769
|
+
if (a.placement !== void 0) {
|
|
1770
|
+
return a.placement === "switcher_entry" && a.isAccount === true;
|
|
1771
|
+
}
|
|
1772
|
+
return true;
|
|
1773
|
+
});
|
|
1774
|
+
}, [allMemberships]);
|
|
1775
|
+
React.useEffect(() => {
|
|
1776
|
+
if (!user?.id) {
|
|
1777
|
+
autoSelectedRef.current = false;
|
|
1778
|
+
setCurrentAccount(null);
|
|
1779
|
+
setMountedPath(null);
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
if (autoSelectedRef.current) return;
|
|
1783
|
+
if (switcherAccounts.length === 0) return;
|
|
1784
|
+
if (switcherAccounts.length === 1) {
|
|
1785
|
+
setCurrentAccount(switcherAccounts[0]);
|
|
1786
|
+
setScope(switcherAccounts[0].path);
|
|
1787
|
+
initVfsRealtimeWs(switcherAccounts[0].accountId);
|
|
1788
|
+
autoSelectedRef.current = true;
|
|
1789
|
+
} else if (switcherAccounts.length > 1) {
|
|
1790
|
+
autoSelectedRef.current = true;
|
|
1791
|
+
}
|
|
1792
|
+
}, [user?.id, switcherAccounts]);
|
|
1793
|
+
const switchAccount = React.useCallback((accountPath) => {
|
|
1794
|
+
const account = switcherAccounts.find((a) => a.path === accountPath);
|
|
1795
|
+
if (account) {
|
|
1796
|
+
setCurrentAccount(account);
|
|
1797
|
+
setScope(account.path);
|
|
1798
|
+
initVfsRealtimeWs(account.accountId);
|
|
1799
|
+
setMountedPath(null);
|
|
1800
|
+
}
|
|
1801
|
+
}, [switcherAccounts]);
|
|
1802
|
+
const mountScope = React.useCallback((scopePath) => {
|
|
1803
|
+
if (mountedPath) {
|
|
1804
|
+
unmountRealtimeScope(mountedPath);
|
|
1805
|
+
}
|
|
1806
|
+
setMountedPath(scopePath);
|
|
1807
|
+
mountRealtimeScope(scopePath);
|
|
1808
|
+
}, [mountedPath]);
|
|
1809
|
+
return {
|
|
1810
|
+
accounts: switcherAccounts,
|
|
1811
|
+
allMemberships,
|
|
1812
|
+
currentAccount,
|
|
1813
|
+
mountedScope: mountedPath,
|
|
1814
|
+
switchAccount,
|
|
1815
|
+
mountScope,
|
|
1816
|
+
loading: query.isLoading,
|
|
1817
|
+
error: query.error
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
function useAuth() {
|
|
1821
|
+
const [user, setUser] = React.useState(null);
|
|
1822
|
+
const [session, setSession] = React.useState(null);
|
|
1823
|
+
const [loading, setLoading] = React.useState(true);
|
|
1824
|
+
const subscriptionRef = React.useRef(null);
|
|
1825
|
+
React.useEffect(() => {
|
|
1826
|
+
supabase.auth.getSession().then(({ data, error }) => {
|
|
1827
|
+
if (error) {
|
|
1828
|
+
console.warn("[useAuth] getSession error, clearing stale session:", error.message);
|
|
1829
|
+
supabase.auth.signOut().catch(() => {
|
|
1830
|
+
});
|
|
1831
|
+
setSession(null);
|
|
1832
|
+
setUser(null);
|
|
1833
|
+
setLoading(false);
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
setSession(data?.session ?? null);
|
|
1837
|
+
setUser(data?.session?.user ?? null);
|
|
1838
|
+
setLoading(false);
|
|
1839
|
+
}).catch((err) => {
|
|
1840
|
+
console.warn("[useAuth] getSession threw, clearing stale session:", err?.message);
|
|
1841
|
+
supabase.auth.signOut().catch(() => {
|
|
1842
|
+
});
|
|
1843
|
+
setSession(null);
|
|
1844
|
+
setUser(null);
|
|
1845
|
+
setLoading(false);
|
|
1846
|
+
});
|
|
1847
|
+
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
|
1848
|
+
(event, newSession) => {
|
|
1849
|
+
if (event === "TOKEN_REFRESHED" && !newSession) {
|
|
1850
|
+
console.warn("[useAuth] TOKEN_REFRESHED with null session \u2014 signing out");
|
|
1851
|
+
supabase.auth.signOut().catch(() => {
|
|
1852
|
+
});
|
|
1853
|
+
setSession(null);
|
|
1854
|
+
setUser(null);
|
|
1855
|
+
setLoading(false);
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1858
|
+
setSession(newSession);
|
|
1859
|
+
setUser(newSession?.user ?? null);
|
|
1860
|
+
setLoading(false);
|
|
1861
|
+
}
|
|
1862
|
+
);
|
|
1863
|
+
subscriptionRef.current = subscription;
|
|
1864
|
+
return () => {
|
|
1865
|
+
subscription?.unsubscribe();
|
|
1866
|
+
};
|
|
1867
|
+
}, []);
|
|
1868
|
+
const signIn = React.useCallback(async (params) => {
|
|
1869
|
+
if (params.provider) {
|
|
1870
|
+
return supabase.auth.signInWithOAuth({
|
|
1871
|
+
provider: params.provider,
|
|
1872
|
+
options: params.redirectTo ? { redirectTo: params.redirectTo } : { redirectTo: window.location.href }
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
return supabase.auth.signInWithPassword({
|
|
1876
|
+
email: params.email,
|
|
1877
|
+
password: params.password
|
|
1878
|
+
});
|
|
1879
|
+
}, []);
|
|
1880
|
+
const signUp = React.useCallback(async (params) => {
|
|
1881
|
+
return supabase.auth.signUp({
|
|
1882
|
+
email: params.email,
|
|
1883
|
+
password: params.password,
|
|
1884
|
+
options: { data: params.metadata || {} }
|
|
1885
|
+
});
|
|
1886
|
+
}, []);
|
|
1887
|
+
const signOut = React.useCallback(async () => {
|
|
1888
|
+
await supabase.auth.signOut();
|
|
1889
|
+
resetAllSdkState();
|
|
1890
|
+
queryClient.clear();
|
|
1891
|
+
}, []);
|
|
1892
|
+
const resetPassword = React.useCallback(async (email) => {
|
|
1893
|
+
const { error } = await supabase.auth.resetPasswordForEmail(email);
|
|
1894
|
+
if (error) throw error;
|
|
1895
|
+
}, []);
|
|
1896
|
+
const signInWithMagicLink = React.useCallback(async (email, redirectTo) => {
|
|
1897
|
+
const { error } = await supabase.auth.signInWithOtp({
|
|
1898
|
+
email,
|
|
1899
|
+
options: redirectTo ? { emailRedirectTo: redirectTo } : { emailRedirectTo: window.location.href }
|
|
1900
|
+
});
|
|
1901
|
+
if (error) throw error;
|
|
1902
|
+
}, []);
|
|
1903
|
+
return { user, session, loading, signIn, signUp, signOut, resetPassword, signInWithMagicLink };
|
|
1904
|
+
}
|
|
1905
|
+
function useAsset(idOrPath) {
|
|
1906
|
+
const [data, setData] = React.useState(null);
|
|
1907
|
+
const [loading, setLoading] = React.useState(false);
|
|
1908
|
+
const [error, setError] = React.useState(null);
|
|
1909
|
+
React.useEffect(() => {
|
|
1910
|
+
if (!idOrPath) {
|
|
1911
|
+
setData(null);
|
|
1912
|
+
setLoading(false);
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(idOrPath);
|
|
1916
|
+
if (isUUID) {
|
|
1917
|
+
setLoading(true);
|
|
1918
|
+
gatewayCall("resolve-asset", { asset_id: idOrPath }).then((result) => {
|
|
1919
|
+
if (result.success && result.public_url) {
|
|
1920
|
+
setData({
|
|
1921
|
+
url: result.public_url,
|
|
1922
|
+
id: result.asset_id,
|
|
1923
|
+
name: result.name,
|
|
1924
|
+
mimeType: result.mime_type,
|
|
1925
|
+
width: result.width,
|
|
1926
|
+
height: result.height,
|
|
1927
|
+
sizeBytes: result.size_bytes
|
|
1928
|
+
});
|
|
1929
|
+
} else {
|
|
1930
|
+
setError(new Error(result.error || "Asset not found"));
|
|
1931
|
+
}
|
|
1932
|
+
setLoading(false);
|
|
1933
|
+
}).catch((e) => {
|
|
1934
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
1935
|
+
setLoading(false);
|
|
1936
|
+
});
|
|
1937
|
+
} else {
|
|
1938
|
+
const config = typeof window !== "undefined" && window.__FSAOS_CONFIG__ || {};
|
|
1939
|
+
const edgeBaseUrl = config.edgeBaseUrl || "";
|
|
1940
|
+
setData({ url: edgeBaseUrl + idOrPath });
|
|
1941
|
+
setLoading(false);
|
|
1942
|
+
}
|
|
1943
|
+
}, [idOrPath]);
|
|
1944
|
+
return { data, loading, error };
|
|
1945
|
+
}
|
|
1946
|
+
var componentCache = {};
|
|
1947
|
+
var componentCssCache = {};
|
|
1948
|
+
registerCleanup(() => {
|
|
1949
|
+
for (const key of Object.keys(componentCache)) {
|
|
1950
|
+
delete componentCache[key];
|
|
1951
|
+
}
|
|
1952
|
+
});
|
|
1953
|
+
var _sdkExports = null;
|
|
1954
|
+
function __registerSdkExports(exports) {
|
|
1955
|
+
_sdkExports = exports;
|
|
1956
|
+
if (typeof window !== "undefined" && !window.__FSAOS_GATEWAY__) {
|
|
1957
|
+
window.__FSAOS_GATEWAY__ = exports;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
function sdkRequireShim(name) {
|
|
1961
|
+
switch (name) {
|
|
1962
|
+
case "react":
|
|
1963
|
+
return typeof window !== "undefined" && window.React || React__default.default;
|
|
1964
|
+
case "react-dom":
|
|
1965
|
+
case "react-dom/client":
|
|
1966
|
+
return typeof window !== "undefined" && window.ReactDOM || ReactDOM__namespace;
|
|
1967
|
+
case "react/jsx-runtime":
|
|
1968
|
+
case "react/jsx-dev-runtime":
|
|
1969
|
+
return typeof window !== "undefined" && window.React ? { jsx: window.React.createElement, jsxs: window.React.createElement, Fragment: window.React.Fragment } : JsxRuntime__namespace;
|
|
1970
|
+
case "@fsaos/gateway":
|
|
1971
|
+
case "@fsaos/react":
|
|
1972
|
+
return typeof window !== "undefined" && window.__FSAOS_GATEWAY__ || _sdkExports || {};
|
|
1973
|
+
case "@fsaos/ui":
|
|
1974
|
+
return typeof window !== "undefined" && window.__FSAOS_UI__ || {};
|
|
1975
|
+
case "@fsaos/theme":
|
|
1976
|
+
return {};
|
|
1977
|
+
default:
|
|
1978
|
+
console.warn(`[useComponent] Unknown module requested: ${name}`);
|
|
1979
|
+
return {};
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
function scopeComponentCss(cssText, scopeClass) {
|
|
1983
|
+
return cssText.replace(
|
|
1984
|
+
/^(\s*)(:root|body)(\s*[{,])/gm,
|
|
1985
|
+
(_match, ws2, _sel, rest) => `${ws2}.${scopeClass}${rest}`
|
|
1986
|
+
);
|
|
1987
|
+
}
|
|
1988
|
+
async function loadComponentCss(cssUrl, scopeId) {
|
|
1989
|
+
if (componentCssCache[cssUrl]) return;
|
|
1990
|
+
try {
|
|
1991
|
+
const response = await fetch(cssUrl);
|
|
1992
|
+
if (!response.ok) return;
|
|
1993
|
+
let cssText = await response.text();
|
|
1994
|
+
if (!cssText.trim()) return;
|
|
1995
|
+
if (cssText.includes("No CSS bundle for this component")) return;
|
|
1996
|
+
if (scopeId && !cssText.includes(`.fsaos-c-${scopeId}`)) ;
|
|
1997
|
+
const style = document.createElement("style");
|
|
1998
|
+
style.setAttribute("data-fsaos-component-css", cssUrl);
|
|
1999
|
+
if (scopeId) ;
|
|
2000
|
+
style.textContent = cssText;
|
|
2001
|
+
document.head.appendChild(style);
|
|
2002
|
+
componentCssCache[cssUrl] = style;
|
|
2003
|
+
} catch {
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
function evaluateBundle(jsCode) {
|
|
2007
|
+
let registered = null;
|
|
2008
|
+
const previousRegister = window.__FSAOS_REGISTER__;
|
|
2009
|
+
window.__FSAOS_REGISTER__ = (component) => {
|
|
2010
|
+
registered = component;
|
|
2011
|
+
};
|
|
2012
|
+
try {
|
|
2013
|
+
const wrappedCode = [
|
|
2014
|
+
"var require = arguments[0];",
|
|
2015
|
+
jsCode,
|
|
2016
|
+
"return window.__FSAOS_COMPONENT__;"
|
|
2017
|
+
].join("\n");
|
|
2018
|
+
const fn = new Function(wrappedCode);
|
|
2019
|
+
const fromIife = fn(sdkRequireShim);
|
|
2020
|
+
const exported = registered ?? fromIife;
|
|
2021
|
+
if (!exported) return null;
|
|
2022
|
+
return exported.default || exported;
|
|
2023
|
+
} finally {
|
|
2024
|
+
if (previousRegister !== void 0) {
|
|
2025
|
+
window.__FSAOS_REGISTER__ = previousRegister;
|
|
2026
|
+
} else {
|
|
2027
|
+
delete window.__FSAOS_REGISTER__;
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
function useComponent(path) {
|
|
2032
|
+
const [Component, setComponent] = React.useState(null);
|
|
2033
|
+
const [loading, setLoading] = React.useState(true);
|
|
2034
|
+
const [error, setError] = React.useState(null);
|
|
2035
|
+
React.useEffect(() => {
|
|
2036
|
+
if (!path) return;
|
|
2037
|
+
let cancelled = false;
|
|
2038
|
+
setLoading(true);
|
|
2039
|
+
setError(null);
|
|
2040
|
+
const config = typeof window !== "undefined" && window.__FSAOS_CONFIG__ || {};
|
|
2041
|
+
const edgeBaseUrl = config.edgeBaseUrl || "";
|
|
2042
|
+
(async () => {
|
|
2043
|
+
try {
|
|
2044
|
+
let version = 1;
|
|
2045
|
+
let cacheKey = path + "@v1";
|
|
2046
|
+
let bundleUrl = edgeBaseUrl + path + "/__bundle.js";
|
|
2047
|
+
try {
|
|
2048
|
+
const metaResponse = await fetch(edgeBaseUrl + path + "/__meta", { cache: "no-store" });
|
|
2049
|
+
if (metaResponse.ok) {
|
|
2050
|
+
const meta = await metaResponse.json();
|
|
2051
|
+
version = meta.version || 1;
|
|
2052
|
+
cacheKey = path + "@v" + version;
|
|
2053
|
+
bundleUrl = edgeBaseUrl + path + "/__bundle.v" + version + ".js";
|
|
2054
|
+
}
|
|
2055
|
+
} catch {
|
|
2056
|
+
bundleUrl = edgeBaseUrl + path + "/__bundle.js?v=" + Date.now();
|
|
2057
|
+
}
|
|
2058
|
+
if (cancelled) return;
|
|
2059
|
+
if (componentCache[cacheKey]) {
|
|
2060
|
+
setComponent(() => componentCache[cacheKey]);
|
|
2061
|
+
setLoading(false);
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
const jsResponse = await fetch(bundleUrl);
|
|
2065
|
+
if (!jsResponse.ok) {
|
|
2066
|
+
throw new Error(
|
|
2067
|
+
`Failed to fetch bundle: ${jsResponse.status} ${jsResponse.statusText} (${bundleUrl})`
|
|
2068
|
+
);
|
|
2069
|
+
}
|
|
2070
|
+
const jsCode = await jsResponse.text();
|
|
2071
|
+
if (cancelled) return;
|
|
2072
|
+
const cssUrl = bundleUrl.replace(/__bundle(\.v\d+)?\.js(\?.*)?$/, "__bundle$1.css");
|
|
2073
|
+
loadComponentCss(cssUrl).catch(() => {
|
|
2074
|
+
});
|
|
2075
|
+
const resolved = evaluateBundle(jsCode);
|
|
2076
|
+
if (!resolved) {
|
|
2077
|
+
throw new Error(
|
|
2078
|
+
"Bundle did not export a component. Expected window.__FSAOS_COMPONENT__ (FSAOS build) or a __FSAOS_REGISTER__(Component) call."
|
|
2079
|
+
);
|
|
2080
|
+
}
|
|
2081
|
+
if (typeof resolved !== "function") {
|
|
2082
|
+
throw new Error(`Bundle export is not a React component (got ${typeof resolved})`);
|
|
2083
|
+
}
|
|
2084
|
+
componentCache[cacheKey] = resolved;
|
|
2085
|
+
if (!cancelled) {
|
|
2086
|
+
setComponent(() => resolved);
|
|
2087
|
+
setLoading(false);
|
|
2088
|
+
}
|
|
2089
|
+
} catch (err) {
|
|
2090
|
+
if (!cancelled) {
|
|
2091
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
2092
|
+
setLoading(false);
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
})();
|
|
2096
|
+
return () => {
|
|
2097
|
+
cancelled = true;
|
|
2098
|
+
};
|
|
2099
|
+
}, [path]);
|
|
2100
|
+
return { Component, loading, error };
|
|
2101
|
+
}
|
|
2102
|
+
var THEME_PROPERTIES = [
|
|
2103
|
+
"--color-primary",
|
|
2104
|
+
"--color-secondary",
|
|
2105
|
+
"--color-background",
|
|
2106
|
+
"--color-surface",
|
|
2107
|
+
"--color-text",
|
|
2108
|
+
"--color-text-secondary",
|
|
2109
|
+
"--color-border",
|
|
2110
|
+
"--color-accent",
|
|
2111
|
+
"--color-success",
|
|
2112
|
+
"--color-warning",
|
|
2113
|
+
"--color-error",
|
|
2114
|
+
"--font-family-heading",
|
|
2115
|
+
"--font-family-body",
|
|
2116
|
+
"--border-radius",
|
|
2117
|
+
"--spacing-unit"
|
|
2118
|
+
];
|
|
2119
|
+
function useTheme() {
|
|
2120
|
+
const themeData = React.useMemo(() => {
|
|
2121
|
+
if (typeof document === "undefined") return { tokens: {} };
|
|
2122
|
+
const vars = {};
|
|
2123
|
+
const style = getComputedStyle(document.documentElement);
|
|
2124
|
+
THEME_PROPERTIES.forEach((p) => {
|
|
2125
|
+
vars[p] = style.getPropertyValue(p).trim();
|
|
2126
|
+
});
|
|
2127
|
+
return { tokens: vars };
|
|
2128
|
+
}, []);
|
|
2129
|
+
return { data: themeData, loading: false, error: null };
|
|
2130
|
+
}
|
|
2131
|
+
function usePermission(_action, _path) {
|
|
2132
|
+
return { data: { allowed: true }, loading: false, error: null };
|
|
2133
|
+
}
|
|
2134
|
+
function usePermissions(_path, actions) {
|
|
2135
|
+
const perms = {};
|
|
2136
|
+
(actions || []).forEach((a) => {
|
|
2137
|
+
perms[a] = true;
|
|
2138
|
+
});
|
|
2139
|
+
return { data: perms, loading: false, error: null };
|
|
2140
|
+
}
|
|
2141
|
+
var _principalIdCache = null;
|
|
2142
|
+
var _principalIdPromise = null;
|
|
2143
|
+
registerCleanup(() => {
|
|
2144
|
+
_principalIdCache = null;
|
|
2145
|
+
_principalIdPromise = null;
|
|
2146
|
+
});
|
|
2147
|
+
async function resolvePrincipalId(authUserId) {
|
|
2148
|
+
let uid = authUserId;
|
|
2149
|
+
if (!uid) {
|
|
2150
|
+
const { data } = await supabase.auth.getSession();
|
|
2151
|
+
uid = data?.session?.user?.id;
|
|
2152
|
+
}
|
|
2153
|
+
if (!uid) return null;
|
|
2154
|
+
if (_principalIdCache && _principalIdCache.authUserId === uid) {
|
|
2155
|
+
return _principalIdCache.principalId;
|
|
2156
|
+
}
|
|
2157
|
+
if (_principalIdPromise) return _principalIdPromise;
|
|
2158
|
+
_principalIdPromise = (async () => {
|
|
2159
|
+
try {
|
|
2160
|
+
const { data, error } = await supabase.from("os_principals").select("id").eq("auth_user_id", uid).eq("principal_type", "user").maybeSingle();
|
|
2161
|
+
if (error) {
|
|
2162
|
+
console.warn("[SDK] Could not resolve principal_id:", error.message);
|
|
2163
|
+
return null;
|
|
2164
|
+
}
|
|
2165
|
+
const principalId = data?.id ?? null;
|
|
2166
|
+
_principalIdCache = { authUserId: uid, principalId };
|
|
2167
|
+
return principalId;
|
|
2168
|
+
} finally {
|
|
2169
|
+
_principalIdPromise = null;
|
|
2170
|
+
}
|
|
2171
|
+
})();
|
|
2172
|
+
return _principalIdPromise;
|
|
2173
|
+
}
|
|
2174
|
+
function usePrincipal() {
|
|
2175
|
+
const authResult = useAuth();
|
|
2176
|
+
const user = authResult.user;
|
|
2177
|
+
const [principalId, setPrincipalId] = React.useState(
|
|
2178
|
+
(_principalIdCache?.authUserId === user?.id ? _principalIdCache?.principalId : null) ?? null
|
|
2179
|
+
);
|
|
2180
|
+
const prevUserIdRef = React.useRef(null);
|
|
2181
|
+
React.useEffect(() => {
|
|
2182
|
+
if (!user?.id) {
|
|
2183
|
+
setPrincipalId(null);
|
|
2184
|
+
prevUserIdRef.current = null;
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
if (prevUserIdRef.current === user.id && principalId !== null) return;
|
|
2188
|
+
prevUserIdRef.current = user.id;
|
|
2189
|
+
resolvePrincipalId(user.id).then((id) => {
|
|
2190
|
+
setPrincipalId(id);
|
|
2191
|
+
});
|
|
2192
|
+
}, [user?.id]);
|
|
2193
|
+
if (authResult.loading) {
|
|
2194
|
+
return { data: null, loading: true, error: null };
|
|
2195
|
+
}
|
|
2196
|
+
if (!user) {
|
|
2197
|
+
return {
|
|
2198
|
+
data: { id: "anonymous", principalId: null, role: "viewer", authenticated: false },
|
|
2199
|
+
loading: false,
|
|
2200
|
+
error: null
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
return {
|
|
2204
|
+
data: {
|
|
2205
|
+
id: user.id,
|
|
2206
|
+
principalId,
|
|
2207
|
+
email: user.email,
|
|
2208
|
+
role: user.role || user.app_metadata?.role || "user",
|
|
2209
|
+
authenticated: true,
|
|
2210
|
+
metadata: user.user_metadata || {}
|
|
2211
|
+
},
|
|
2212
|
+
loading: false,
|
|
2213
|
+
error: null
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
function useTool() {
|
|
2217
|
+
return { data: null, loading: false, error: null };
|
|
2218
|
+
}
|
|
2219
|
+
function ComponentRenderer(props) {
|
|
2220
|
+
const { path, ...rest } = props;
|
|
2221
|
+
const result = useComponent(path);
|
|
2222
|
+
const Comp = result.Component;
|
|
2223
|
+
const { loading, error } = result;
|
|
2224
|
+
if (loading) return React.createElement("div", { className: "fsaos-loading" }, "");
|
|
2225
|
+
if (error) return React.createElement("div", { className: "fsaos-error" }, "Failed to load component");
|
|
2226
|
+
if (!Comp) return null;
|
|
2227
|
+
return React.createElement(Comp, rest);
|
|
2228
|
+
}
|
|
2229
|
+
function KernelProvider(props) {
|
|
2230
|
+
return props.children;
|
|
2231
|
+
}
|
|
2232
|
+
function useChannelMessages(options) {
|
|
2233
|
+
const scopeKey = useScopeKey();
|
|
2234
|
+
const {
|
|
2235
|
+
channelPath,
|
|
2236
|
+
parentMessageId,
|
|
2237
|
+
realtime = true,
|
|
2238
|
+
enabled: userEnabled
|
|
2239
|
+
} = options;
|
|
2240
|
+
const isEnabled = userEnabled !== void 0 ? userEnabled && !!channelPath : !!channelPath;
|
|
2241
|
+
const stableKey = React.useMemo(
|
|
2242
|
+
() => vfsKeys.channelMessages(channelPath ?? "", parentMessageId),
|
|
2243
|
+
[channelPath, parentMessageId, scopeKey]
|
|
2244
|
+
);
|
|
2245
|
+
const qc = reactQuery.useQueryClient();
|
|
2246
|
+
const debounceRef = React.useRef(null);
|
|
2247
|
+
const query = reactQuery.useQuery({
|
|
2248
|
+
queryKey: stableKey,
|
|
2249
|
+
queryFn: () => fetchChannelMessages(channelPath, parentMessageId),
|
|
2250
|
+
enabled: isEnabled
|
|
2251
|
+
});
|
|
2252
|
+
React.useEffect(() => {
|
|
2253
|
+
if (!realtime || !isEnabled) return;
|
|
2254
|
+
const unsub = subscribeToEvents(
|
|
2255
|
+
"vfs_change",
|
|
2256
|
+
() => {
|
|
2257
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
2258
|
+
debounceRef.current = setTimeout(() => {
|
|
2259
|
+
qc.invalidateQueries({ queryKey: stableKey });
|
|
2260
|
+
}, 500);
|
|
2261
|
+
},
|
|
2262
|
+
(data) => {
|
|
2263
|
+
return data.item_type === "message" && typeof data.path === "string" && data.path.startsWith(channelPath);
|
|
2264
|
+
}
|
|
2265
|
+
);
|
|
2266
|
+
return () => {
|
|
2267
|
+
unsub();
|
|
2268
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
2269
|
+
};
|
|
2270
|
+
}, [realtime, isEnabled, channelPath, stableKey, qc]);
|
|
2271
|
+
const messages = query.data ?? [];
|
|
2272
|
+
return {
|
|
2273
|
+
messages,
|
|
2274
|
+
threads: React.useMemo(() => messages.filter((m) => m.reply_count > 0), [messages]),
|
|
2275
|
+
isLoading: query.isLoading,
|
|
2276
|
+
isFetching: query.isFetching,
|
|
2277
|
+
error: query.error,
|
|
2278
|
+
refetch: query.refetch
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
function useInfiniteChannelMessages(options) {
|
|
2282
|
+
useScopeKey();
|
|
2283
|
+
const { channelPath, parentMessageId, limit = 50, realtime = true, enabled = true } = options;
|
|
2284
|
+
const isThread = !!parentMessageId;
|
|
2285
|
+
const queryEnabled = enabled && !!channelPath;
|
|
2286
|
+
const query = reactQuery.useInfiniteQuery({
|
|
2287
|
+
queryKey: [...vfsKeys.channelMessages(channelPath || "", parentMessageId), "infinite", limit],
|
|
2288
|
+
queryFn: async ({ pageParam }) => {
|
|
2289
|
+
return fetchChannelMessagesPage(channelPath, {
|
|
2290
|
+
parentMessageId,
|
|
2291
|
+
limit,
|
|
2292
|
+
before_seq: isThread ? void 0 : pageParam,
|
|
2293
|
+
after_seq: isThread ? pageParam : void 0
|
|
2294
|
+
});
|
|
2295
|
+
},
|
|
2296
|
+
initialPageParam: void 0,
|
|
2297
|
+
getNextPageParam: (lastPage) => {
|
|
2298
|
+
if (!isThread) return void 0;
|
|
2299
|
+
if (lastPage.messages.length < limit) return void 0;
|
|
2300
|
+
const maxSeq = lastPage.messages[lastPage.messages.length - 1]?.seq;
|
|
2301
|
+
return maxSeq;
|
|
2302
|
+
},
|
|
2303
|
+
getPreviousPageParam: (firstPage) => {
|
|
2304
|
+
if (isThread) return void 0;
|
|
2305
|
+
if (firstPage.messages.length < limit) return void 0;
|
|
2306
|
+
const minSeq = firstPage.messages[0]?.seq;
|
|
2307
|
+
return minSeq;
|
|
2308
|
+
},
|
|
2309
|
+
enabled: queryEnabled
|
|
2310
|
+
});
|
|
2311
|
+
const messages = React.useMemo(() => {
|
|
2312
|
+
if (!query.data?.pages) return [];
|
|
2313
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2314
|
+
const flat = [];
|
|
2315
|
+
for (const page of query.data.pages) {
|
|
2316
|
+
for (const msg of page.messages) {
|
|
2317
|
+
if (!seen.has(msg.id)) {
|
|
2318
|
+
seen.add(msg.id);
|
|
2319
|
+
flat.push(msg);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
return flat.sort((a, b) => a.seq - b.seq);
|
|
2324
|
+
}, [query.data?.pages]);
|
|
2325
|
+
React.useEffect(() => {
|
|
2326
|
+
if (!realtime || !channelPath || !queryEnabled) return;
|
|
2327
|
+
const unsub = subscribeToEvents(channelPath, (event) => {
|
|
2328
|
+
if (event.event_type === "vfs_change") {
|
|
2329
|
+
query.refetch();
|
|
2330
|
+
}
|
|
2331
|
+
});
|
|
2332
|
+
return unsub;
|
|
2333
|
+
}, [realtime, channelPath, queryEnabled]);
|
|
2334
|
+
return {
|
|
2335
|
+
messages,
|
|
2336
|
+
threads: messages.filter((m) => m.reply_count > 0),
|
|
2337
|
+
isLoading: query.isLoading,
|
|
2338
|
+
isFetching: query.isFetching,
|
|
2339
|
+
error: query.error,
|
|
2340
|
+
refetch: query.refetch,
|
|
2341
|
+
// Backward pagination (top-level: load older)
|
|
2342
|
+
hasPreviousPage: query.hasPreviousPage ?? false,
|
|
2343
|
+
fetchPreviousPage: query.fetchPreviousPage,
|
|
2344
|
+
isFetchingPreviousPage: query.isFetchingPreviousPage ?? false,
|
|
2345
|
+
// Forward pagination (thread: load newer)
|
|
2346
|
+
hasNextPage: query.hasNextPage ?? false,
|
|
2347
|
+
fetchNextPage: query.fetchNextPage,
|
|
2348
|
+
isFetchingNextPage: query.isFetchingNextPage ?? false
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
function normalizeAiMode(value) {
|
|
2352
|
+
if (value === "on" || value === "auto") return "on";
|
|
2353
|
+
return "off";
|
|
2354
|
+
}
|
|
2355
|
+
function normalizeChannel(raw) {
|
|
2356
|
+
const td = raw.type_data || {};
|
|
2357
|
+
return {
|
|
2358
|
+
id: raw.channel_id || "",
|
|
2359
|
+
path: raw.channel_path || "",
|
|
2360
|
+
name: raw.channel_name || "",
|
|
2361
|
+
displayName: raw.display_name || raw.channel_name || "",
|
|
2362
|
+
aiMode: normalizeAiMode(raw.ai_mode),
|
|
2363
|
+
isPrivate: raw.is_private || false,
|
|
2364
|
+
isDm: raw.is_dm || false,
|
|
2365
|
+
messageCount: raw.message_count || 0,
|
|
2366
|
+
updatedAt: raw.updated_at || "",
|
|
2367
|
+
chatScopePath: raw.chat_scope_path || "",
|
|
2368
|
+
contextId: raw.context_id || "",
|
|
2369
|
+
contextPath: raw.context_path || "",
|
|
2370
|
+
contextType: raw.context_type || "",
|
|
2371
|
+
contextName: raw.context_name || "",
|
|
2372
|
+
contextDisplayName: raw.context_display_name || "",
|
|
2373
|
+
contextPrincipalId: raw.context_principal_id,
|
|
2374
|
+
accountId: raw.account_id || "",
|
|
2375
|
+
accountPath: raw.account_path || "",
|
|
2376
|
+
accountDisplayName: raw.account_display_name || "",
|
|
2377
|
+
memberRole: raw.member_role || "member",
|
|
2378
|
+
memberPath: raw.member_path || "",
|
|
2379
|
+
modePath: raw.mode_path || "",
|
|
2380
|
+
autoPilot: raw.auto_pilot || td.auto_pilot || false
|
|
2381
|
+
};
|
|
2382
|
+
}
|
|
2383
|
+
function useAllChannels(options = {}) {
|
|
2384
|
+
const scopeKey = useScopeKey();
|
|
2385
|
+
const { realtime = true, enabled = true } = options;
|
|
2386
|
+
const qc = reactQuery.useQueryClient();
|
|
2387
|
+
const debounceRef = React.useRef(null);
|
|
2388
|
+
const stableKey = React.useMemo(() => vfsKeys.channels("all"), [scopeKey]);
|
|
2389
|
+
const query = reactQuery.useQuery({
|
|
2390
|
+
queryKey: stableKey,
|
|
2391
|
+
queryFn: async () => {
|
|
2392
|
+
const result = await gatewayCall("list-channels", {});
|
|
2393
|
+
const raw = result.channels || [];
|
|
2394
|
+
return raw.map(normalizeChannel);
|
|
2395
|
+
},
|
|
2396
|
+
enabled
|
|
2397
|
+
});
|
|
2398
|
+
React.useEffect(() => {
|
|
2399
|
+
if (!realtime || !enabled) return;
|
|
2400
|
+
const unsub = subscribeToEvents(
|
|
2401
|
+
"vfs_change",
|
|
2402
|
+
() => {
|
|
2403
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
2404
|
+
debounceRef.current = setTimeout(() => {
|
|
2405
|
+
qc.invalidateQueries({ queryKey: stableKey });
|
|
2406
|
+
}, 1e3);
|
|
2407
|
+
},
|
|
2408
|
+
(data) => data.item_type === "channel" || data.item_type === "message"
|
|
2409
|
+
);
|
|
2410
|
+
const unsub2 = subscribeToEvents(
|
|
2411
|
+
"ccm_change",
|
|
2412
|
+
() => {
|
|
2413
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
2414
|
+
debounceRef.current = setTimeout(() => {
|
|
2415
|
+
qc.invalidateQueries({ queryKey: stableKey });
|
|
2416
|
+
}, 1e3);
|
|
2417
|
+
},
|
|
2418
|
+
(data) => data.item_type === "channel" || data.item_type === "message"
|
|
2419
|
+
);
|
|
2420
|
+
return () => {
|
|
2421
|
+
unsub();
|
|
2422
|
+
unsub2();
|
|
2423
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
2424
|
+
};
|
|
2425
|
+
}, [realtime, enabled, stableKey, qc]);
|
|
2426
|
+
const channels = query.data ?? [];
|
|
2427
|
+
return {
|
|
2428
|
+
channels,
|
|
2429
|
+
isLoading: query.isLoading,
|
|
2430
|
+
isFetching: query.isFetching,
|
|
2431
|
+
error: query.error,
|
|
2432
|
+
refetch: query.refetch
|
|
2433
|
+
};
|
|
2434
|
+
}
|
|
2435
|
+
function useChannels(options = {}) {
|
|
2436
|
+
const result = useAllChannels(options);
|
|
2437
|
+
const filtered = React.useMemo(
|
|
2438
|
+
() => result.channels.filter((c) => !c.isDm),
|
|
2439
|
+
[result.channels]
|
|
2440
|
+
);
|
|
2441
|
+
return { ...result, channels: filtered };
|
|
2442
|
+
}
|
|
2443
|
+
function useDmChannels(options = {}) {
|
|
2444
|
+
const result = useAllChannels(options);
|
|
2445
|
+
const filtered = React.useMemo(
|
|
2446
|
+
() => result.channels.filter((c) => c.isDm),
|
|
2447
|
+
[result.channels]
|
|
2448
|
+
);
|
|
2449
|
+
return { ...result, channels: filtered };
|
|
2450
|
+
}
|
|
2451
|
+
function useNotifications(options = {}) {
|
|
2452
|
+
const scopeKey = useScopeKey();
|
|
2453
|
+
const {
|
|
2454
|
+
filter = "all",
|
|
2455
|
+
limit = 200,
|
|
2456
|
+
realtime = true,
|
|
2457
|
+
enabled = true
|
|
2458
|
+
} = options;
|
|
2459
|
+
const qc = reactQuery.useQueryClient();
|
|
2460
|
+
const debounceRef = React.useRef(null);
|
|
2461
|
+
const stableKey = React.useMemo(() => vfsKeys.notifications(filter, limit), [filter, limit, scopeKey]);
|
|
2462
|
+
const query = reactQuery.useQuery({
|
|
2463
|
+
queryKey: stableKey,
|
|
2464
|
+
queryFn: async () => {
|
|
2465
|
+
const params = { limit };
|
|
2466
|
+
if (filter === "unread") params.filter = "unread";
|
|
2467
|
+
const result = await gatewayCall("get-notifications", params);
|
|
2468
|
+
return result.notifications || [];
|
|
2469
|
+
},
|
|
2470
|
+
enabled
|
|
2471
|
+
});
|
|
2472
|
+
React.useEffect(() => {
|
|
2473
|
+
if (!realtime || !enabled) return;
|
|
2474
|
+
const unsubs = ["vfs_change", "ccm_change"].map(
|
|
2475
|
+
(eventType) => subscribeToEvents(
|
|
2476
|
+
eventType,
|
|
2477
|
+
() => {
|
|
2478
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
2479
|
+
debounceRef.current = setTimeout(() => {
|
|
2480
|
+
qc.invalidateQueries({ queryKey: stableKey });
|
|
2481
|
+
}, 2e3);
|
|
2482
|
+
},
|
|
2483
|
+
(data) => data.item_type === "notification" || data.item_type === "message"
|
|
2484
|
+
)
|
|
2485
|
+
);
|
|
2486
|
+
return () => {
|
|
2487
|
+
unsubs.forEach((u) => u());
|
|
2488
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
2489
|
+
};
|
|
2490
|
+
}, [realtime, enabled, stableKey, qc]);
|
|
2491
|
+
const notifications = query.data ?? [];
|
|
2492
|
+
return {
|
|
2493
|
+
notifications,
|
|
2494
|
+
isLoading: query.isLoading,
|
|
2495
|
+
isFetching: query.isFetching,
|
|
2496
|
+
error: query.error,
|
|
2497
|
+
refetch: query.refetch
|
|
2498
|
+
};
|
|
2499
|
+
}
|
|
2500
|
+
function useUnreadCounts(options = {}) {
|
|
2501
|
+
const { channels: channelList, realtime = true, enabled = true } = options;
|
|
2502
|
+
const { notifications, isLoading, isFetching, error, refetch } = useNotifications({
|
|
2503
|
+
filter: "unread",
|
|
2504
|
+
limit: 200,
|
|
2505
|
+
realtime,
|
|
2506
|
+
enabled
|
|
2507
|
+
});
|
|
2508
|
+
const unreadByChannel = React.useMemo(() => {
|
|
2509
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2510
|
+
for (const n of notifications) {
|
|
2511
|
+
if (n.is_unread && n.channel_id) {
|
|
2512
|
+
counts.set(n.channel_id, (counts.get(n.channel_id) || 0) + 1);
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
return counts;
|
|
2516
|
+
}, [notifications]);
|
|
2517
|
+
const totalUnread = React.useMemo(() => {
|
|
2518
|
+
let total = 0;
|
|
2519
|
+
unreadByChannel.forEach((v) => {
|
|
2520
|
+
total += v;
|
|
2521
|
+
});
|
|
2522
|
+
return total;
|
|
2523
|
+
}, [unreadByChannel]);
|
|
2524
|
+
const { dmUnread, channelUnread } = React.useMemo(() => {
|
|
2525
|
+
if (!channelList) return { dmUnread: 0, channelUnread: 0 };
|
|
2526
|
+
const dmIds = new Set(channelList.filter((c) => c.isDm).map((c) => c.id));
|
|
2527
|
+
let dm = 0;
|
|
2528
|
+
let ch = 0;
|
|
2529
|
+
unreadByChannel.forEach((count, channelId) => {
|
|
2530
|
+
if (dmIds.has(channelId)) dm += count;
|
|
2531
|
+
else ch += count;
|
|
2532
|
+
});
|
|
2533
|
+
return { dmUnread: dm, channelUnread: ch };
|
|
2534
|
+
}, [channelList, unreadByChannel]);
|
|
2535
|
+
return {
|
|
2536
|
+
unreadByChannel,
|
|
2537
|
+
totalUnread,
|
|
2538
|
+
dmUnread,
|
|
2539
|
+
channelUnread,
|
|
2540
|
+
isLoading,
|
|
2541
|
+
isFetching,
|
|
2542
|
+
error,
|
|
2543
|
+
refetch
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2546
|
+
function useCreate() {
|
|
2547
|
+
const qc = reactQuery.useQueryClient();
|
|
2548
|
+
return reactQuery.useMutation({
|
|
2549
|
+
mutationFn: (params) => gatewayCall("create", params),
|
|
2550
|
+
onSuccess: (_data, variables) => {
|
|
2551
|
+
const parentPath = variables.parent_path;
|
|
2552
|
+
if (parentPath) {
|
|
2553
|
+
qc.invalidateQueries({ queryKey: vfsKeys.children(parentPath) });
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
});
|
|
2557
|
+
}
|
|
2558
|
+
function useUpdate() {
|
|
2559
|
+
const qc = reactQuery.useQueryClient();
|
|
2560
|
+
return reactQuery.useMutation({
|
|
2561
|
+
mutationFn: (params) => gatewayCall("update", params),
|
|
2562
|
+
onSuccess: (_data, variables) => {
|
|
2563
|
+
const path = variables.path;
|
|
2564
|
+
if (path) {
|
|
2565
|
+
qc.invalidateQueries({ queryKey: vfsKeys.item(path) });
|
|
2566
|
+
const segments = path.split("/");
|
|
2567
|
+
segments.pop();
|
|
2568
|
+
const parentPath = segments.join("/");
|
|
2569
|
+
if (parentPath) {
|
|
2570
|
+
qc.invalidateQueries({ queryKey: vfsKeys.children(parentPath) });
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
});
|
|
2575
|
+
}
|
|
2576
|
+
function useDelete() {
|
|
2577
|
+
const qc = reactQuery.useQueryClient();
|
|
2578
|
+
return reactQuery.useMutation({
|
|
2579
|
+
mutationFn: (params) => gatewayCall("delete", params),
|
|
2580
|
+
onSuccess: (_data, variables) => {
|
|
2581
|
+
const path = variables.path;
|
|
2582
|
+
if (path) {
|
|
2583
|
+
qc.removeQueries({ queryKey: vfsKeys.item(path) });
|
|
2584
|
+
const segments = path.split("/");
|
|
2585
|
+
segments.pop();
|
|
2586
|
+
const parentPath = segments.join("/");
|
|
2587
|
+
if (parentPath) {
|
|
2588
|
+
qc.invalidateQueries({ queryKey: vfsKeys.children(parentPath) });
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
});
|
|
2593
|
+
}
|
|
2594
|
+
function useMove() {
|
|
2595
|
+
const qc = reactQuery.useQueryClient();
|
|
2596
|
+
return reactQuery.useMutation({
|
|
2597
|
+
mutationFn: (params) => gatewayCall("move", params),
|
|
2598
|
+
onSuccess: (_data, variables) => {
|
|
2599
|
+
const path = variables.path;
|
|
2600
|
+
const newParentPath = variables.new_parent_path;
|
|
2601
|
+
if (path) {
|
|
2602
|
+
qc.invalidateQueries({ queryKey: vfsKeys.item(path) });
|
|
2603
|
+
const segments = path.split("/");
|
|
2604
|
+
segments.pop();
|
|
2605
|
+
const oldParentPath = segments.join("/");
|
|
2606
|
+
if (oldParentPath) {
|
|
2607
|
+
qc.invalidateQueries({ queryKey: vfsKeys.children(oldParentPath) });
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
if (newParentPath) {
|
|
2611
|
+
qc.invalidateQueries({ queryKey: vfsKeys.children(newParentPath) });
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
});
|
|
2615
|
+
}
|
|
2616
|
+
function useLink() {
|
|
2617
|
+
const qc = reactQuery.useQueryClient();
|
|
2618
|
+
return reactQuery.useMutation({
|
|
2619
|
+
mutationFn: (params) => gatewayCall("link", params),
|
|
2620
|
+
onSuccess: (_data, variables) => {
|
|
2621
|
+
const sourceId = variables.source_id;
|
|
2622
|
+
const targetId = variables.target_id;
|
|
2623
|
+
if (sourceId) {
|
|
2624
|
+
qc.invalidateQueries({ queryKey: vfsKeys.edges(sourceId) });
|
|
2625
|
+
}
|
|
2626
|
+
if (targetId) {
|
|
2627
|
+
qc.invalidateQueries({ queryKey: vfsKeys.edges(targetId) });
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
});
|
|
2631
|
+
}
|
|
2632
|
+
function useMutation(method, options) {
|
|
2633
|
+
const qc = reactQuery.useQueryClient();
|
|
2634
|
+
const { optimistic, ...rest } = options ?? {};
|
|
2635
|
+
return reactQuery.useMutation({
|
|
2636
|
+
mutationFn: (params) => gatewayCall(method, params),
|
|
2637
|
+
onMutate: optimistic ? async (params) => {
|
|
2638
|
+
await qc.cancelQueries({ queryKey: optimistic.queryKey });
|
|
2639
|
+
const previous = qc.getQueryData(optimistic.queryKey);
|
|
2640
|
+
qc.setQueryData(optimistic.queryKey, (old) => optimistic.update(old, params));
|
|
2641
|
+
return { previous };
|
|
2642
|
+
} : void 0,
|
|
2643
|
+
onError: (err, _vars, context) => {
|
|
2644
|
+
if (optimistic && context?.previous !== void 0) {
|
|
2645
|
+
qc.setQueryData(optimistic.queryKey, context.previous);
|
|
2646
|
+
}
|
|
2647
|
+
rest.onError?.(err);
|
|
2648
|
+
},
|
|
2649
|
+
onSuccess: rest.onSuccess,
|
|
2650
|
+
onSettled: optimistic ? () => {
|
|
2651
|
+
qc.invalidateQueries({ queryKey: optimistic.queryKey });
|
|
2652
|
+
} : void 0
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2655
|
+
function useEventStream(handlerOrEventType, handlerOrOptions, filter) {
|
|
2656
|
+
let eventType;
|
|
2657
|
+
let handler;
|
|
2658
|
+
let resolvedFilter;
|
|
2659
|
+
let enabled = true;
|
|
2660
|
+
if (typeof handlerOrEventType === "string") {
|
|
2661
|
+
eventType = handlerOrEventType;
|
|
2662
|
+
handler = handlerOrOptions;
|
|
2663
|
+
resolvedFilter = filter;
|
|
2664
|
+
} else {
|
|
2665
|
+
handler = handlerOrEventType;
|
|
2666
|
+
const opts = handlerOrOptions;
|
|
2667
|
+
eventType = opts.eventType;
|
|
2668
|
+
resolvedFilter = opts.filter;
|
|
2669
|
+
enabled = opts.enabled ?? true;
|
|
2670
|
+
}
|
|
2671
|
+
const handlerRef = React.useRef(handler);
|
|
2672
|
+
const filterRef = React.useRef(resolvedFilter);
|
|
2673
|
+
React.useEffect(() => {
|
|
2674
|
+
handlerRef.current = handler;
|
|
2675
|
+
filterRef.current = resolvedFilter;
|
|
2676
|
+
}, [handler, resolvedFilter]);
|
|
2677
|
+
React.useEffect(() => {
|
|
2678
|
+
if (!enabled) return;
|
|
2679
|
+
const unsub = subscribeToEvents(
|
|
2680
|
+
eventType,
|
|
2681
|
+
(data) => handlerRef.current(data),
|
|
2682
|
+
filterRef.current ? (data) => filterRef.current(data) : void 0
|
|
2683
|
+
);
|
|
2684
|
+
return unsub;
|
|
2685
|
+
}, [eventType, enabled]);
|
|
2686
|
+
}
|
|
2687
|
+
function useRealtimeQuery(options) {
|
|
2688
|
+
const { queryKey, queryFn, eventType, filter, enabled = true, debounceMs = 500 } = options;
|
|
2689
|
+
const qc = reactQuery.useQueryClient();
|
|
2690
|
+
const debounceRef = React.useRef(null);
|
|
2691
|
+
const query = reactQuery.useQuery({
|
|
2692
|
+
queryKey,
|
|
2693
|
+
queryFn,
|
|
2694
|
+
enabled
|
|
2695
|
+
});
|
|
2696
|
+
React.useEffect(() => {
|
|
2697
|
+
if (!enabled) return;
|
|
2698
|
+
const unsub = subscribeToEvents(
|
|
2699
|
+
eventType,
|
|
2700
|
+
() => {
|
|
2701
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
2702
|
+
debounceRef.current = setTimeout(() => {
|
|
2703
|
+
qc.invalidateQueries({ queryKey });
|
|
2704
|
+
}, debounceMs);
|
|
2705
|
+
},
|
|
2706
|
+
filter
|
|
2707
|
+
);
|
|
2708
|
+
return () => {
|
|
2709
|
+
unsub();
|
|
2710
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
2711
|
+
};
|
|
2712
|
+
}, [eventType, enabled, debounceMs, qc, JSON.stringify(queryKey)]);
|
|
2713
|
+
return query;
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
// src/schema-utils.ts
|
|
2717
|
+
function keyToLabel(key) {
|
|
2718
|
+
return key.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
2719
|
+
}
|
|
2720
|
+
function inferFilterWidget(type, enumValues) {
|
|
2721
|
+
if (enumValues && enumValues.length > 0) return "select";
|
|
2722
|
+
if (type === "boolean") return "toggle";
|
|
2723
|
+
if (type === "integer" || type === "number") return "range";
|
|
2724
|
+
if (type === "string") return "text";
|
|
2725
|
+
return "none";
|
|
2726
|
+
}
|
|
2727
|
+
function isFilterable(type, enumValues) {
|
|
2728
|
+
if (type === "object" || type === "array") return false;
|
|
2729
|
+
return true;
|
|
2730
|
+
}
|
|
2731
|
+
function isSortable(type) {
|
|
2732
|
+
return ["string", "number", "integer", "boolean"].includes(type);
|
|
2733
|
+
}
|
|
2734
|
+
function isColumnSuitable(type) {
|
|
2735
|
+
return type !== "object" && type !== "array";
|
|
2736
|
+
}
|
|
2737
|
+
function extractFields(schema, editable, defaults, fieldDefaults) {
|
|
2738
|
+
const properties = schema?.properties;
|
|
2739
|
+
if (!properties || typeof properties !== "object") return [];
|
|
2740
|
+
const fields = [];
|
|
2741
|
+
for (const [key, prop] of Object.entries(properties)) {
|
|
2742
|
+
const type = prop.type || "string";
|
|
2743
|
+
const enumValues = prop.enum;
|
|
2744
|
+
const filterable = isFilterable(type);
|
|
2745
|
+
const sortable = isSortable(type);
|
|
2746
|
+
const filterWidget = inferFilterWidget(type, enumValues);
|
|
2747
|
+
fields.push({
|
|
2748
|
+
key,
|
|
2749
|
+
label: prop.title || keyToLabel(key),
|
|
2750
|
+
type,
|
|
2751
|
+
description: prop.description,
|
|
2752
|
+
enum_values: enumValues,
|
|
2753
|
+
default_value: defaults[key] ?? fieldDefaults[key] ?? prop.default,
|
|
2754
|
+
editable,
|
|
2755
|
+
filterable,
|
|
2756
|
+
sortable,
|
|
2757
|
+
filter_widget: filterWidget
|
|
2758
|
+
});
|
|
2759
|
+
}
|
|
2760
|
+
return fields;
|
|
2761
|
+
}
|
|
2762
|
+
function interpretSchema(typeDef) {
|
|
2763
|
+
const defaults = typeDef.default_data || {};
|
|
2764
|
+
const fieldDefaults = typeDef.field_defaults || {};
|
|
2765
|
+
const inputFields = extractFields(
|
|
2766
|
+
typeDef.input_schema,
|
|
2767
|
+
true,
|
|
2768
|
+
defaults,
|
|
2769
|
+
fieldDefaults
|
|
2770
|
+
);
|
|
2771
|
+
const systemFields = extractFields(
|
|
2772
|
+
typeDef.system_schema,
|
|
2773
|
+
false,
|
|
2774
|
+
defaults,
|
|
2775
|
+
fieldDefaults
|
|
2776
|
+
);
|
|
2777
|
+
const allFields = [...inputFields, ...systemFields];
|
|
2778
|
+
const columns = allFields.filter((f) => isColumnSuitable(f.type));
|
|
2779
|
+
const filterable = allFields.filter((f) => f.filterable);
|
|
2780
|
+
const sortable = allFields.filter((f) => f.sortable);
|
|
2781
|
+
return {
|
|
2782
|
+
fields: allFields,
|
|
2783
|
+
columns,
|
|
2784
|
+
filterable,
|
|
2785
|
+
sortable,
|
|
2786
|
+
form_fields: inputFields,
|
|
2787
|
+
system_fields: systemFields
|
|
2788
|
+
};
|
|
2789
|
+
}
|
|
2790
|
+
function getDefaultSort(_typeDef) {
|
|
2791
|
+
return { field: "updated_at", direction: "desc" };
|
|
2792
|
+
}
|
|
2793
|
+
function getRequiredFormFields(typeDef) {
|
|
2794
|
+
const schema = interpretSchema(typeDef);
|
|
2795
|
+
const required = new Set(typeDef.input_schema?.required || []);
|
|
2796
|
+
return schema.form_fields.filter(
|
|
2797
|
+
(f) => required.has(f.key) || f.default_value === void 0
|
|
2798
|
+
);
|
|
2799
|
+
}
|
|
2800
|
+
var _idCounter = 0;
|
|
2801
|
+
function generateUploadId() {
|
|
2802
|
+
return `upload_${Date.now()}_${++_idCounter}`;
|
|
2803
|
+
}
|
|
2804
|
+
async function uploadFile(file, options, onProgress, signal2) {
|
|
2805
|
+
const fileName = options.name || (file instanceof File ? file.name : `file-${Date.now()}`);
|
|
2806
|
+
const contentType = file.type || "application/octet-stream";
|
|
2807
|
+
const itemType = options.itemType || "file";
|
|
2808
|
+
const createResult = await gatewayCall("create", {
|
|
2809
|
+
parent_path: options.parentPath,
|
|
2810
|
+
item_type: itemType,
|
|
2811
|
+
name: fileName,
|
|
2812
|
+
type_data: {
|
|
2813
|
+
...options.typeData || {},
|
|
2814
|
+
file_source: {
|
|
2815
|
+
type: "upload",
|
|
2816
|
+
content_type: contentType
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
});
|
|
2820
|
+
const result = createResult;
|
|
2821
|
+
if (!result.id) {
|
|
2822
|
+
throw new Error(result.error || result.message || "Failed to create file item");
|
|
2823
|
+
}
|
|
2824
|
+
const itemId = result.id;
|
|
2825
|
+
const itemPath = result.path;
|
|
2826
|
+
if (result.storage_completed && result.file_ref) {
|
|
2827
|
+
return { itemId, itemPath, fileRef: result.file_ref };
|
|
2828
|
+
}
|
|
2829
|
+
const storageKey = result.storage_key;
|
|
2830
|
+
const uploadToken = result.upload_token;
|
|
2831
|
+
const storageWorkerUrl = result.storage_worker_url || "https://fsaos-storage.radns.workers.dev";
|
|
2832
|
+
if (!storageKey) {
|
|
2833
|
+
throw new Error("Gateway returned no storage_key \u2014 file_source may not be supported for this item type");
|
|
2834
|
+
}
|
|
2835
|
+
await uploadBinaryXHR(storageWorkerUrl, storageKey, uploadToken, file, contentType, onProgress, signal2);
|
|
2836
|
+
const fileRef = await confirmUpload(itemId, storageKey, contentType);
|
|
2837
|
+
return { itemId, itemPath, fileRef };
|
|
2838
|
+
}
|
|
2839
|
+
function uploadBinaryXHR(storageWorkerUrl, storageKey, uploadToken, body, contentType, onProgress, signal2) {
|
|
2840
|
+
return new Promise((resolve, reject) => {
|
|
2841
|
+
const xhr = new XMLHttpRequest();
|
|
2842
|
+
xhr.open("PUT", `${storageWorkerUrl}/upload`);
|
|
2843
|
+
xhr.setRequestHeader("X-Storage-Key", storageKey);
|
|
2844
|
+
xhr.setRequestHeader("Content-Type", contentType);
|
|
2845
|
+
if (uploadToken) {
|
|
2846
|
+
xhr.setRequestHeader("X-Upload-Token", uploadToken);
|
|
2847
|
+
}
|
|
2848
|
+
xhr.upload.onprogress = (e) => {
|
|
2849
|
+
if (e.lengthComputable && onProgress) {
|
|
2850
|
+
onProgress(Math.round(e.loaded / e.total * 100));
|
|
2851
|
+
}
|
|
2852
|
+
};
|
|
2853
|
+
xhr.onload = () => {
|
|
2854
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
2855
|
+
resolve();
|
|
2856
|
+
} else {
|
|
2857
|
+
reject(new Error(`Storage upload failed: HTTP ${xhr.status} \u2014 ${xhr.responseText?.slice(0, 300)}`));
|
|
2858
|
+
}
|
|
2859
|
+
};
|
|
2860
|
+
xhr.onerror = () => reject(new Error("Storage upload network error"));
|
|
2861
|
+
xhr.ontimeout = () => reject(new Error("Storage upload timed out"));
|
|
2862
|
+
if (signal2) {
|
|
2863
|
+
if (signal2.aborted) {
|
|
2864
|
+
reject(new Error("Upload cancelled"));
|
|
2865
|
+
return;
|
|
2866
|
+
}
|
|
2867
|
+
signal2.addEventListener("abort", () => {
|
|
2868
|
+
xhr.abort();
|
|
2869
|
+
reject(new Error("Upload cancelled"));
|
|
2870
|
+
});
|
|
2871
|
+
}
|
|
2872
|
+
xhr.send(body);
|
|
2873
|
+
});
|
|
2874
|
+
}
|
|
2875
|
+
async function confirmUpload(itemId, storageKey, contentType) {
|
|
2876
|
+
const token = await getAccessToken();
|
|
2877
|
+
const response = await fetch(`${GATEWAY_URL}/storage/upload-complete`, {
|
|
2878
|
+
method: "POST",
|
|
2879
|
+
headers: {
|
|
2880
|
+
"Content-Type": "application/json",
|
|
2881
|
+
...token ? { "Authorization": `Bearer ${token}` } : {}
|
|
2882
|
+
},
|
|
2883
|
+
body: JSON.stringify({
|
|
2884
|
+
item_id: itemId,
|
|
2885
|
+
storage_key: storageKey,
|
|
2886
|
+
content_type: contentType
|
|
2887
|
+
})
|
|
2888
|
+
});
|
|
2889
|
+
if (!response.ok) {
|
|
2890
|
+
const text = await response.text().catch(() => "");
|
|
2891
|
+
throw new Error(`Upload confirmation failed: HTTP ${response.status} \u2014 ${text.slice(0, 300)}`);
|
|
2892
|
+
}
|
|
2893
|
+
const result = await response.json();
|
|
2894
|
+
if (!result.success) {
|
|
2895
|
+
throw new Error(result.error || result.message || "Upload confirmation rejected");
|
|
2896
|
+
}
|
|
2897
|
+
return result.file_ref;
|
|
2898
|
+
}
|
|
2899
|
+
function useFileUpload() {
|
|
2900
|
+
const [uploads, setUploads] = React.useState([]);
|
|
2901
|
+
const qc = reactQuery.useQueryClient();
|
|
2902
|
+
const uploadsRef = React.useRef(uploads);
|
|
2903
|
+
uploadsRef.current = uploads;
|
|
2904
|
+
const updateUpload = React.useCallback((id, patch) => {
|
|
2905
|
+
setUploads((prev) => prev.map((u) => u.id === id ? { ...u, ...patch } : u));
|
|
2906
|
+
}, []);
|
|
2907
|
+
const addUpload = React.useCallback((item) => {
|
|
2908
|
+
setUploads((prev) => [...prev, item]);
|
|
2909
|
+
}, []);
|
|
2910
|
+
const executeUpload = React.useCallback(async (uploadId, file, options, resumeFrom, existingState) => {
|
|
2911
|
+
const fileName = options.name || file.name;
|
|
2912
|
+
const contentType = file.type || "application/octet-stream";
|
|
2913
|
+
const itemType = options.itemType || "file";
|
|
2914
|
+
let itemId = existingState?.itemId || null;
|
|
2915
|
+
let itemPath = existingState?.itemPath || null;
|
|
2916
|
+
let storageKey = existingState?.storageKey || null;
|
|
2917
|
+
let uploadToken = existingState?.uploadToken || null;
|
|
2918
|
+
let storageWorkerUrl = existingState?.storageWorkerUrl || null;
|
|
2919
|
+
try {
|
|
2920
|
+
if (!resumeFrom || resumeFrom === "create") {
|
|
2921
|
+
updateUpload(uploadId, { status: "creating", error: null, _failedAt: null });
|
|
2922
|
+
const createResult = await gatewayCall("create", {
|
|
2923
|
+
parent_path: options.parentPath,
|
|
2924
|
+
item_type: itemType,
|
|
2925
|
+
name: fileName,
|
|
2926
|
+
type_data: {
|
|
2927
|
+
...options.typeData || {},
|
|
2928
|
+
file_source: {
|
|
2929
|
+
type: "upload",
|
|
2930
|
+
content_type: contentType
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
});
|
|
2934
|
+
const result = createResult;
|
|
2935
|
+
if (!result.id) {
|
|
2936
|
+
throw Object.assign(
|
|
2937
|
+
new Error(result.error || result.message || "Failed to create file item"),
|
|
2938
|
+
{ _step: "create" }
|
|
2939
|
+
);
|
|
2940
|
+
}
|
|
2941
|
+
itemId = result.id;
|
|
2942
|
+
itemPath = result.path;
|
|
2943
|
+
updateUpload(uploadId, { itemId, itemPath });
|
|
2944
|
+
if (result.storage_completed && result.file_ref) {
|
|
2945
|
+
updateUpload(uploadId, {
|
|
2946
|
+
status: "complete",
|
|
2947
|
+
progress: 100,
|
|
2948
|
+
fileRef: result.file_ref
|
|
2949
|
+
});
|
|
2950
|
+
qc.invalidateQueries({ queryKey: vfsKeys.children(options.parentPath) });
|
|
2951
|
+
return;
|
|
2952
|
+
}
|
|
2953
|
+
storageKey = result.storage_key;
|
|
2954
|
+
uploadToken = result.upload_token || "";
|
|
2955
|
+
storageWorkerUrl = result.storage_worker_url || "https://fsaos-storage.radns.workers.dev";
|
|
2956
|
+
updateUpload(uploadId, {
|
|
2957
|
+
_storageKey: storageKey,
|
|
2958
|
+
_uploadToken: uploadToken,
|
|
2959
|
+
_storageWorkerUrl: storageWorkerUrl
|
|
2960
|
+
});
|
|
2961
|
+
if (!storageKey) {
|
|
2962
|
+
throw Object.assign(
|
|
2963
|
+
new Error("Gateway returned no storage_key"),
|
|
2964
|
+
{ _step: "create" }
|
|
2965
|
+
);
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
if (!resumeFrom || resumeFrom === "create" || resumeFrom === "upload") {
|
|
2969
|
+
updateUpload(uploadId, { status: "uploading", progress: 0, error: null, _failedAt: null });
|
|
2970
|
+
const xhrPromise = new Promise((resolve, reject) => {
|
|
2971
|
+
const xhr = new XMLHttpRequest();
|
|
2972
|
+
updateUpload(uploadId, { _xhr: xhr });
|
|
2973
|
+
xhr.open("PUT", `${storageWorkerUrl}/upload`);
|
|
2974
|
+
xhr.setRequestHeader("X-Storage-Key", storageKey);
|
|
2975
|
+
xhr.setRequestHeader("Content-Type", contentType);
|
|
2976
|
+
if (uploadToken) {
|
|
2977
|
+
xhr.setRequestHeader("X-Upload-Token", uploadToken);
|
|
2978
|
+
}
|
|
2979
|
+
xhr.upload.onprogress = (e) => {
|
|
2980
|
+
if (e.lengthComputable) {
|
|
2981
|
+
updateUpload(uploadId, { progress: Math.round(e.loaded / e.total * 100) });
|
|
2982
|
+
}
|
|
2983
|
+
};
|
|
2984
|
+
xhr.onload = () => {
|
|
2985
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
2986
|
+
resolve();
|
|
2987
|
+
} else {
|
|
2988
|
+
reject(Object.assign(
|
|
2989
|
+
new Error(`Storage upload failed: HTTP ${xhr.status}`),
|
|
2990
|
+
{ _step: "upload" }
|
|
2991
|
+
));
|
|
2992
|
+
}
|
|
2993
|
+
};
|
|
2994
|
+
xhr.onerror = () => reject(Object.assign(
|
|
2995
|
+
new Error("Storage upload network error"),
|
|
2996
|
+
{ _step: "upload" }
|
|
2997
|
+
));
|
|
2998
|
+
xhr.ontimeout = () => reject(Object.assign(
|
|
2999
|
+
new Error("Storage upload timed out"),
|
|
3000
|
+
{ _step: "upload" }
|
|
3001
|
+
));
|
|
3002
|
+
xhr.send(file);
|
|
3003
|
+
});
|
|
3004
|
+
await xhrPromise;
|
|
3005
|
+
updateUpload(uploadId, { progress: 100, _xhr: null });
|
|
3006
|
+
}
|
|
3007
|
+
updateUpload(uploadId, { status: "confirming", error: null, _failedAt: null });
|
|
3008
|
+
const fileRef = await confirmUpload(itemId, storageKey, contentType);
|
|
3009
|
+
updateUpload(uploadId, {
|
|
3010
|
+
status: "complete",
|
|
3011
|
+
fileRef
|
|
3012
|
+
});
|
|
3013
|
+
qc.invalidateQueries({ queryKey: vfsKeys.children(options.parentPath) });
|
|
3014
|
+
} catch (err) {
|
|
3015
|
+
const message = err?.message || String(err);
|
|
3016
|
+
const failedAt = err?._step || "create";
|
|
3017
|
+
updateUpload(uploadId, {
|
|
3018
|
+
status: "error",
|
|
3019
|
+
error: message,
|
|
3020
|
+
_failedAt: failedAt,
|
|
3021
|
+
_xhr: null
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3024
|
+
}, [updateUpload, qc]);
|
|
3025
|
+
const upload = React.useCallback((file, options) => {
|
|
3026
|
+
const id = generateUploadId();
|
|
3027
|
+
const item = {
|
|
3028
|
+
id,
|
|
3029
|
+
file,
|
|
3030
|
+
status: "creating",
|
|
3031
|
+
progress: 0,
|
|
3032
|
+
error: null,
|
|
3033
|
+
itemId: null,
|
|
3034
|
+
itemPath: null,
|
|
3035
|
+
fileRef: null,
|
|
3036
|
+
options,
|
|
3037
|
+
_storageKey: null,
|
|
3038
|
+
_uploadToken: null,
|
|
3039
|
+
_storageWorkerUrl: null,
|
|
3040
|
+
_xhr: null,
|
|
3041
|
+
_failedAt: null
|
|
3042
|
+
};
|
|
3043
|
+
addUpload(item);
|
|
3044
|
+
executeUpload(id, file, options);
|
|
3045
|
+
}, [addUpload, executeUpload]);
|
|
3046
|
+
const uploadMultiple = React.useCallback((files, options) => {
|
|
3047
|
+
for (const file of files) {
|
|
3048
|
+
upload(file, options);
|
|
3049
|
+
}
|
|
3050
|
+
}, [upload]);
|
|
3051
|
+
const retryUpload = React.useCallback((uploadId) => {
|
|
3052
|
+
const item = uploadsRef.current.find((u) => u.id === uploadId);
|
|
3053
|
+
if (!item || item.status !== "error") return;
|
|
3054
|
+
const resumeFrom = item._failedAt || "create";
|
|
3055
|
+
const existingState = item._storageKey && item.itemId ? {
|
|
3056
|
+
storageKey: item._storageKey,
|
|
3057
|
+
uploadToken: item._uploadToken || "",
|
|
3058
|
+
storageWorkerUrl: item._storageWorkerUrl || "https://fsaos-storage.radns.workers.dev",
|
|
3059
|
+
itemId: item.itemId,
|
|
3060
|
+
itemPath: item.itemPath || ""
|
|
3061
|
+
} : void 0;
|
|
3062
|
+
executeUpload(uploadId, item.file, item.options, resumeFrom, existingState);
|
|
3063
|
+
}, [executeUpload]);
|
|
3064
|
+
const cancelUpload = React.useCallback((uploadId) => {
|
|
3065
|
+
const item = uploadsRef.current.find((u) => u.id === uploadId);
|
|
3066
|
+
if (!item) return;
|
|
3067
|
+
if (item._xhr) {
|
|
3068
|
+
item._xhr.abort();
|
|
3069
|
+
}
|
|
3070
|
+
updateUpload(uploadId, {
|
|
3071
|
+
status: "error",
|
|
3072
|
+
error: "Cancelled",
|
|
3073
|
+
_xhr: null,
|
|
3074
|
+
_failedAt: null
|
|
3075
|
+
});
|
|
3076
|
+
}, [updateUpload]);
|
|
3077
|
+
const removeUpload = React.useCallback((uploadId) => {
|
|
3078
|
+
setUploads((prev) => prev.filter((u) => u.id !== uploadId));
|
|
3079
|
+
}, []);
|
|
3080
|
+
const clearCompleted = React.useCallback(() => {
|
|
3081
|
+
setUploads((prev) => prev.filter((u) => u.status !== "complete"));
|
|
3082
|
+
}, []);
|
|
3083
|
+
const isUploading = uploads.some(
|
|
3084
|
+
(u) => u.status === "creating" || u.status === "uploading" || u.status === "confirming"
|
|
3085
|
+
);
|
|
3086
|
+
const publicUploads = uploads.map(({ _storageKey, _uploadToken, _storageWorkerUrl, _xhr, _failedAt, ...rest }) => rest);
|
|
3087
|
+
return {
|
|
3088
|
+
upload,
|
|
3089
|
+
uploadMultiple,
|
|
3090
|
+
uploads: publicUploads,
|
|
3091
|
+
retryUpload,
|
|
3092
|
+
cancelUpload,
|
|
3093
|
+
removeUpload,
|
|
3094
|
+
clearCompleted,
|
|
3095
|
+
isUploading
|
|
3096
|
+
};
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
// src/index.ts
|
|
3100
|
+
__registerSdkExports(src_exports);
|
|
3101
|
+
|
|
3102
|
+
Object.defineProperty(exports, "QueryClientProvider", {
|
|
3103
|
+
enumerable: true,
|
|
3104
|
+
get: function () { return reactQuery.QueryClientProvider; }
|
|
3105
|
+
});
|
|
3106
|
+
exports.ComponentRenderer = ComponentRenderer;
|
|
3107
|
+
exports.EnforcementDeniedError = EnforcementDeniedError;
|
|
3108
|
+
exports.KernelProvider = KernelProvider;
|
|
3109
|
+
exports.auth = auth;
|
|
3110
|
+
exports.awaitScopeReady = awaitScopeReady;
|
|
3111
|
+
exports.callTool = callTool;
|
|
3112
|
+
exports.clearCachedToken = clearCachedToken;
|
|
3113
|
+
exports.clearSession = clearSession;
|
|
3114
|
+
exports.createItem = createItem;
|
|
3115
|
+
exports.disconnectSSE = disconnectSSE;
|
|
3116
|
+
exports.disposeVfsRealtimeWs = disposeVfsRealtimeWs;
|
|
3117
|
+
exports.emitEvent = emitEvent;
|
|
3118
|
+
exports.fetchChannelMessages = fetchChannelMessages;
|
|
3119
|
+
exports.fetchChannelMessagesPage = fetchChannelMessagesPage;
|
|
3120
|
+
exports.fetchEdgesForItem = fetchEdgesForItem;
|
|
3121
|
+
exports.fetchItemHistory = fetchItemHistory;
|
|
3122
|
+
exports.fetchItems = fetchItems;
|
|
3123
|
+
exports.fetchMemberFocus = fetchMemberFocus;
|
|
3124
|
+
exports.fetchOpenEnvelope = fetchOpenEnvelope;
|
|
3125
|
+
exports.fetchRecentActivity = fetchRecentActivity;
|
|
3126
|
+
exports.fetchTypeDefinitions = fetchTypeDefinitions;
|
|
3127
|
+
exports.fetchVfsChildren = fetchVfsChildren;
|
|
3128
|
+
exports.fetchVfsChildrenPage = fetchVfsChildrenPage;
|
|
3129
|
+
exports.fetchVfsItem = fetchVfsItem;
|
|
3130
|
+
exports.fetchVfsItemById = fetchVfsItemById;
|
|
3131
|
+
exports.fetchVfsTree = fetchVfsTree;
|
|
3132
|
+
exports.gatewayCall = gatewayCall;
|
|
3133
|
+
exports.getAccessToken = getAccessToken;
|
|
3134
|
+
exports.getAssetUrl = getAssetUrl;
|
|
3135
|
+
exports.getDefaultSort = getDefaultSort;
|
|
3136
|
+
exports.getRequiredFormFields = getRequiredFormFields;
|
|
3137
|
+
exports.getScope = getScope;
|
|
3138
|
+
exports.getScopeVersion = getScopeVersion;
|
|
3139
|
+
exports.getSessionEntry = getSessionEntry;
|
|
3140
|
+
exports.initSession = initSession;
|
|
3141
|
+
exports.initVfsRealtimeWs = initVfsRealtimeWs;
|
|
3142
|
+
exports.interpretSchema = interpretSchema;
|
|
3143
|
+
exports.invalidateAllVfs = invalidateAllVfs;
|
|
3144
|
+
exports.invalidateChannelMessages = invalidateChannelMessages;
|
|
3145
|
+
exports.invalidateChildren = invalidateChildren;
|
|
3146
|
+
exports.invalidateItem = invalidateItem;
|
|
3147
|
+
exports.invalidateOpenEnvelope = invalidateOpenEnvelope;
|
|
3148
|
+
exports.invalidatePathAndParent = invalidatePathAndParent;
|
|
3149
|
+
exports.invalidateSubtree = invalidateSubtree;
|
|
3150
|
+
exports.invalidateTypes = invalidateTypes;
|
|
3151
|
+
exports.isScopeReady = isScopeReady;
|
|
3152
|
+
exports.listChildren = listChildren;
|
|
3153
|
+
exports.mount = mount;
|
|
3154
|
+
exports.mountRealtimeScope = mountRealtimeScope;
|
|
3155
|
+
exports.normalizeItem = normalizeItem;
|
|
3156
|
+
exports.ping = ping;
|
|
3157
|
+
exports.pushChanges = pushChanges;
|
|
3158
|
+
exports.queryClient = queryClient;
|
|
3159
|
+
exports.readItem = readItem;
|
|
3160
|
+
exports.registerCleanup = registerCleanup;
|
|
3161
|
+
exports.resetAllSdkState = resetAllSdkState;
|
|
3162
|
+
exports.resolvePrincipalId = resolvePrincipalId;
|
|
3163
|
+
exports.setCachedToken = setCachedToken;
|
|
3164
|
+
exports.setScope = setScope;
|
|
3165
|
+
exports.setupRequireShim = setupRequireShim;
|
|
3166
|
+
exports.signal = signal;
|
|
3167
|
+
exports.subscribeScope = subscribeScope;
|
|
3168
|
+
exports.subscribeToEvents = subscribeToEvents;
|
|
3169
|
+
exports.subscribeToPath = subscribeToPath;
|
|
3170
|
+
exports.subscribeToScope = subscribeToScope;
|
|
3171
|
+
exports.unmountRealtimeScope = unmountRealtimeScope;
|
|
3172
|
+
exports.updateItem = updateItem;
|
|
3173
|
+
exports.uploadFile = uploadFile;
|
|
3174
|
+
exports.useAccounts = useAccounts;
|
|
3175
|
+
exports.useAllChannels = useAllChannels;
|
|
3176
|
+
exports.useAsset = useAsset;
|
|
3177
|
+
exports.useAuth = useAuth;
|
|
3178
|
+
exports.useChannelMessages = useChannelMessages;
|
|
3179
|
+
exports.useChannels = useChannels;
|
|
3180
|
+
exports.useChildren = useChildren;
|
|
3181
|
+
exports.useComponent = useComponent;
|
|
3182
|
+
exports.useCreate = useCreate;
|
|
3183
|
+
exports.useDelete = useDelete;
|
|
3184
|
+
exports.useDmChannels = useDmChannels;
|
|
3185
|
+
exports.useEdges = useEdges;
|
|
3186
|
+
exports.useEventStream = useEventStream;
|
|
3187
|
+
exports.useFileUpload = useFileUpload;
|
|
3188
|
+
exports.useInfiniteChannelMessages = useInfiniteChannelMessages;
|
|
3189
|
+
exports.useInfiniteChildren = useInfiniteChildren;
|
|
3190
|
+
exports.useInfiniteItems = useInfiniteItems;
|
|
3191
|
+
exports.useItem = useItem;
|
|
3192
|
+
exports.useItemById = useItemById;
|
|
3193
|
+
exports.useItemHistory = useItemHistory;
|
|
3194
|
+
exports.useItems = useItems;
|
|
3195
|
+
exports.useLink = useLink;
|
|
3196
|
+
exports.useList = useList;
|
|
3197
|
+
exports.useMemberFocus = useMemberFocus;
|
|
3198
|
+
exports.useMove = useMove;
|
|
3199
|
+
exports.useMutation = useMutation;
|
|
3200
|
+
exports.useNotifications = useNotifications;
|
|
3201
|
+
exports.useOpen = useOpen;
|
|
3202
|
+
exports.usePermission = usePermission;
|
|
3203
|
+
exports.usePermissions = usePermissions;
|
|
3204
|
+
exports.usePrincipal = usePrincipal;
|
|
3205
|
+
exports.useRealtimeQuery = useRealtimeQuery;
|
|
3206
|
+
exports.useRecentActivity = useRecentActivity;
|
|
3207
|
+
exports.useScope = useScope;
|
|
3208
|
+
exports.useScopeReady = useScopeReady;
|
|
3209
|
+
exports.useSearch = useSearch;
|
|
3210
|
+
exports.useTheme = useTheme;
|
|
3211
|
+
exports.useTool = useTool;
|
|
3212
|
+
exports.useTree = useTree;
|
|
3213
|
+
exports.useType = useType;
|
|
3214
|
+
exports.useTypeHelpers = useTypeHelpers;
|
|
3215
|
+
exports.useTypes = useTypes;
|
|
3216
|
+
exports.useUnreadCounts = useUnreadCounts;
|
|
3217
|
+
exports.useUpdate = useUpdate;
|
|
3218
|
+
exports.vfsKeys = vfsKeys;
|
|
3219
|
+
//# sourceMappingURL=index.cjs.map
|
|
3220
|
+
//# sourceMappingURL=index.cjs.map
|