@spoosh/react 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,549 @@
1
+ "use client";
2
+ "use strict";
3
+ "use client";
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
+
22
+ // src/index.ts
23
+ var src_exports = {};
24
+ __export(src_exports, {
25
+ createReactSpoosh: () => createReactSpoosh,
26
+ createUseInfiniteRead: () => createUseInfiniteRead,
27
+ createUseRead: () => createUseRead,
28
+ createUseWrite: () => createUseWrite
29
+ });
30
+ module.exports = __toCommonJS(src_exports);
31
+
32
+ // src/useRead/index.ts
33
+ var import_react = require("react");
34
+ var import_core = require("@spoosh/core");
35
+ function createUseRead(options) {
36
+ const { api, stateManager, eventEmitter, pluginExecutor } = options;
37
+ return function useRead(readFn, readOptions) {
38
+ const {
39
+ enabled = true,
40
+ tags,
41
+ additionalTags,
42
+ ...pluginOpts
43
+ } = readOptions ?? {};
44
+ const hookId = (0, import_react.useId)();
45
+ const selectorResultRef = (0, import_react.useRef)({
46
+ call: null,
47
+ selector: null
48
+ });
49
+ const selectorProxy = (0, import_core.createSelectorProxy)((result2) => {
50
+ selectorResultRef.current = result2;
51
+ });
52
+ readFn(selectorProxy);
53
+ const capturedCall = selectorResultRef.current.call;
54
+ if (!capturedCall) {
55
+ throw new Error(
56
+ "useRead requires calling an HTTP method ($get). Example: useRead((api) => api.posts.$get())"
57
+ );
58
+ }
59
+ const requestOptions = capturedCall.options;
60
+ const resolvedPath = (0, import_core.resolvePath)(capturedCall.path, requestOptions?.params);
61
+ const resolvedTags = (0, import_core.resolveTags)({ tags, additionalTags }, resolvedPath);
62
+ const queryKey = stateManager.createQueryKey({
63
+ path: capturedCall.path,
64
+ method: capturedCall.method,
65
+ options: capturedCall.options
66
+ });
67
+ const controllerRef = (0, import_react.useRef)(null);
68
+ const lifecycleRef = (0, import_react.useRef)({
69
+ initialized: false,
70
+ prevContext: null
71
+ });
72
+ if (controllerRef.current && controllerRef.current.queryKey !== queryKey) {
73
+ lifecycleRef.current.prevContext = controllerRef.current.controller.getContext();
74
+ }
75
+ if (!controllerRef.current || controllerRef.current.queryKey !== queryKey) {
76
+ const controller2 = (0, import_core.createOperationController)({
77
+ operationType: "read",
78
+ path: capturedCall.path,
79
+ method: capturedCall.method,
80
+ tags: resolvedTags,
81
+ requestOptions: capturedCall.options,
82
+ stateManager,
83
+ eventEmitter,
84
+ pluginExecutor,
85
+ hookId,
86
+ fetchFn: async (fetchOpts) => {
87
+ let current = api;
88
+ for (const segment of resolvedPath) {
89
+ current = current[segment];
90
+ }
91
+ const method = current[capturedCall.method];
92
+ return method(fetchOpts);
93
+ }
94
+ });
95
+ controllerRef.current = { controller: controller2, queryKey };
96
+ }
97
+ const controller = controllerRef.current.controller;
98
+ controller.setPluginOptions(pluginOpts);
99
+ const state = (0, import_react.useSyncExternalStore)(
100
+ controller.subscribe,
101
+ controller.getState,
102
+ controller.getState
103
+ );
104
+ const [requestState, setRequestState] = (0, import_react.useState)({ isPending: false, error: void 0 });
105
+ const abortRef = (0, import_react.useRef)(controller.abort);
106
+ abortRef.current = controller.abort;
107
+ const pluginOptsKey = JSON.stringify(pluginOpts);
108
+ const executeWithTracking = (0, import_react.useCallback)(
109
+ async (force = false) => {
110
+ setRequestState((prev) => ({ ...prev, isPending: true }));
111
+ try {
112
+ const response = await controller.execute(void 0, { force });
113
+ if (response.error) {
114
+ setRequestState({ isPending: false, error: response.error });
115
+ } else {
116
+ setRequestState({ isPending: false, error: void 0 });
117
+ }
118
+ return response;
119
+ } catch (err) {
120
+ setRequestState({ isPending: false, error: err });
121
+ throw err;
122
+ }
123
+ },
124
+ [controller]
125
+ );
126
+ (0, import_react.useEffect)(() => {
127
+ return () => {
128
+ controllerRef.current?.controller.unmount();
129
+ lifecycleRef.current.initialized = false;
130
+ };
131
+ }, []);
132
+ (0, import_react.useEffect)(() => {
133
+ if (!enabled) return;
134
+ const { initialized, prevContext } = lifecycleRef.current;
135
+ if (!initialized) {
136
+ controller.mount();
137
+ lifecycleRef.current.initialized = true;
138
+ } else if (prevContext) {
139
+ controller.update(prevContext);
140
+ lifecycleRef.current.prevContext = null;
141
+ }
142
+ executeWithTracking(false);
143
+ const unsubRefetch = eventEmitter.on("refetch", (event) => {
144
+ if (event.queryKey === queryKey) {
145
+ executeWithTracking(true);
146
+ }
147
+ });
148
+ const unsubInvalidate = eventEmitter.on(
149
+ "invalidate",
150
+ (invalidatedTags) => {
151
+ const hasMatch = invalidatedTags.some(
152
+ (tag) => resolvedTags.includes(tag)
153
+ );
154
+ if (hasMatch) {
155
+ executeWithTracking(true);
156
+ }
157
+ }
158
+ );
159
+ return () => {
160
+ unsubRefetch();
161
+ unsubInvalidate();
162
+ };
163
+ }, [queryKey, enabled]);
164
+ (0, import_react.useEffect)(() => {
165
+ if (!enabled || !lifecycleRef.current.initialized) return;
166
+ const prevContext = controller.getContext();
167
+ controller.update(prevContext);
168
+ }, [pluginOptsKey]);
169
+ const abort = (0, import_react.useCallback)(() => {
170
+ abortRef.current();
171
+ }, []);
172
+ const refetch = (0, import_react.useCallback)(() => {
173
+ return executeWithTracking(true);
174
+ }, [executeWithTracking]);
175
+ const entry = stateManager.getCache(queryKey);
176
+ const pluginResultData = entry?.pluginResult ? Object.fromEntries(entry.pluginResult) : {};
177
+ const opts = capturedCall.options;
178
+ const inputInner = {};
179
+ if (opts?.query !== void 0) {
180
+ inputInner.query = opts.query;
181
+ }
182
+ if (opts?.body !== void 0) {
183
+ inputInner.body = opts.body;
184
+ }
185
+ if (opts?.formData !== void 0) {
186
+ inputInner.formData = opts.formData;
187
+ }
188
+ if (opts?.params !== void 0) {
189
+ inputInner.params = opts.params;
190
+ }
191
+ const inputField = Object.keys(inputInner).length > 0 ? { input: inputInner } : {};
192
+ const hasData = state.data !== void 0;
193
+ const loading = requestState.isPending && !hasData;
194
+ const fetching = requestState.isPending;
195
+ const result = {
196
+ ...pluginResultData,
197
+ ...inputField,
198
+ data: state.data,
199
+ error: requestState.error ?? state.error,
200
+ loading,
201
+ fetching,
202
+ abort,
203
+ refetch
204
+ };
205
+ return result;
206
+ };
207
+ }
208
+
209
+ // src/useWrite/index.ts
210
+ var import_react2 = require("react");
211
+ var import_core2 = require("@spoosh/core");
212
+ function createUseWrite(options) {
213
+ const { api, stateManager, pluginExecutor, eventEmitter } = options;
214
+ return function useWrite(writeFn) {
215
+ const hookId = (0, import_react2.useId)();
216
+ const selectorResultRef = (0, import_react2.useRef)({
217
+ call: null,
218
+ selector: null
219
+ });
220
+ const selectorProxy = (0, import_core2.createSelectorProxy)((result2) => {
221
+ selectorResultRef.current = result2;
222
+ });
223
+ writeFn(selectorProxy);
224
+ const selectedEndpoint = selectorResultRef.current.selector;
225
+ if (!selectedEndpoint) {
226
+ throw new Error(
227
+ "useWrite requires selecting an HTTP method ($post, $put, $patch, $delete). Example: useWrite((api) => api.posts.$post)"
228
+ );
229
+ }
230
+ const queryKey = stateManager.createQueryKey({
231
+ path: selectedEndpoint.path,
232
+ method: selectedEndpoint.method,
233
+ options: void 0
234
+ });
235
+ const controllerRef = (0, import_react2.useRef)(null);
236
+ if (!controllerRef.current || controllerRef.current.queryKey !== queryKey) {
237
+ controllerRef.current = {
238
+ controller: (0, import_core2.createOperationController)({
239
+ operationType: "write",
240
+ path: selectedEndpoint.path,
241
+ method: selectedEndpoint.method,
242
+ tags: [],
243
+ stateManager,
244
+ eventEmitter,
245
+ pluginExecutor,
246
+ hookId,
247
+ fetchFn: async (fetchOpts) => {
248
+ const params = fetchOpts?.params;
249
+ const resolvedPath = (0, import_core2.resolvePath)(selectedEndpoint.path, params);
250
+ let current = api;
251
+ for (const segment of resolvedPath) {
252
+ current = current[segment];
253
+ }
254
+ const method = current[selectedEndpoint.method];
255
+ return method(fetchOpts);
256
+ }
257
+ }),
258
+ queryKey
259
+ };
260
+ }
261
+ const controller = controllerRef.current.controller;
262
+ const state = (0, import_react2.useSyncExternalStore)(
263
+ controller.subscribe,
264
+ controller.getState,
265
+ controller.getState
266
+ );
267
+ const [lastTriggerOptions, setLastTriggerOptions] = (0, import_react2.useState)(void 0);
268
+ const [requestState, setRequestState] = (0, import_react2.useState)({ isPending: false, error: void 0 });
269
+ const reset = (0, import_react2.useCallback)(() => {
270
+ stateManager.deleteCache(queryKey);
271
+ setRequestState({ isPending: false, error: void 0 });
272
+ }, [queryKey]);
273
+ const abort = (0, import_react2.useCallback)(() => {
274
+ controller.abort();
275
+ }, []);
276
+ const trigger = (0, import_react2.useCallback)(
277
+ async (triggerOptions) => {
278
+ setLastTriggerOptions(triggerOptions);
279
+ setRequestState((prev) => ({ ...prev, isPending: true }));
280
+ const params = triggerOptions?.params;
281
+ const resolvedPath = (0, import_core2.resolvePath)(selectedEndpoint.path, params);
282
+ const tags = (0, import_core2.resolveTags)(triggerOptions, resolvedPath);
283
+ controller.setPluginOptions({ ...triggerOptions, tags });
284
+ try {
285
+ const response = await controller.execute(triggerOptions, {
286
+ force: true
287
+ });
288
+ if (response.error) {
289
+ setRequestState({ isPending: false, error: response.error });
290
+ } else {
291
+ setRequestState({ isPending: false, error: void 0 });
292
+ }
293
+ return response;
294
+ } catch (err) {
295
+ setRequestState({ isPending: false, error: err });
296
+ throw err;
297
+ }
298
+ },
299
+ [selectedEndpoint.path]
300
+ );
301
+ const entry = stateManager.getCache(queryKey);
302
+ const pluginResultData = entry?.pluginResult ? Object.fromEntries(entry.pluginResult) : {};
303
+ const opts = lastTriggerOptions;
304
+ const inputInner = {};
305
+ if (opts?.query !== void 0) {
306
+ inputInner.query = opts.query;
307
+ }
308
+ if (opts?.body !== void 0) {
309
+ inputInner.body = opts.body;
310
+ }
311
+ if (opts?.formData !== void 0) {
312
+ inputInner.formData = opts.formData;
313
+ }
314
+ if (opts?.params !== void 0) {
315
+ inputInner.params = opts.params;
316
+ }
317
+ const inputField = Object.keys(inputInner).length > 0 ? { input: inputInner } : {};
318
+ const loading = requestState.isPending;
319
+ const result = {
320
+ trigger,
321
+ ...pluginResultData,
322
+ ...inputField,
323
+ data: state.data,
324
+ error: requestState.error ?? state.error,
325
+ loading,
326
+ reset,
327
+ abort
328
+ };
329
+ return result;
330
+ };
331
+ }
332
+
333
+ // src/useInfiniteRead/index.ts
334
+ var import_react3 = require("react");
335
+ var import_core3 = require("@spoosh/core");
336
+ function createUseInfiniteRead(options) {
337
+ const { api, stateManager, eventEmitter, pluginExecutor } = options;
338
+ return function useInfiniteRead(readFn, readOptions) {
339
+ const {
340
+ enabled = true,
341
+ tags,
342
+ additionalTags,
343
+ canFetchNext,
344
+ nextPageRequest,
345
+ merger,
346
+ canFetchPrev,
347
+ prevPageRequest,
348
+ ...pluginOpts
349
+ } = readOptions;
350
+ const hookId = (0, import_react3.useId)();
351
+ const selectorResultRef = (0, import_react3.useRef)({
352
+ call: null,
353
+ selector: null
354
+ });
355
+ const selectorProxy = (0, import_core3.createSelectorProxy)((result2) => {
356
+ selectorResultRef.current = result2;
357
+ });
358
+ readFn(selectorProxy);
359
+ const capturedCall = selectorResultRef.current.call;
360
+ if (!capturedCall) {
361
+ throw new Error(
362
+ "useInfiniteRead requires calling an HTTP method ($get). Example: useInfiniteRead((api) => api.posts.$get())"
363
+ );
364
+ }
365
+ const requestOptions = capturedCall.options;
366
+ const initialRequest = {
367
+ query: requestOptions?.query,
368
+ params: requestOptions?.params,
369
+ body: requestOptions?.body
370
+ };
371
+ const baseOptionsForKey = {
372
+ ...capturedCall.options,
373
+ query: void 0,
374
+ params: void 0,
375
+ body: void 0
376
+ };
377
+ const resolvedPath = (0, import_core3.resolvePath)(capturedCall.path, requestOptions?.params);
378
+ const resolvedTags = (0, import_core3.resolveTags)({ tags, additionalTags }, resolvedPath);
379
+ const canFetchNextRef = (0, import_react3.useRef)(canFetchNext);
380
+ const canFetchPrevRef = (0, import_react3.useRef)(canFetchPrev);
381
+ const nextPageRequestRef = (0, import_react3.useRef)(nextPageRequest);
382
+ const prevPageRequestRef = (0, import_react3.useRef)(prevPageRequest);
383
+ const mergerRef = (0, import_react3.useRef)(merger);
384
+ canFetchNextRef.current = canFetchNext;
385
+ canFetchPrevRef.current = canFetchPrev;
386
+ nextPageRequestRef.current = nextPageRequest;
387
+ prevPageRequestRef.current = prevPageRequest;
388
+ mergerRef.current = merger;
389
+ const queryKey = stateManager.createQueryKey({
390
+ path: capturedCall.path,
391
+ method: capturedCall.method,
392
+ options: baseOptionsForKey
393
+ });
394
+ const controllerRef = (0, import_react3.useRef)(null);
395
+ if (!controllerRef.current || controllerRef.current.queryKey !== queryKey) {
396
+ controllerRef.current = {
397
+ controller: (0, import_core3.createInfiniteReadController)({
398
+ path: capturedCall.path,
399
+ method: capturedCall.method,
400
+ tags: resolvedTags,
401
+ initialRequest,
402
+ baseOptionsForKey,
403
+ canFetchNext: (ctx) => canFetchNextRef.current(ctx),
404
+ canFetchPrev: canFetchPrev ? (ctx) => canFetchPrevRef.current?.(ctx) ?? false : void 0,
405
+ nextPageRequest: (ctx) => nextPageRequestRef.current(ctx),
406
+ prevPageRequest: prevPageRequest ? (ctx) => prevPageRequestRef.current?.(ctx) ?? {} : void 0,
407
+ merger: (responses) => mergerRef.current(responses),
408
+ stateManager,
409
+ eventEmitter,
410
+ pluginExecutor,
411
+ hookId,
412
+ fetchFn: async (opts, signal) => {
413
+ const fetchPath = (0, import_core3.resolvePath)(capturedCall.path, opts.params);
414
+ let current = api;
415
+ for (const segment of fetchPath) {
416
+ current = current[segment];
417
+ }
418
+ const method = current[capturedCall.method];
419
+ const fetchOptions = {
420
+ ...capturedCall.options,
421
+ query: opts.query,
422
+ params: opts.params,
423
+ body: opts.body,
424
+ signal
425
+ };
426
+ return method(fetchOptions);
427
+ }
428
+ }),
429
+ queryKey
430
+ };
431
+ }
432
+ const controller = controllerRef.current.controller;
433
+ controller.setPluginOptions(pluginOpts);
434
+ const state = (0, import_react3.useSyncExternalStore)(
435
+ controller.subscribe,
436
+ controller.getState,
437
+ controller.getState
438
+ );
439
+ const fetchingDirection = controller.getFetchingDirection();
440
+ const fetching = fetchingDirection !== null;
441
+ const fetchingNext = fetchingDirection === "next";
442
+ const fetchingPrev = fetchingDirection === "prev";
443
+ const loading = fetching && state.data === void 0;
444
+ const lifecycleRef = (0, import_react3.useRef)({
445
+ initialized: false,
446
+ prevContext: null
447
+ });
448
+ (0, import_react3.useEffect)(() => {
449
+ return () => {
450
+ controllerRef.current?.controller.unmount();
451
+ lifecycleRef.current.initialized = false;
452
+ };
453
+ }, []);
454
+ (0, import_react3.useEffect)(() => {
455
+ controller.mount();
456
+ lifecycleRef.current.initialized = true;
457
+ const unsubInvalidate = eventEmitter.on(
458
+ "invalidate",
459
+ (invalidatedTags) => {
460
+ const hasMatch = invalidatedTags.some(
461
+ (tag) => resolvedTags.includes(tag)
462
+ );
463
+ if (hasMatch) {
464
+ controller.refetch();
465
+ }
466
+ }
467
+ );
468
+ return () => {
469
+ unsubInvalidate();
470
+ };
471
+ }, []);
472
+ (0, import_react3.useEffect)(() => {
473
+ if (!lifecycleRef.current.initialized) return;
474
+ if (enabled) {
475
+ const currentState = controller.getState();
476
+ const isFetching = controller.getFetchingDirection() !== null;
477
+ if (currentState.data === void 0 && !isFetching) {
478
+ controller.fetchNext();
479
+ }
480
+ }
481
+ }, [enabled]);
482
+ (0, import_react3.useEffect)(() => {
483
+ if (!enabled || !lifecycleRef.current.initialized) return;
484
+ const prevContext = controller.getContext();
485
+ controller.update(prevContext);
486
+ }, [JSON.stringify(pluginOpts)]);
487
+ const result = {
488
+ data: state.data,
489
+ allResponses: state.allResponses,
490
+ loading,
491
+ fetching,
492
+ fetchingNext,
493
+ fetchingPrev,
494
+ canFetchNext: state.canFetchNext,
495
+ canFetchPrev: state.canFetchPrev,
496
+ fetchNext: controller.fetchNext,
497
+ fetchPrev: controller.fetchPrev,
498
+ refetch: controller.refetch,
499
+ abort: controller.abort,
500
+ error: state.error
501
+ };
502
+ return result;
503
+ };
504
+ }
505
+
506
+ // src/createReactSpoosh/index.ts
507
+ function createReactSpoosh(instance) {
508
+ const { api, stateManager, eventEmitter, pluginExecutor } = instance;
509
+ const useRead = createUseRead({
510
+ api,
511
+ stateManager,
512
+ eventEmitter,
513
+ pluginExecutor
514
+ });
515
+ const useWrite = createUseWrite({
516
+ api,
517
+ stateManager,
518
+ eventEmitter,
519
+ pluginExecutor
520
+ });
521
+ const useInfiniteRead = createUseInfiniteRead({
522
+ api,
523
+ stateManager,
524
+ eventEmitter,
525
+ pluginExecutor
526
+ });
527
+ const instanceApiContext = {
528
+ api,
529
+ stateManager,
530
+ eventEmitter,
531
+ pluginExecutor
532
+ };
533
+ const plugins = pluginExecutor.getPlugins();
534
+ const instanceApis = plugins.reduce(
535
+ (acc, plugin) => {
536
+ if (plugin.instanceApi) {
537
+ return { ...acc, ...plugin.instanceApi(instanceApiContext) };
538
+ }
539
+ return acc;
540
+ },
541
+ {}
542
+ );
543
+ return {
544
+ useRead,
545
+ useWrite,
546
+ useInfiniteRead,
547
+ ...instanceApis
548
+ };
549
+ }