@studiocms/cfetch 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,7 +17,7 @@ This is an [Astro integration](https://docs.astro.build/en/guides/integrations-g
17
17
 
18
18
  ### Installation
19
19
 
20
- Install the integration **automatically** using the Astro CLI:
20
+ 1. Install the integration **automatically** using the Astro CLI:
21
21
 
22
22
  ```bash
23
23
  pnpm astro add @studiocms/cfetch
@@ -47,7 +47,23 @@ npm install @studiocms/cfetch
47
47
  yarn add @studiocms/cfetch
48
48
  ```
49
49
 
50
- 2. Add the integration to your astro config
50
+ 2. Install peer dependencies
51
+
52
+ If your package manager does not automatically install peer dependencies, you will need to ensure `Effect` is installed.
53
+
54
+ ```bash
55
+ pnpm add effect
56
+ ```
57
+
58
+ ```bash
59
+ npm install effect
60
+ ```
61
+
62
+ ```bash
63
+ yarn add effect
64
+ ```
65
+
66
+ 3. Add the integration to your astro config
51
67
 
52
68
  ```diff
53
69
  +import cFetch from "@studiocms/cfetch";
@@ -61,39 +77,266 @@ export default defineConfig({
61
77
 
62
78
  ### Usage
63
79
 
64
- You can import the `cFetch` function anywhere and use it as you would use a normal `fetch` call. `cFetch` adapts the same default options as `fetch`:
80
+ This integration includes various versions of cached fetch functions and [Effects](https://effect.website) to allow full control of how you work with your data.
81
+
82
+ #### Effects
83
+
84
+ All Effects have the following return pattern or derivatives there of
85
+
86
+ ```ts
87
+ Effect.Effect<CachedResponse<T>, FetchError, never>;
88
+ ```
89
+
90
+ ##### `CachedResponse<T>` type
91
+
92
+ ```ts
93
+ interface CachedResponse<T> {
94
+ data: T;
95
+ ok: boolean;
96
+ status: number;
97
+ statusText: string;
98
+ headers: Record<string, string>;
99
+ }
100
+ ```
101
+
102
+ ##### `CFetchConfig` type
103
+
104
+ ```ts
105
+ interface CFetchConfig {
106
+ ttl?: Duration.DurationInput;
107
+ tags?: string[];
108
+ key?: string;
109
+ verbose?: boolean;
110
+ }
111
+ ```
112
+
113
+ ##### `cFetchEffect`
114
+
115
+ ###### Interface
116
+
117
+ ```ts
118
+ const cFetchEffect: <T>(
119
+ url: string | URL,
120
+ parser: (response: Response) => Promise<T>,
121
+ options?: RequestInit | undefined,
122
+ cacheConfig?: CFetchConfig | undefined
123
+ ) => Effect.Effect<CachedResponse<T>, FetchError, never>
124
+ ```
125
+
126
+ ###### Example Usage
127
+
128
+ ```ts
129
+ import { cFetchEffect, Duration } from "c:fetch"
130
+
131
+ const effect = cFetchEffect<{ foo: string; bar: number; }>(
132
+ 'https://api.example.com/data',
133
+ (res) => res.json(),
134
+ { method: "GET" },
135
+ { ttl?: Duration.hours(1), tags?: ['example'], key?: "api-data-fetch", verbose?: false }
136
+ );
137
+ /*
138
+ Return type:
139
+ Effect.Effect<CachedResponse<{ foo: string; bar: number; }>, FetchError, never>
140
+ */
141
+ ```
142
+
143
+ ##### `cFetchEffectJson`
144
+
145
+ ###### Interface
146
+
147
+ ```ts
148
+ const cFetchEffectJson: <T>(
149
+ url: string | URL,
150
+ options?: RequestInit | undefined,
151
+ cacheConfig?: CFetchConfig | undefined
152
+ ) => Effect.Effect<CachedResponse<T>, FetchError, never>
153
+ ```
154
+
155
+ ###### Example Usage
156
+
157
+ ```ts
158
+ import { cFetchEffectJson } from "c:fetch"
159
+
160
+ const effect = cFetchEffectJson<{ foo: string; bar: number; }>(
161
+ 'https://api.example.com/data',
162
+ { method: "GET" }
163
+ );
164
+ /*
165
+ Return type:
166
+ Effect.Effect<CachedResponse<{ foo: string; bar: number; }>, FetchError, never>
167
+ */
168
+ ```
169
+
170
+ ##### `cFetchEffectText`
171
+
172
+ ###### Interface
173
+
174
+ ```ts
175
+ const cFetchEffectText: (
176
+ url: string | URL,
177
+ options?: RequestInit | undefined,
178
+ cacheConfig?: CFetchConfig | undefined
179
+ ) => Effect.Effect<CachedResponse<string>, FetchError, never>
180
+ ```
181
+
182
+ ###### Example Usage
65
183
 
66
- ```astro
67
- ---
68
- import { cFetch } from 'c:fetch';
184
+ ```ts
185
+ import { cFetchEffectText } from "c:fetch"
69
186
 
70
- const response = await cFetch(
71
- 'https://example.com', // string | URL | Request
72
- { /* Normal fetch init optional options here, method, mode, etc. */ },
73
- { lifetime: "1h" }, // Optional, allows changing the default lifetime of the cache
74
- 'json', // Optional, allows changing the type of response object to be cached. 'json' (default) or 'text'
187
+ const effect = cFetchEffectText(
188
+ 'https://example.com',
189
+ { method: "GET" }
75
190
  );
191
+ /*
192
+ Return type:
193
+ Effect.Effect<CachedResponse<string>, FetchError, never>
194
+ */
195
+ ```
196
+
197
+ ##### `cFetchEffectBlob`
198
+
199
+ ###### Interface
200
+
201
+ ```ts
202
+ const cFetchEffectBlob: (
203
+ url: string | URL,
204
+ options?: RequestInit | undefined,
205
+ cacheConfig?: CFetchConfig | undefined
206
+ ) => Effect.Effect<CachedResponse<Blob>, FetchError, never>
207
+ ```
208
+
209
+ ###### Example Usage
210
+
211
+ ```ts
212
+ import { cFetchEffectBlob } from "c:fetch"
213
+
214
+ const effect = cFetchEffectBlob(
215
+ 'https://example.com/image.png',
216
+ { method: "GET" }
217
+ );
218
+ /*
219
+ Return type:
220
+ Effect.Effect<CachedResponse<Blob>, FetchError, never>
221
+ */
222
+ ```
76
223
 
77
- const html = await response.text();
78
- ---
224
+ #### Functions
225
+
226
+ All Functions have the following return pattern or derivatives there of
227
+
228
+ ```ts
229
+ CachedResponse<T>;
230
+ ```
231
+
232
+ ##### `cFetch`
233
+
234
+ ###### Interface
235
+
236
+ ```ts
237
+ const cFetch: <T>(
238
+ url: string | URL,
239
+ parser: (response: Response) => Promise<T>,
240
+ options?: RequestInit | undefined,
241
+ cacheConfig?: CFetchConfig | undefined
242
+ ) => Promise<CachedResponse<T>>
79
243
  ```
80
244
 
81
- If you need to access the other available metadata (such as the `lastChecked` value which provides the last time the cache was updated), you can pass `true` as the fourth parameter, which will change the returned object to the following:
245
+ ###### Example Usage
82
246
 
83
- ```astro
84
- ---
85
- import { cFetch } from 'c:fetch';
247
+ ```ts
248
+ import { cFetch } from "c:fetch"
86
249
 
87
- const { lastCheck, data } = await cFetch(
88
- 'https://example.com',
89
- { /* ... */ },
90
- { lifetime: "1h" },
91
- 'json',
92
- true // Changes the the output to include the lastCheck value
250
+ const effect = await cFetch<{ foo: string; bar: number; }>(
251
+ 'https://api.example.com/data',
252
+ (res) => res.json(),
253
+ { method: "GET" }
93
254
  );
255
+ /*
256
+ Return type:
257
+ CachedResponse<{ foo: string; bar: number; }>
258
+ */
259
+ ```
260
+
261
+ ##### `cFetchJson`
262
+
263
+ ###### Interface
264
+
265
+ ```ts
266
+ const cFetchJson: <T>(
267
+ url: string | URL,
268
+ options?: RequestInit | undefined,
269
+ cacheConfig?: CFetchConfig | undefined
270
+ ) => Promise<CachedResponse<T>>
271
+ ```
94
272
 
95
- const html = await data.text();
96
- ---
273
+ ###### Example Usage
274
+
275
+ ```ts
276
+ import { cFetchJson } from "c:fetch"
277
+
278
+ const effect = await cFetchJson<{ foo: string; bar: number; }>(
279
+ 'https://api.example.com/data',
280
+ { method: "GET" }
281
+ );
282
+ /*
283
+ Return type:
284
+ CachedResponse<{ foo: string; bar: number; }>
285
+ */
286
+ ```
287
+
288
+ ##### `cFetchText`
289
+
290
+ ###### Interface
291
+
292
+ ```ts
293
+ const cFetchText: (
294
+ url: string | URL,
295
+ options?: RequestInit | undefined,
296
+ cacheConfig?: CFetchConfig | undefined
297
+ ) => Promise<CachedResponse<string>>
298
+ ```
299
+
300
+ ###### Example Usage
301
+
302
+ ```ts
303
+ import { cFetchText } from "c:fetch"
304
+
305
+ const effect = await cFetchText(
306
+ 'https://example.com',
307
+ { method: "GET" }
308
+ );
309
+ /*
310
+ Return type:
311
+ CachedResponse<string>
312
+ */
313
+ ```
314
+
315
+ ##### `cFetchBlob`
316
+
317
+ ###### Interface
318
+
319
+ ```ts
320
+ const cFetchBlob: (
321
+ url: string | URL,
322
+ options?: RequestInit | undefined,
323
+ cacheConfig?: CFetchConfig | undefined
324
+ ) => Promise<CachedResponse<Blob>>
325
+ ```
326
+
327
+ ###### Example Usage
328
+
329
+ ```ts
330
+ import { cFetchBlob } from "c:fetch"
331
+
332
+ const effect = await cFetchBlob(
333
+ 'https://example.com/image.png',
334
+ { method: "GET" }
335
+ );
336
+ /*
337
+ Return type:
338
+ CachedResponse<Blob>
339
+ */
97
340
  ```
98
341
 
99
342
  ## Licensing
@@ -0,0 +1,78 @@
1
+ /**
2
+ * @module cfetch/cache
3
+ * Cache service implementation using Effect-TS.
4
+ */
5
+ import { Context, Duration, Effect } from 'effect';
6
+ /**
7
+ * Represents a cache entry with its value, expiration time, last updated time, and tags.
8
+ */
9
+ export interface CacheEntry<A> {
10
+ value: A;
11
+ expiresAt: number;
12
+ lastUpdatedAt: number;
13
+ tags: Set<string>;
14
+ }
15
+ /**
16
+ * Represents the status of a cache entry, including expiration and tags.
17
+ */
18
+ export interface CacheEntryStatus {
19
+ expiresAt: Date;
20
+ lastUpdatedAt: Date;
21
+ tags: Set<string>;
22
+ }
23
+ declare const CacheMaps_base: Context.TagClass<CacheMaps, "@studiocms/cfetch/CacheMaps", {
24
+ store: Map<string, CacheEntry<unknown>>;
25
+ tagIndex: Map<string, Set<string>>;
26
+ }>;
27
+ /**
28
+ * Tag to hold the in-memory cache maps.
29
+ *
30
+ * This tag provides access to the main cache store and the tag index for invalidation.
31
+ */
32
+ export declare class CacheMaps extends CacheMaps_base {
33
+ }
34
+ declare const CacheService_base: Effect.Service.Class<CacheService, "@studiocms/cfetch/CacheService", {
35
+ readonly effect: Effect.Effect<{
36
+ get: <A>(key: string) => Effect.Effect<A | null, never, never>;
37
+ set: <A>(key: string, value: A, options?: {
38
+ ttl?: Duration.DurationInput;
39
+ tags?: string[];
40
+ }) => Effect.Effect<void, never, never>;
41
+ delete: (key: string) => Effect.Effect<void, never, never>;
42
+ invalidateTags: (tags: string[]) => Effect.Effect<void, never, never>;
43
+ clear: () => Effect.Effect<void, never, never>;
44
+ }, never, CacheMaps>;
45
+ }>;
46
+ /**
47
+ * A service for managing cached data with TTL (time-to-live) and tag-based invalidation.
48
+ *
49
+ * @remarks
50
+ * This service provides an in-memory cache with the following features:
51
+ * - Automatic expiration based on TTL
52
+ * - Tag-based organization for batch invalidation
53
+ * - Effect-based API for safe side-effect management
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * const program = Effect.gen(function* () {
58
+ * const cache = yield* CacheService;
59
+ *
60
+ * // Set a value with custom TTL and tags
61
+ * yield* cache.set('user:123', userData, {
62
+ * ttl: Duration.minutes(5),
63
+ * tags: ['user', 'profile']
64
+ * });
65
+ *
66
+ * // Get a value
67
+ * const result = yield* cache.get('user:123');
68
+ *
69
+ * // Invalidate all entries with specific tags
70
+ * yield* cache.invalidateTags(['user']);
71
+ * });
72
+ * ```
73
+ *
74
+ * @public
75
+ */
76
+ export declare class CacheService extends CacheService_base {
77
+ }
78
+ export {};
package/dist/cache.js ADDED
@@ -0,0 +1,89 @@
1
+ import { Clock, Context, Duration, Effect } from "effect";
2
+ import { defaultConfigLive } from "./consts";
3
+ class CacheMaps extends Context.Tag("@studiocms/cfetch/CacheMaps")() {
4
+ }
5
+ class ConfigFetchError {
6
+ _tag = "ConfigFetchError";
7
+ }
8
+ const getConfig = async () => {
9
+ try {
10
+ const config = await import("virtual:cfetch/config");
11
+ return config.default;
12
+ } catch (error) {
13
+ console.warn("Could not load virtual:cfetch/config, using default config.");
14
+ return defaultConfigLive;
15
+ }
16
+ };
17
+ class CacheService extends Effect.Service()("@studiocms/cfetch/CacheService", {
18
+ effect: Effect.gen(function* () {
19
+ const { store, tagIndex } = yield* CacheMaps;
20
+ const config = yield* Effect.tryPromise({
21
+ try: () => getConfig(),
22
+ catch: () => new ConfigFetchError()
23
+ }).pipe(Effect.catchTag("ConfigFetchError", () => Effect.succeed(defaultConfigLive)));
24
+ const get = (key) => Effect.gen(function* () {
25
+ const now = yield* Clock.currentTimeMillis;
26
+ const entry = store.get(key);
27
+ if (!entry) return null;
28
+ if (entry.expiresAt < now) {
29
+ yield* deleteKey(key);
30
+ return null;
31
+ }
32
+ return entry.value;
33
+ });
34
+ const set = (key, value, options) => Effect.gen(function* () {
35
+ const now = yield* Clock.currentTimeMillis;
36
+ const ttl = options?.ttl ?? Duration.millis(config.lifetime);
37
+ const tags = new Set(options?.tags ?? []);
38
+ const expiresAt = now + Duration.toMillis(ttl);
39
+ store.set(key, { value, expiresAt, lastUpdatedAt: now, tags });
40
+ for (const tag of tags) {
41
+ if (!tagIndex.has(tag)) {
42
+ tagIndex.set(tag, /* @__PURE__ */ new Set());
43
+ }
44
+ tagIndex.get(tag)?.add(key);
45
+ }
46
+ });
47
+ const deleteKey = (key) => Effect.sync(() => {
48
+ const entry = store.get(key);
49
+ if (entry) {
50
+ for (const tag of entry.tags) {
51
+ const keys = tagIndex.get(tag);
52
+ if (keys) {
53
+ keys.delete(key);
54
+ if (keys.size === 0) {
55
+ tagIndex.delete(tag);
56
+ }
57
+ }
58
+ }
59
+ store.delete(key);
60
+ }
61
+ });
62
+ const invalidateTags = (tags) => Effect.gen(function* () {
63
+ for (const tag of tags) {
64
+ const keys = tagIndex.get(tag);
65
+ if (keys) {
66
+ for (const key of [...keys]) {
67
+ yield* deleteKey(key);
68
+ }
69
+ }
70
+ }
71
+ });
72
+ const clear = () => Effect.sync(() => {
73
+ store.clear();
74
+ tagIndex.clear();
75
+ });
76
+ return {
77
+ get,
78
+ set,
79
+ delete: deleteKey,
80
+ invalidateTags,
81
+ clear
82
+ };
83
+ })
84
+ }) {
85
+ }
86
+ export {
87
+ CacheMaps,
88
+ CacheService
89
+ };
package/dist/consts.d.ts CHANGED
@@ -1,12 +1,8 @@
1
1
  /**
2
- * This module contains constants for cFetch
3
- * @module
4
- */
5
- import type { CacheConfig } from './types.js';
6
- /**
7
- * Default cache configuration object.
2
+ * @module cfetch/consts
8
3
  *
9
- * @property {string} lifetime - The duration for which the cache is valid.
10
- * Accepts a string representation of time, e.g., '1h' for 1 hour.
4
+ * Constant values used throughout the cfetch package.
11
5
  */
6
+ import type { CacheConfig, CacheConfigLive } from './types.js';
12
7
  export declare const defaultConfig: CacheConfig;
8
+ export declare const defaultConfigLive: CacheConfigLive;
package/dist/consts.js CHANGED
@@ -1,6 +1,11 @@
1
+ import { Duration } from "effect";
1
2
  const defaultConfig = {
2
- lifetime: "1h"
3
+ lifetime: Duration.hours(1)
4
+ };
5
+ const defaultConfigLive = {
6
+ lifetime: Duration.toMillis(Duration.hours(1))
3
7
  };
4
8
  export {
5
- defaultConfig
9
+ defaultConfig,
10
+ defaultConfigLive
6
11
  };
package/dist/index.d.ts CHANGED
@@ -1,38 +1,65 @@
1
1
  /**
2
- * This module contains the AstroIntegration for cFetch
3
- * @module
4
- */
5
- import type { AstroIntegration } from 'astro';
6
- import type { CacheConfig } from './types.js';
7
- /**
8
- * Astro integration that allows you to have a cached fetch function in your Astro SSR project.
2
+ * @module @studiocms/cfetch
9
3
  *
10
- * This integration will add a virtual import for `cached:fetch` that you can use in your components.
4
+ * An Astro integration that provides a caching fetch utility using Effect.
11
5
  *
12
- * @returns {AstroIntegration} The integration object for Astro
13
- * @see {@link https://github.com/withstudiocms/cfetch} for more details
14
- * @example
15
- * ```typescript
6
+ * This module exports the `cFetch` function, which can be used to create an Astro
7
+ * integration that adds virtual modules for cached fetching capabilities. It also
8
+ * exports the `Duration` type from Effect for specifying cache lifetimes.
9
+ *
10
+ * The `cFetch` integration injects virtual modules:
11
+ * - `virtual:cfetch/config`: Exports the default cache configuration.
12
+ * - `c:fetch`: Exports various cached fetch functions and types.
13
+ *
14
+ * Example usage:
15
+ *
16
+ * ```ts
17
+ * import cFetch, { Duration } from '@studiocms/cfetch';
16
18
  * import { defineConfig } from 'astro/config';
17
- * import cFetch from '@studiocms/cfetch';
18
19
  *
19
20
  * export default defineConfig({
20
21
  * integrations: [
21
22
  * cFetch({
22
- * lifetime: '1h', // OPTIONAL Cache lifetime, can be '<number>m' or '<number>h'
23
- * })
23
+ * lifetime: Duration.minutes(5), // Set cache lifetime to 5 minutes (default is 1 hour)
24
+ * }),
24
25
  * ],
25
26
  * });
26
27
  * ```
27
28
  *
28
- * then in your components you can use:
29
+ * You can then use the cached fetch functions in your Astro components or pages:
29
30
  *
30
- * ```typescript
31
+ * ```ts
31
32
  * import { cFetch } from 'c:fetch';
32
33
  *
33
- * const response = await cFetch('https://example.com/api/data');
34
- * const data = await response.json();
35
- * // Use the data in your component
34
+ * const response = await cFetch('https://api.example.com/data', (res) => res.json());
35
+ * console.log(response.data);
36
+ * ```
37
+ */
38
+ import type { AstroIntegration } from 'astro';
39
+ import type { CacheConfig } from './types.js';
40
+ export { Duration } from 'effect';
41
+ /**
42
+ * Creates a caching fetch integration for Astro.
43
+ *
44
+ * This integration provides a cached fetch implementation that can be configured
45
+ * with custom cache lifetime and other options. It sets up virtual module imports
46
+ * and injects TypeScript type definitions for the cached fetch functionality.
47
+ *
48
+ * @param opts - Optional cache configuration options to customize the caching behavior
49
+ * @returns An Astro integration object with hooks for configuration setup and completion
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * // astro.config.mjs
54
+ * import cFetch, { Duration } from '@studiocms/cfetch';
55
+ *
56
+ * export default defineConfig({
57
+ * integrations: [
58
+ * cFetch({
59
+ * lifetime: Duration.minutes(10), // Cache entries live for 10 minutes (default is 1 hour)
60
+ * })
61
+ * ]
62
+ * });
36
63
  * ```
37
64
  */
38
65
  export declare function cFetch(opts?: CacheConfig): AstroIntegration;
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
+ import { Duration } from "effect";
1
2
  import { defaultConfig } from "./consts.js";
2
3
  import stub from "./stub.js";
3
4
  import { addVirtualImports, createResolver } from "./utils/integration.js";
5
+ import { Duration as Duration2 } from "effect";
4
6
  function cFetch(opts) {
5
7
  const name = "@studiocms/cfetch";
6
8
  const { resolve } = createResolver(import.meta.url);
@@ -15,7 +17,10 @@ function cFetch(opts) {
15
17
  addVirtualImports(params, {
16
18
  name,
17
19
  imports: {
18
- "virtual:cfetch/config": `export default ${JSON.stringify(options)}`,
20
+ "virtual:cfetch/config": `export default ${JSON.stringify({
21
+ // Convert Duration.DurationInput to milliseconds number (required to preserve value through JSON.stringify)
22
+ lifetime: Duration.toMillis(options.lifetime)
23
+ })}`,
19
24
  "c:fetch": `export * from '${resolve("./wrappers.js")}';`
20
25
  }
21
26
  });
@@ -31,6 +36,7 @@ function cFetch(opts) {
31
36
  }
32
37
  var index_default = cFetch;
33
38
  export {
39
+ Duration2 as Duration,
34
40
  cFetch,
35
41
  index_default as default
36
42
  };