@fractalshq/sync 0.0.4 → 0.0.5

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.
@@ -20,6 +20,18 @@ var __spreadValues = (a, b) => {
20
20
  return a;
21
21
  };
22
22
  var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
23
+ var __objRest = (source, exclude) => {
24
+ var target = {};
25
+ for (var prop in source)
26
+ if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
27
+ target[prop] = source[prop];
28
+ if (source != null && __getOwnPropSymbols)
29
+ for (var prop of __getOwnPropSymbols(source)) {
30
+ if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
31
+ target[prop] = source[prop];
32
+ }
33
+ return target;
34
+ };
23
35
  var __export = (target, all) => {
24
36
  for (var name in all)
25
37
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -38,23 +50,191 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
38
50
  var react_exports = {};
39
51
  __export(react_exports, {
40
52
  SyncProvider: () => SyncProvider,
41
- SyncReactError: () => SyncReactError,
42
53
  useClaimFlow: () => useClaimFlow,
43
54
  useClaimHistory: () => useClaimHistory,
44
55
  useClaimTransaction: () => useClaimTransaction,
45
56
  useClaimableDistributions: () => useClaimableDistributions,
46
57
  useCommitClaim: () => useCommitClaim,
58
+ useCommitDistribution: () => useCommitDistribution,
47
59
  useDistribution: () => useDistribution,
60
+ useDistributionFlow: () => useDistributionFlow,
61
+ useDistributionTransaction: () => useDistributionTransaction,
48
62
  useDistributions: () => useDistributions,
49
63
  useSyncClient: () => useSyncClient
50
64
  });
51
65
  module.exports = __toCommonJS(react_exports);
52
66
  var import_react = require("react");
53
67
  var import_react_query = require("@tanstack/react-query");
68
+
69
+ // src/core/constants.ts
70
+ var DEFAULT_RPC_ENDPOINT = process.env.NEXT_PUBLIC_SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com";
71
+
72
+ // src/core/persistence.ts
73
+ var STORAGE_PREFIX = "distribution:persistence-warning:";
74
+ var PERSISTENCE_WARNING_EVENT = "distribution:persistence-warning";
75
+ function isBrowser() {
76
+ return typeof window !== "undefined" && typeof localStorage !== "undefined";
77
+ }
78
+ function persistToStorage(record) {
79
+ if (!isBrowser()) return;
80
+ try {
81
+ localStorage.setItem(`${STORAGE_PREFIX}${record.id}`, JSON.stringify(record));
82
+ } catch (error) {
83
+ console.error("[sync:persistence-warning] Failed to write to localStorage", error);
84
+ }
85
+ }
86
+ function emitWarning(record) {
87
+ if (!isBrowser()) return;
88
+ try {
89
+ window.dispatchEvent(new CustomEvent(PERSISTENCE_WARNING_EVENT, { detail: record }));
90
+ } catch (error) {
91
+ console.error("[sync:persistence-warning] Failed to emit warning event", error);
92
+ }
93
+ }
94
+ function recordPersistenceWarning(input) {
95
+ var _a2;
96
+ if (!isBrowser()) return void 0;
97
+ const record = __spreadProps(__spreadValues({}, input), {
98
+ createdAt: (_a2 = input.createdAt) != null ? _a2 : Date.now()
99
+ });
100
+ persistToStorage(record);
101
+ emitWarning(record);
102
+ return record;
103
+ }
104
+ function handlePersistenceWarningResponse(response) {
105
+ var _a2;
106
+ if (!isBrowser() || !response || typeof response !== "object") return;
107
+ const warning = response.persistenceWarning;
108
+ if (!warning) return;
109
+ const distributionId = warning.distributionId || response.distributionId || "unknown";
110
+ const exportJson = (_a2 = response.exportJson) != null ? _a2 : warning.exportJson;
111
+ const reason = warning.reason || "persist_distribution_failed";
112
+ const message = warning.message || "We were unable to save your distribution draft. Download the JSON to avoid data loss.";
113
+ const key = warning.id || `${distributionId}:${reason}:${Date.now()}`;
114
+ recordPersistenceWarning({
115
+ id: key,
116
+ distributionId,
117
+ reason,
118
+ message,
119
+ detail: warning.detail,
120
+ exportJson
121
+ });
122
+ }
123
+
124
+ // src/core/http.ts
54
125
  var FALLBACK_BASE_PATH = "/api/v1/fractals-sync";
55
126
  var _a, _b;
56
127
  var ENV_BASE_PATH = typeof process !== "undefined" && (((_a = process.env) == null ? void 0 : _a.NEXT_PUBLIC_SYNC_PATH) || ((_b = process.env) == null ? void 0 : _b.NEXT_PUBLIC_FRACTALS_SYNC_PATH)) || void 0;
57
128
  var DEFAULT_BASE_PATH = sanitizeBasePath(ENV_BASE_PATH || FALLBACK_BASE_PATH);
129
+ function resolveRequestConfig(options) {
130
+ return {
131
+ basePath: sanitizeBasePath((options == null ? void 0 : options.basePath) || DEFAULT_BASE_PATH),
132
+ fetcher: resolveFetcher(options == null ? void 0 : options.fetcher)
133
+ };
134
+ }
135
+ var SyncReactError = class extends Error {
136
+ constructor(status, code, details) {
137
+ super(code || `Sync request failed with status ${status}`);
138
+ this.status = status;
139
+ this.code = code;
140
+ this.details = details;
141
+ this.name = "SyncReactError";
142
+ }
143
+ };
144
+ async function requestJSON(config, path, init) {
145
+ var _a2;
146
+ const url = buildUrl(config.basePath, path);
147
+ const requestInit = prepareInit(init);
148
+ requestInit.credentials = (_a2 = requestInit.credentials) != null ? _a2 : isRelativeUrl(url) ? "include" : void 0;
149
+ const response = await config.fetcher(url, requestInit);
150
+ const payload = await parseResponseBody(response);
151
+ if (!response.ok) {
152
+ const errorCode = typeof payload === "object" && payload && "error" in payload ? String(payload.error) : void 0;
153
+ throw new SyncReactError(response.status, errorCode, payload);
154
+ }
155
+ return payload;
156
+ }
157
+ function resolveFetcher(custom) {
158
+ if (custom) return custom;
159
+ if (typeof fetch === "function") return fetch.bind(globalThis);
160
+ throw new Error("Global fetch is not available. Provide a fetcher via Sync configuration.");
161
+ }
162
+ function sanitizeBasePath(path) {
163
+ if (!path) return FALLBACK_BASE_PATH;
164
+ const trimmed = path.trim();
165
+ if (!trimmed || trimmed === "/") return "/";
166
+ return trimmed.replace(/\/+$/, "");
167
+ }
168
+ function buildUrl(basePath, path) {
169
+ if (path && isAbsoluteUrl(path)) return path;
170
+ const normalizedBase = sanitizeBasePath(basePath);
171
+ const suffix = path ? `/${path.replace(/^\/+/, "")}` : "";
172
+ if (!normalizedBase || normalizedBase === "/") {
173
+ return suffix ? suffix : normalizedBase;
174
+ }
175
+ return `${normalizedBase}${suffix}`;
176
+ }
177
+ function isAbsoluteUrl(path) {
178
+ return /^https?:\/\//i.test(path);
179
+ }
180
+ function isRelativeUrl(path) {
181
+ return !isAbsoluteUrl(path);
182
+ }
183
+ function prepareInit(init) {
184
+ const finalInit = __spreadValues({}, init);
185
+ if ((init == null ? void 0 : init.headers) instanceof Headers) {
186
+ const cloned = new Headers(init.headers);
187
+ if (!cloned.has("content-type")) cloned.set("content-type", "application/json");
188
+ finalInit.headers = cloned;
189
+ return finalInit;
190
+ }
191
+ const headers = new Headers(init == null ? void 0 : init.headers);
192
+ if (!headers.has("content-type")) {
193
+ headers.set("content-type", "application/json");
194
+ }
195
+ finalInit.headers = headers;
196
+ return finalInit;
197
+ }
198
+ async function parseResponseBody(response) {
199
+ if (response.status === 204) return void 0;
200
+ const text = await response.text();
201
+ if (!text) return void 0;
202
+ try {
203
+ return JSON.parse(text);
204
+ } catch (e) {
205
+ return text;
206
+ }
207
+ }
208
+
209
+ // src/core/api.ts
210
+ async function buildDistributionTransaction(input, options) {
211
+ const config = resolveRequestConfig(options);
212
+ const path = (input == null ? void 0 : input.distributionId) ? `/distributions/${encodeURIComponent(input.distributionId)}/create-transaction` : "/distributions/create-transaction";
213
+ const payload = await requestJSON(config, path, {
214
+ method: "POST",
215
+ body: JSON.stringify(input)
216
+ });
217
+ handlePersistenceWarningResponse(payload);
218
+ return payload;
219
+ }
220
+ async function commitDistributionSignature(input, options) {
221
+ const distributionId = input == null ? void 0 : input.distributionId;
222
+ if (!distributionId) {
223
+ throw new SyncReactError(400, "distribution_id_required");
224
+ }
225
+ const config = resolveRequestConfig(options);
226
+ const _a2 = input, { distributionId: _unused } = _a2, body = __objRest(_a2, ["distributionId"]);
227
+ return requestJSON(
228
+ config,
229
+ `/distributions/${encodeURIComponent(distributionId)}/commit`,
230
+ {
231
+ method: "POST",
232
+ body: JSON.stringify(body)
233
+ }
234
+ );
235
+ }
236
+
237
+ // src/react/index.tsx
58
238
  var SyncContext = (0, import_react.createContext)({ basePath: DEFAULT_BASE_PATH });
59
239
  function SyncProvider({ basePath, fetcher, children }) {
60
240
  const value = (0, import_react.useMemo)(
@@ -67,13 +247,11 @@ function SyncProvider({ basePath, fetcher, children }) {
67
247
  return (0, import_react.createElement)(SyncContext.Provider, { value }, children);
68
248
  }
69
249
  function useSyncClient() {
70
- const context = (0, import_react.useContext)(SyncContext);
71
- const basePath = sanitizeBasePath((context == null ? void 0 : context.basePath) || DEFAULT_BASE_PATH);
72
- const fetcher = resolveFetcher(context == null ? void 0 : context.fetcher);
250
+ const requestConfig = useSyncRequestConfig();
73
251
  return (0, import_react.useMemo)(() => {
74
- const request = (path, init) => requestJSON({ basePath, fetcher }, path, init);
252
+ const request = (path, init) => requestJSON(requestConfig, path, init);
75
253
  return {
76
- basePath,
254
+ basePath: requestConfig.basePath,
77
255
  request,
78
256
  get: (path, init) => request(path, __spreadProps(__spreadValues({}, init), { method: "GET" })),
79
257
  post: (path, body, init) => {
@@ -84,22 +262,16 @@ function useSyncClient() {
84
262
  return request(path, finalInit);
85
263
  }
86
264
  };
87
- }, [basePath, fetcher]);
265
+ }, [requestConfig]);
88
266
  }
89
- var SyncReactError = class extends Error {
90
- constructor(status, code, details) {
91
- super(code || `Sync request failed with status ${status}`);
92
- this.status = status;
93
- this.code = code;
94
- this.details = details;
95
- this.name = "SyncReactError";
96
- }
97
- };
98
- function useDistributions(options) {
267
+ function useDistributions(paramsOrOptions, optionsMaybe) {
268
+ var _a2, _b2;
99
269
  const client = useSyncClient();
270
+ const { params, options } = resolveDistributionsArgs(paramsOrOptions, optionsMaybe);
271
+ const path = buildDistributionsPath(params);
100
272
  return (0, import_react_query.useQuery)(__spreadValues({
101
- queryKey: ["sync", "distributions", "me"],
102
- queryFn: () => client.get("/distributions/me")
273
+ queryKey: ["sync", "distributions", "me", (_a2 = params == null ? void 0 : params.role) != null ? _a2 : "claimant", (_b2 = params == null ? void 0 : params.status) != null ? _b2 : "all"],
274
+ queryFn: () => client.get(path)
103
275
  }, options));
104
276
  }
105
277
  function useDistribution(distributionId, options) {
@@ -113,6 +285,88 @@ function useDistribution(distributionId, options) {
113
285
  enabled: Boolean(id) && ((_a2 = options == null ? void 0 : options.enabled) != null ? _a2 : true)
114
286
  }));
115
287
  }
288
+ function useDistributionTransaction(options) {
289
+ const requestConfig = useSyncRequestConfig();
290
+ return (0, import_react_query.useMutation)(__spreadValues({
291
+ mutationFn: async (input) => {
292
+ if (!input) throw new SyncReactError(400, "distribution_input_required");
293
+ return buildDistributionTransaction(input, requestConfig);
294
+ }
295
+ }, options));
296
+ }
297
+ function useCommitDistribution(options) {
298
+ const requestConfig = useSyncRequestConfig();
299
+ return (0, import_react_query.useMutation)(__spreadValues({
300
+ mutationFn: async (input) => {
301
+ if (!(input == null ? void 0 : input.distributionId)) throw new SyncReactError(400, "distribution_id_required");
302
+ return commitDistributionSignature(input, requestConfig);
303
+ }
304
+ }, options));
305
+ }
306
+ function useDistributionFlow() {
307
+ var _a2, _b2;
308
+ const buildMutation = useDistributionTransaction();
309
+ const commitMutation = useCommitDistribution();
310
+ const [latestPayload, setLatestPayload] = (0, import_react.useState)(null);
311
+ const [latestInput, setLatestInput] = (0, import_react.useState)(null);
312
+ const build = (0, import_react.useCallback)(
313
+ async (input) => {
314
+ const payload = await buildMutation.mutateAsync(input);
315
+ setLatestPayload(payload);
316
+ setLatestInput(cloneDistributionInput(input));
317
+ return payload;
318
+ },
319
+ [buildMutation]
320
+ );
321
+ const doCommit = (0, import_react.useCallback)(
322
+ async (signer, input) => {
323
+ let payload = null;
324
+ if (input) {
325
+ payload = await build(input);
326
+ } else if (latestInput) {
327
+ payload = await build(latestInput);
328
+ } else if (latestPayload) {
329
+ payload = latestPayload;
330
+ }
331
+ if (!payload) {
332
+ throw new SyncReactError(400, "distribution_payload_missing");
333
+ }
334
+ if (!payload.distributionId) {
335
+ throw new SyncReactError(400, "distribution_id_required");
336
+ }
337
+ const signerInput = await signer(payload);
338
+ if (!signerInput || !signerInput.signature && !signerInput.transaction && !signerInput.signedTransactionBase64) {
339
+ throw new SyncReactError(400, "distribution_signature_required");
340
+ }
341
+ const commitPayload = __spreadProps(__spreadValues({}, signerInput), {
342
+ distributionId: payload.distributionId
343
+ });
344
+ if (!commitPayload.exportJson && payload.exportJson) {
345
+ commitPayload.exportJson = payload.exportJson;
346
+ }
347
+ const commitResult = await commitMutation.mutateAsync(commitPayload);
348
+ return { payload, commit: commitResult, signerInput };
349
+ },
350
+ [build, latestInput, latestPayload, commitMutation]
351
+ );
352
+ const reset = (0, import_react.useCallback)(() => {
353
+ setLatestPayload(null);
354
+ setLatestInput(null);
355
+ buildMutation.reset();
356
+ commitMutation.reset();
357
+ }, [buildMutation, commitMutation]);
358
+ const error = (_b2 = (_a2 = buildMutation.error) != null ? _a2 : commitMutation.error) != null ? _b2 : null;
359
+ const state = (0, import_react.useMemo)(
360
+ () => ({
361
+ latestPayload,
362
+ building: buildMutation.isPending,
363
+ committing: commitMutation.isPending,
364
+ error
365
+ }),
366
+ [latestPayload, buildMutation.isPending, commitMutation.isPending, error]
367
+ );
368
+ return { state, build, commit: doCommit, claim: doCommit, reset };
369
+ }
116
370
  function useClaimableDistributions(options) {
117
371
  const client = useSyncClient();
118
372
  return (0, import_react_query.useQuery)(__spreadValues({
@@ -186,80 +440,50 @@ function useClaimFlow(distributionId) {
186
440
  };
187
441
  return { state, build, claim, reset };
188
442
  }
189
- async function requestJSON(config, path, init) {
190
- var _a2;
191
- const url = buildUrl(config.basePath, path);
192
- const requestInit = prepareInit(init);
193
- requestInit.credentials = (_a2 = requestInit.credentials) != null ? _a2 : isRelativeUrl(url) ? "include" : void 0;
194
- const response = await config.fetcher(url, requestInit);
195
- const payload = await parseResponseBody(response);
196
- if (!response.ok) {
197
- const errorCode = typeof payload === "object" && payload && "error" in payload ? String(payload.error) : void 0;
198
- throw new SyncReactError(response.status, errorCode, payload);
199
- }
200
- return payload;
443
+ function useSyncRequestConfig() {
444
+ const context = (0, import_react.useContext)(SyncContext);
445
+ const basePath = sanitizeBasePath((context == null ? void 0 : context.basePath) || DEFAULT_BASE_PATH);
446
+ const fetcher = resolveFetcher(context == null ? void 0 : context.fetcher);
447
+ return (0, import_react.useMemo)(() => ({ basePath, fetcher }), [basePath, fetcher]);
201
448
  }
202
- function prepareInit(init) {
203
- const finalInit = __spreadValues({}, init);
204
- if ((init == null ? void 0 : init.headers) instanceof Headers) {
205
- const cloned = new Headers(init.headers);
206
- if (!cloned.has("content-type")) cloned.set("content-type", "application/json");
207
- finalInit.headers = cloned;
208
- return finalInit;
209
- }
210
- const headers = new Headers(init == null ? void 0 : init.headers);
211
- if (!headers.has("content-type")) {
212
- headers.set("content-type", "application/json");
213
- }
214
- finalInit.headers = headers;
215
- return finalInit;
449
+ function cloneDistributionInput(input) {
450
+ return __spreadProps(__spreadValues({}, input), {
451
+ recipients: input.recipients.map((recipient) => __spreadValues({}, recipient))
452
+ });
216
453
  }
217
- async function parseResponseBody(response) {
218
- if (response.status === 204) return void 0;
219
- const text = await response.text();
220
- if (!text) return void 0;
221
- try {
222
- return JSON.parse(text);
223
- } catch (e) {
224
- return text;
454
+ function resolveDistributionsArgs(paramsOrOptions, optionsMaybe) {
455
+ if (isDistributionsParams(paramsOrOptions)) {
456
+ return { params: paramsOrOptions, options: optionsMaybe };
225
457
  }
458
+ return { params: void 0, options: paramsOrOptions };
226
459
  }
227
- function resolveFetcher(custom) {
228
- if (custom) return custom;
229
- if (typeof fetch === "function") return fetch.bind(globalThis);
230
- throw new Error("Global fetch is not available. Provide a fetcher via <SyncProvider fetcher={...}>.");
460
+ function isDistributionsParams(arg) {
461
+ if (!arg || typeof arg !== "object") return false;
462
+ return "role" in arg || "status" in arg;
231
463
  }
232
- function sanitizeBasePath(path) {
233
- if (!path) return FALLBACK_BASE_PATH;
234
- const trimmed = path.trim();
235
- if (!trimmed || trimmed === "/") return "/";
236
- return trimmed.replace(/\/+$/, "");
237
- }
238
- function buildUrl(basePath, path) {
239
- if (path && isAbsoluteUrl(path)) return path;
240
- const normalizedBase = sanitizeBasePath(basePath);
241
- const suffix = path ? `/${path.replace(/^\/+/, "")}` : "";
242
- if (!normalizedBase || normalizedBase === "/") {
243
- return suffix ? suffix : normalizedBase;
464
+ function buildDistributionsPath(params) {
465
+ const search = new URLSearchParams();
466
+ if ((params == null ? void 0 : params.role) && params.role !== "claimant") {
467
+ search.set("role", params.role);
244
468
  }
245
- return `${normalizedBase}${suffix}`;
246
- }
247
- function isAbsoluteUrl(path) {
248
- return /^https?:\/\//i.test(path);
249
- }
250
- function isRelativeUrl(path) {
251
- return !isAbsoluteUrl(path);
469
+ if (params == null ? void 0 : params.status) {
470
+ search.set("status", params.status);
471
+ }
472
+ const qs = search.toString();
473
+ return qs ? `/distributions/me?${qs}` : "/distributions/me";
252
474
  }
253
475
  // Annotate the CommonJS export names for ESM import in node:
254
476
  0 && (module.exports = {
255
477
  SyncProvider,
256
- SyncReactError,
257
478
  useClaimFlow,
258
479
  useClaimHistory,
259
480
  useClaimTransaction,
260
481
  useClaimableDistributions,
261
482
  useCommitClaim,
483
+ useCommitDistribution,
262
484
  useDistribution,
485
+ useDistributionFlow,
486
+ useDistributionTransaction,
263
487
  useDistributions,
264
488
  useSyncClient
265
489
  });