@vobs/resource 0.3.0 → 1.0.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 vobsjs
3
+ Copyright (c) 2026 vobs contributors
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # @vobs/resource
2
+
3
+ Signal-backed async data for vobs: keyed request caching, stale-while-revalidate, revision-guarded mutations, and SSR dehydration.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @vobs/resource
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```ts
14
+ import { createResourceClient } from '@vobs/resource'
15
+
16
+ const client = createResourceClient({ staleTime: 30_000, retry: 2 })
17
+
18
+ const users = client.resource({
19
+ key: ['users', { page: 1 }],
20
+ fetcher: signal => fetch('/api/users?page=1', { signal }).then(res => res.json()),
21
+ strategy: 'stale-while-revalidate'
22
+ })
23
+
24
+ users.loading.value // true while the first request is in flight
25
+ users.data.value // T | null once settled
26
+ users.error.value // Error | null on failure
27
+
28
+ await users.refetch()
29
+ client.invalidate(['users', { page: 1 }])
30
+ ```
31
+
32
+ Resources created with the same serialized key share one cache entry and one in-flight request. Results carry a revision: a response that settles after `mutate` or `optimistic` is discarded, so late data never overwrites newer local writes. With `stale-while-revalidate`, cached data is returned immediately while a fresh request runs in the background; `cache-first` re-fetches only after `staleTime` expires.
33
+
34
+ ## API
35
+
36
+ | Signature | Description |
37
+ | --- | --- |
38
+ | `createResourceClient(options?: ResourceClientOptions): ResourceClient` | Create a client; options set default `staleTime`, `retry`, `retryDelay`, and `onError`. |
39
+ | `resource(fetcher or options): Resource` | Create a resource on the module-level default client. |
40
+ | `client.resource(options): Resource` | Pass `key` to enable caching (also accepts a signal or function key for reactive re-fetch); `cache: false` opts out. |
41
+ | `resource.data / error / loading` | Signals for current value, error, and request state. |
42
+ | `resource.refetch() / prefetch() / invalidate()` | Force a request, start one opportunistically, or expire the cache. |
43
+ | `resource.execute()` | Runs the fetcher now and returns the result; revision-guarded like every settle path. |
44
+ | `resource.mutate(next)` | Write through the shared cache immediately. |
45
+ | `resource.optimistic(next, action)` | Apply `next`, then roll back and set `error` if `action` rejects. |
46
+ | `resource.dispose()` | Detach; aborts the request when it is the last subscriber. |
47
+ | `client.invalidate(key) / client.get(key)` | Expire or read a cached snapshot by key. |
48
+ | `client.prefetchAll() / dehydrate() / hydrate(snapshot) / clear()` | Await in-flight work, serialize for SSR, restore, and reset. |
49
+ | `stableSerialize(value): string` | Deterministic serialization used for cache keys. |
50
+ | `serializeResourceState(snapshot): string` | JSON-stringify dehydrated state with HTML-safe escaping. |
51
+ | `resourcePlugin(options?) / resourceRouterPlugin(options?)` | Provide the client as `RESOURCE_KEY`; run `route.meta.prefetch` handlers before navigation. |
52
+ | `insertResourceBoundary(parent, anchor, options) / ResourceBoundary(props)` | Branch on loading, empty, error, and data of a resource. |
53
+
54
+ ## Types
55
+
56
+ Resource, ResourceKey, ResourceKeySource, ResourceFetcher, ResourceOptions, ResourceCacheStrategy, RetryDelay, ResourceSnapshot, ResourceClient, ResourceClientOptions, ResourceDehydratedEntry, ResourceDehydratedState, ResourceBoundaryChild, ResourceBoundaryFallback, ResourceBoundaryOptions, ResourceBoundaryProps, ResourceBoundaryView, ResourcePluginOptions, ResourceRoutePrefetch, ResourceRoutePrefetchContext, ResourceRouterPluginOptions
package/package.json CHANGED
@@ -1,41 +1,22 @@
1
1
  {
2
- "name": "@vobs/resource",
3
- "version": "0.3.0",
4
- "description": "Declarative async data resources for vobs: fetch, cancel, cache, invalidate, refetch and mutation.",
5
- "type": "module",
6
- "publishConfig": {
7
- "access": "public"
8
- },
9
2
  "license": "MIT",
10
- "author": "vobsjs",
11
- "repository": {
12
- "type": "git",
13
- "url": "git+https://github.com/vobsjs/vobs.git",
14
- "directory": "packages/features/resource"
15
- },
16
- "bugs": {
17
- "url": "https://github.com/vobsjs/vobs/issues"
18
- },
19
- "homepage": "https://github.com/vobsjs/vobs#readme",
20
- "dependencies": {
21
- "@vobs/reactivity": "0.3.0",
22
- "@vobs/runtime-core": "0.3.0"
23
- },
24
3
  "files": [
25
- "dist"
4
+ "src",
5
+ "README.md",
6
+ "LICENSE"
26
7
  ],
8
+ "name": "@vobs/resource",
9
+ "version": "1.0.0",
10
+ "type": "module",
11
+ "main": "src/index.ts",
12
+ "types": "src/index.ts",
27
13
  "exports": {
28
- ".": {
29
- "types": "./dist/index.d.ts",
30
- "import": "./dist/index.js"
31
- },
32
- "./package.json": "./package.json"
14
+ ".": "./src/index.ts"
33
15
  },
34
- "types": "./dist/index.d.ts",
35
- "module": "./dist/index.js",
36
- "main": "./dist/index.js",
37
- "sideEffects": false,
38
- "engines": {
39
- "node": ">=22.12.0"
16
+ "dependencies": {
17
+ "@vobs/reactivity": "1.0.0",
18
+ "@vobs/router": "1.0.0",
19
+ "@vobs/runtime": "1.0.0",
20
+ "@vobs/vobs": "1.0.0"
40
21
  }
41
22
  }
@@ -0,0 +1,52 @@
1
+ import { createFragment, insertBoundary, type NodeFactory, type VobsNode } from '@vobs/vobs'
2
+ import type { Resource } from './resource'
3
+
4
+ export type ResourceBoundaryChild<T> = (data: T) => ReturnType<NodeFactory>
5
+ export type ResourceBoundaryView = ReturnType<NodeFactory> | NodeFactory
6
+ export type ResourceBoundaryFallback = (
7
+ error: Error,
8
+ retry: () => Promise<unknown>
9
+ ) => ReturnType<NodeFactory>
10
+
11
+ export interface ResourceBoundaryOptions<T> {
12
+ resource: Resource<T>
13
+ children: ResourceBoundaryChild<T>
14
+ loading?: ResourceBoundaryView
15
+ empty?: ResourceBoundaryView
16
+ fallback?: ResourceBoundaryFallback
17
+ }
18
+
19
+ export interface ResourceBoundaryProps<T> extends ResourceBoundaryOptions<T> {}
20
+
21
+ /**
22
+ * Inserts a reactive Resource state branch. The compiler can lower a future
23
+ * ResourceBoundary JSX element to this host-level instruction without a wrapper node.
24
+ */
25
+ export function insertResourceBoundary<T>(
26
+ parent: Node,
27
+ anchor: Node | null,
28
+ options: ResourceBoundaryOptions<T>
29
+ ): void {
30
+ insertBoundary(parent, anchor, {
31
+ onRetry: () => options.resource.refetch(),
32
+ fallback: (error, retry) => options.fallback?.(error, () => Promise.resolve(retry())) ?? null,
33
+ children: () => {
34
+ if (options.resource.error.value) throw options.resource.error.value
35
+ if (options.resource.loading.value) return resolveView(options.loading)
36
+
37
+ const data = options.resource.data.value
38
+ if (data === null) return resolveView(options.empty)
39
+ return options.children(data)
40
+ }
41
+ })
42
+ }
43
+
44
+ /** Component-shaped API backed by the same no-wrapper boundary instruction. */
45
+ export function ResourceBoundary<T>(props: ResourceBoundaryProps<T>): VobsNode {
46
+ return createFragment((parent, anchor) => insertResourceBoundary(parent, anchor, props))
47
+ }
48
+
49
+ function resolveView(view: ResourceBoundaryView | undefined): ReturnType<NodeFactory> {
50
+ if (!view) return null
51
+ return typeof view === 'function' ? view() : view
52
+ }
package/src/index.ts ADDED
@@ -0,0 +1,35 @@
1
+ export {
2
+ createResourceClient,
3
+ resource,
4
+ stableSerialize,
5
+ serializeResourceState
6
+ } from './resource'
7
+ export { insertResourceBoundary, ResourceBoundary } from './boundary'
8
+ export type {
9
+ Resource,
10
+ ResourceCacheStrategy,
11
+ ResourceClient,
12
+ ResourceClientOptions,
13
+ ResourceDehydratedEntry,
14
+ ResourceDehydratedState,
15
+ ResourceFetcher,
16
+ ResourceKey,
17
+ ResourceKeySource,
18
+ ResourceOptions,
19
+ RetryDelay,
20
+ ResourceSnapshot
21
+ } from './resource'
22
+ export type {
23
+ ResourceBoundaryChild,
24
+ ResourceBoundaryFallback,
25
+ ResourceBoundaryOptions,
26
+ ResourceBoundaryProps,
27
+ ResourceBoundaryView
28
+ } from './boundary'
29
+ export { RESOURCE_KEY, resourcePlugin, resourceRouterPlugin } from './plugin'
30
+ export type {
31
+ ResourcePluginOptions,
32
+ ResourceRoutePrefetchContext,
33
+ ResourceRoutePrefetch,
34
+ ResourceRouterPluginOptions
35
+ } from './plugin'
package/src/plugin.ts ADDED
@@ -0,0 +1,70 @@
1
+ import { createInjectionKey, type VobsPlugin } from '@vobs/vobs'
2
+ import { ROUTER_KEY, type RouteLocation, type Router } from '@vobs/router'
3
+ import { createResourceClient, type ResourceClient, type ResourceClientOptions } from './resource'
4
+
5
+ export const RESOURCE_KEY = createInjectionKey<ResourceClient>('vobs.resource')
6
+
7
+ export interface ResourcePluginOptions extends ResourceClientOptions {
8
+ client?: ResourceClient
9
+ }
10
+
11
+ export interface ResourceRoutePrefetchContext {
12
+ readonly route: RouteLocation
13
+ readonly client: ResourceClient
14
+ }
15
+
16
+ export type ResourceRoutePrefetch = (
17
+ context: ResourceRoutePrefetchContext
18
+ ) => unknown | PromiseLike<unknown>
19
+
20
+ export interface ResourceRouterPluginOptions {
21
+ readonly router?: Router
22
+ readonly client?: ResourceClient
23
+ }
24
+
25
+ export function resourcePlugin(options: ResourcePluginOptions = {}): VobsPlugin {
26
+ return {
27
+ name: '@vobs/resource',
28
+ version: '0.1.0',
29
+ install(context) {
30
+ const client = options.client ?? createResourceClient(options)
31
+ context.provide(RESOURCE_KEY, client)
32
+ return () => {
33
+ if (!options.client) client.clear()
34
+ }
35
+ }
36
+ }
37
+ }
38
+
39
+ export function resourceRouterPlugin(options: ResourceRouterPluginOptions = {}): VobsPlugin {
40
+ return {
41
+ name: '@vobs/resource-router',
42
+ version: '0.1.0',
43
+ install(context) {
44
+ const router = options.router ?? context.inject(ROUTER_KEY)
45
+ const client = options.client ?? context.inject(RESOURCE_KEY)
46
+ if (!router) throw new Error('Vobs Resource Router: 找不到 Router,请安装 routerPlugin 或传入 router')
47
+ if (!client) throw new Error('Vobs Resource Router: 找不到 ResourceClient,请安装 resourcePlugin 或传入 client')
48
+
49
+ return router.beforeEach(async to => {
50
+ const prefetch = readPrefetch(to)
51
+ await Promise.all(prefetch.map((task, index) => router.devtools.trackDataRequest(
52
+ 'loader',
53
+ `${to.fullPath}#prefetch-${index + 1}`,
54
+ () => task({ route: to, client }),
55
+ { route: to.fullPath, trigger: 'resource' }
56
+ )))
57
+ })
58
+ }
59
+ }
60
+ }
61
+
62
+ function readPrefetch(route: RouteLocation): readonly ResourceRoutePrefetch[] {
63
+ const value = route.meta.prefetch
64
+ if (value === undefined) return []
65
+ if (typeof value === 'function') return [value as ResourceRoutePrefetch]
66
+ if (!Array.isArray(value) || value.some(task => typeof task !== 'function')) {
67
+ throw new Error('Vobs Resource Router: route.meta.prefetch 必须是函数或函数数组')
68
+ }
69
+ return value as ResourceRoutePrefetch[]
70
+ }