@joycostudio/susano 0.1.2 → 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/README.md +337 -112
- package/dist/index.d.mts +164 -83
- package/dist/index.d.ts +164 -83
- package/dist/index.js +216 -124
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +216 -125
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -1,211 +1,436 @@
|
|
|
1
|
-
# <img src="./static/JOYCO.png" alt="JOYCO Logo" height="36" width="36" align="top" />
|
|
1
|
+
# <img src="./static/JOYCO.png" alt="JOYCO Logo" height="36" width="36" align="top" /> Susano
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@joycostudio/susano)
|
|
4
|
+
[](https://bundlejs.com/?q=%40joycostudio%2Fsusano&treeshake=%5B*%5D)
|
|
5
|
+
|
|
6
|
+
Asset load orchestration made easy. A typed, promise-friendly loader for images, video, audio, and arbitrary data — with built-in progress tracking, per-call postprocessing, and pluggable loader types.
|
|
7
|
+
|
|
8
|
+
## Mental model
|
|
9
|
+
|
|
10
|
+
Susano separates two things that are usually conflated:
|
|
11
|
+
|
|
12
|
+
- **Content** — the expensive fetch (`HTMLImageElement`, decoded buffer, parsed JSON). **Cached and deduped per URL.** Loaded once, no matter how many call sites ask for it.
|
|
13
|
+
- **Result** — what *your* call wants out of that content, via an optional `postprocess`. **Per call, never cached.** Two call sites can derive different results from the same shared content.
|
|
14
|
+
|
|
15
|
+
So `load()` returns a **`Promise<R>`** of your call's result, and `batch()` returns a **`Batch`** handle that tracks a fixed set. Reach the shared content loader (raw content, events) via `susano.get(url)`. Want to cache a transformed result too? Encode the transform in a custom loader, so its output *becomes* the content.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
| Feature | Description |
|
|
20
|
+
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
21
|
+
| **Scoped batches** | `batch([...])` loads a fixed set and tracks **only** that set's progress/completion. Auto-starts; returns a handle with `.promise` / `.progress` / `.completed`. |
|
|
22
|
+
| **Content dedup** | One content fetch per URL across every `load()` / `batch()` — in-flight or already-loaded assets are joined, never re-fetched. |
|
|
23
|
+
| **Per-call postprocess** | `postprocess` is your call's projection of the shared content. It always runs for your call; results aren't cached. |
|
|
24
|
+
| **Built-in loaders** | First-class `image`, `video`, `audio`, and `generic` out of the box. |
|
|
25
|
+
| **Pluggable types** | Pass a `{ [type]: LoaderClass }` map to `new Susano(...)` — the keys become the valid `type`s, fully inferred for `load()` / `batch()`. |
|
|
26
|
+
| **Stable promises** | `load()` resolves with your result; every `Batch` exposes a `.promise`. Await one result or a whole set. |
|
|
27
|
+
| **Typed manifest** | TypeScript knows which `loaderArgs` and `postprocess` content type are valid for each registered `type`. |
|
|
28
|
+
|
|
29
|
+
## Install
|
|
4
30
|
|
|
5
31
|
```bash
|
|
6
32
|
pnpm add @joycostudio/susano
|
|
7
33
|
```
|
|
8
34
|
|
|
9
|
-
## Quick
|
|
35
|
+
## Quick start
|
|
10
36
|
|
|
11
37
|
```ts
|
|
12
38
|
import { susano } from '@joycostudio/susano'
|
|
13
39
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
40
|
+
const batch = susano.batch(
|
|
41
|
+
[
|
|
42
|
+
{ url: '/hero.png', type: 'image' },
|
|
43
|
+
{ url: '/intro.mp4', type: 'video' },
|
|
44
|
+
{ url: '/music.mp3', type: 'audio' },
|
|
45
|
+
],
|
|
46
|
+
{
|
|
47
|
+
onProgress: ({ value }) => console.log(`${Math.round(value * 100)}%`),
|
|
48
|
+
onCompleted: () => console.log('All assets loaded!'),
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
await batch.promise
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Quick start (React)
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import { useEffect, useState } from 'react'
|
|
59
|
+
import { susano, type BatchProgressEventArgs } from '@joycostudio/susano'
|
|
60
|
+
|
|
61
|
+
function App() {
|
|
62
|
+
const [progress, setProgress] = useState(0)
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
susano.batch(
|
|
66
|
+
[
|
|
67
|
+
{ url: '/hero.png', type: 'image' },
|
|
68
|
+
{ url: '/intro.mp4', type: 'video' },
|
|
69
|
+
],
|
|
70
|
+
{ onProgress: ({ value }: BatchProgressEventArgs) => setProgress(value) }
|
|
71
|
+
)
|
|
72
|
+
}, [])
|
|
73
|
+
|
|
74
|
+
return <div>Loading: {Math.round(progress * 100)}%</div>
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Architecture
|
|
81
|
+
|
|
82
|
+
| Layer | Responsibility |
|
|
83
|
+
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
84
|
+
| **Susano** | Registry of loader classes keyed by `type`, plus the `items` cache (one content loader per URL). Exposes `load()`, `batch()`, `get()`. Its private per-call projection ensures content, then applies that call's `postprocess`. |
|
|
85
|
+
| **SusanoLoader** | Caches one piece of raw **content** per URL. Idempotent `load()`, tracks `status` / `progress`, emits `loaded` / `progress` / `error`. Does **not** postprocess. |
|
|
86
|
+
| **Batch** | Tracks progress + completion for a fixed set of loads. Owns its own counter and completion promise — concurrent batches never interfere. |
|
|
87
|
+
| **Loader types** | `ImageLoader`, `VideoLoader`, `AudioLoader`, `GenericLoader` — each wraps a native element or a custom `loadFn`. |
|
|
88
|
+
|
|
89
|
+
> Completion is the resolution of a `Promise.all` over the batch — one-shot by construction. A failed load is *fail-soft*: it surfaces via `onError` and still advances the batch so a load screen can finish.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Default instance
|
|
94
|
+
|
|
95
|
+
The package ships a pre-configured `susano` singleton with `image`, `video`, `audio`, and `generic` loaders registered. For most apps this is the only instance you need.
|
|
17
96
|
|
|
18
|
-
|
|
19
|
-
susano
|
|
20
|
-
|
|
21
|
-
|
|
97
|
+
```ts
|
|
98
|
+
import { susano } from '@joycostudio/susano'
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Need a second, isolated registry? Construct your own — the loader map is the single source of truth, and `load()` / `batch()` are fully inferred from it:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { Susano, ImageLoader, VideoLoader, AudioLoader, GenericLoader } from '@joycostudio/susano'
|
|
105
|
+
|
|
106
|
+
const s = new Susano({
|
|
107
|
+
image: ImageLoader,
|
|
108
|
+
video: VideoLoader,
|
|
109
|
+
audio: AudioLoader,
|
|
110
|
+
generic: GenericLoader,
|
|
22
111
|
})
|
|
112
|
+
|
|
113
|
+
s.load('/x.png', { type: 'image' }) // `type` is 'image' | 'video' | 'audio' | 'generic'
|
|
23
114
|
```
|
|
24
115
|
|
|
25
|
-
|
|
116
|
+
Add your own loader by adding a key — no separate type map, no `registerLoader`, nothing to keep in sync:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
const s = new Susano({ image: ImageLoader, json: JSONLoader })
|
|
120
|
+
// ^ adds 'json' as a valid type, fully typed
|
|
121
|
+
```
|
|
26
122
|
|
|
27
|
-
|
|
123
|
+
---
|
|
28
124
|
|
|
29
|
-
|
|
125
|
+
## Core API
|
|
30
126
|
|
|
31
|
-
###
|
|
127
|
+
### Susano
|
|
32
128
|
|
|
33
|
-
|
|
129
|
+
`new Susano(loaders)` — `loaders` is a `{ [type]: LoaderClass }` map. Its keys become the valid `type`s; each loader's content/`loaderArgs` types are inferred per key.
|
|
130
|
+
|
|
131
|
+
| Member | Description |
|
|
132
|
+
| ------------------------------- | ------------------------------------------------------------------------- |
|
|
133
|
+
| `load(url, cnfg)` | Load one asset; returns a `Promise<R>` for this call's result. |
|
|
134
|
+
| `batch(entries, handlers?)` | Load a fixed set and track its progress/completion; returns a `Batch`. |
|
|
135
|
+
| `get(url)` | The shared content loader cached for `url`, or `undefined`. Shorthand for `items.get(url)`. |
|
|
136
|
+
| `items` | `Map<url, loader>` — every content loader, keyed by URL. |
|
|
137
|
+
|
|
138
|
+
A load config splits **construction** from **projection**:
|
|
34
139
|
|
|
35
140
|
```ts
|
|
36
|
-
|
|
37
|
-
type
|
|
38
|
-
loaderArgs
|
|
141
|
+
susano.load(url, {
|
|
142
|
+
type, // which loader
|
|
143
|
+
loaderArgs?, // construction: how to produce the content (srcSet, loadEvent, loadFn…)
|
|
144
|
+
cache?, // false → force a fresh content fetch (default true)
|
|
145
|
+
postprocess?, // per call: (content, loader) => R | Promise<R>
|
|
146
|
+
onLoaded?, // per call: (result, loader) => void
|
|
147
|
+
onProgress?, // observes the shared content fetch: (value, loader) => void
|
|
148
|
+
onError?, // per call: (error, loader) => void
|
|
39
149
|
})
|
|
40
150
|
```
|
|
41
151
|
|
|
42
|
-
|
|
152
|
+
`loaderArgs` defines the content and is honored once (when the loader is first created for a URL). `postprocess` / `onLoaded` / `onError` are **per call** — each `load()`/batch entry gets its own.
|
|
43
153
|
|
|
44
|
-
|
|
154
|
+
#### `load()`
|
|
45
155
|
|
|
46
156
|
```ts
|
|
47
|
-
const
|
|
157
|
+
const bitmap = await susano.load('/hero.png', {
|
|
158
|
+
type: 'image',
|
|
159
|
+
postprocess: async (img) => createImageBitmap(img),
|
|
160
|
+
}) // ImageBitmap (this call's result)
|
|
161
|
+
|
|
162
|
+
susano.get('/hero.png')?.content // the shared HTMLImageElement (cached)
|
|
48
163
|
```
|
|
49
164
|
|
|
50
|
-
|
|
165
|
+
Calling `load()` again for the same URL reuses the cached content (no second fetch) and runs *this* call's `postprocess`. Pass `cache: false` to force a fresh content fetch that overwrites the cached content.
|
|
51
166
|
|
|
52
|
-
|
|
167
|
+
#### `batch()`
|
|
53
168
|
|
|
54
169
|
```ts
|
|
55
|
-
susano.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
170
|
+
const batch = susano.batch(
|
|
171
|
+
[
|
|
172
|
+
{ url: '/studio.glb', type: 'gltf', postprocess: parseScene },
|
|
173
|
+
{ url: '/env.exr', type: 'exr' },
|
|
174
|
+
{ url: '/noise.png', type: 'texture', cache: false },
|
|
175
|
+
],
|
|
176
|
+
{ onProgress, onCompleted, onError }
|
|
177
|
+
)
|
|
63
178
|
```
|
|
64
179
|
|
|
65
|
-
|
|
180
|
+
Each entry runs a per-call load. Content fetches dedupe across entries (and across any concurrent `load()`), so an in-flight or already-loaded asset is **joined**, not re-fetched. Auto-starts; returns a [`Batch`](#batch). An empty batch completes immediately with `value: 1`.
|
|
181
|
+
|
|
182
|
+
### Batch
|
|
183
|
+
|
|
184
|
+
The handle returned by `susano.batch()`. Auto-starts on the next microtask, so handlers passed to `batch()` (or `.on()` listeners attached immediately after) never miss a tick.
|
|
66
185
|
|
|
67
|
-
|
|
186
|
+
| Member | Type | Description |
|
|
187
|
+
| ------------- | ------------------ | -------------------------------------------- |
|
|
188
|
+
| `promise` | `Promise<Batch>` | Resolves once the whole set has settled. |
|
|
189
|
+
| `progress` | `number` | `0 → 1`, readable at any time. |
|
|
190
|
+
| `completed` | `boolean` | `true` once finished. |
|
|
191
|
+
| `loadCount` | `number` | Loads settled so far. |
|
|
192
|
+
| `loadLength` | `number` | Total loads in the batch. |
|
|
193
|
+
| `items` | `BatchItem[]` | `{ loader, promise }` per entry (entry order). |
|
|
68
194
|
|
|
69
195
|
```ts
|
|
70
|
-
|
|
196
|
+
type BatchHandlers = {
|
|
197
|
+
onProgress?: (e: BatchProgressEventArgs) => void // { value, loader, batch }
|
|
198
|
+
onCompleted?: (batch: Batch) => void
|
|
199
|
+
onError?: (e: BatchErrorEventArgs) => void // { loader, error, batch }
|
|
200
|
+
}
|
|
71
201
|
```
|
|
72
202
|
|
|
73
|
-
|
|
203
|
+
`Batch` extends `TinyEmitter` — subscribe directly with `.on('progress' | 'error' | 'completed', …)`. The `'completed'` event fires exactly once; `'progress'`'s `loader` is `null` for the empty-batch tick.
|
|
74
204
|
|
|
75
|
-
|
|
205
|
+
---
|
|
76
206
|
|
|
77
|
-
|
|
207
|
+
### SusanoLoader
|
|
208
|
+
|
|
209
|
+
Base class for every loader. Caches one piece of raw content per URL. You instantiate subclasses; reach any loader via `susano.get(url)`.
|
|
210
|
+
|
|
211
|
+
#### Properties
|
|
212
|
+
|
|
213
|
+
| Property | Type | Description |
|
|
214
|
+
| ---------- | --------------------------------------------- | ---------------------------------------------------- |
|
|
215
|
+
| `url` | `string` | The source URL. |
|
|
216
|
+
| `status` | `'idle' \| 'loading' \| 'loaded' \| 'error'` | Content lifecycle state. |
|
|
217
|
+
| `loading` | `boolean` | `true` while the content fetch is in flight. |
|
|
218
|
+
| `loaded` | `boolean` | `true` once content has loaded. |
|
|
219
|
+
| `content` | `T` | The raw loaded content. |
|
|
220
|
+
| `progress` | `number` | `0 → 1`. |
|
|
221
|
+
| `promise` | `Promise<T>` | Stable promise resolving with the raw **content**. |
|
|
222
|
+
|
|
223
|
+
#### Methods
|
|
224
|
+
|
|
225
|
+
| Method | Description |
|
|
226
|
+
| -------------------- | --------------------------------------------------------------------------------------------------- |
|
|
227
|
+
| `load(cache = true)` | Start (or join) the content fetch. Idempotent: repeat calls return the same promise without re-fetching. `cache: false` forces a fresh fetch that overwrites the content. |
|
|
228
|
+
|
|
229
|
+
#### Events
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
susano.load('/photo.png', { type: 'image' })
|
|
233
|
+
const loader = susano.get('/photo.png')!
|
|
234
|
+
loader.on('loaded', (l) => console.log(l.content))
|
|
235
|
+
loader.on('error', (err) => console.error(err))
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
#### Postprocess
|
|
78
239
|
|
|
79
|
-
|
|
80
|
-
| -------- | -------- | ----------------------------- |
|
|
81
|
-
| `srcSet` | `string` | Maps to `img.srcset` |
|
|
82
|
-
| `sizes` | `string` | Maps to `img.sizes` |
|
|
240
|
+
`postprocess` is a **per-call** projection from the shared content to your result. It runs every call (never cached), and an async `postprocess` blocks that call's `loaded` / `promise` until it resolves.
|
|
83
241
|
|
|
84
242
|
```ts
|
|
85
|
-
susano.
|
|
243
|
+
const bitmap: ImageBitmap = await susano.load('/hero.png', {
|
|
86
244
|
type: 'image',
|
|
87
|
-
|
|
245
|
+
postprocess: async (img) => createImageBitmap(img),
|
|
88
246
|
})
|
|
89
247
|
```
|
|
90
248
|
|
|
91
|
-
|
|
249
|
+
> Two calls for the same URL share one content fetch but each run their own `postprocess` — so there's no composition or "which transform wins" ambiguity. If a transform is expensive and you want its output cached/shared, promote it into a custom loader so the transformed value becomes the content.
|
|
92
250
|
|
|
93
|
-
|
|
251
|
+
---
|
|
94
252
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
253
|
+
## Built-in loaders
|
|
254
|
+
|
|
255
|
+
### image
|
|
256
|
+
|
|
257
|
+
Loads images via `HTMLImageElement`.
|
|
99
258
|
|
|
100
259
|
```ts
|
|
101
|
-
|
|
102
|
-
type: 'video',
|
|
103
|
-
loaderArgs: { loadEvent: 'canplaythrough' },
|
|
104
|
-
})
|
|
260
|
+
import { ImageLoader, type SusanoImageLoaderConfig } from '@joycostudio/susano'
|
|
105
261
|
```
|
|
106
262
|
|
|
107
|
-
|
|
263
|
+
| `loaderArgs` | Type | Description |
|
|
264
|
+
| ------------ | -------- | -------------------- |
|
|
265
|
+
| `srcSet` | `string` | Maps to `img.srcset` |
|
|
266
|
+
| `sizes` | `string` | Maps to `img.sizes` |
|
|
108
267
|
|
|
109
|
-
|
|
268
|
+
```ts
|
|
269
|
+
susano.load('/photo.png', {
|
|
270
|
+
type: 'image',
|
|
271
|
+
loaderArgs: { srcSet: '/photo.png 1x, /photo-2x.png 2x', sizes: '100vw' },
|
|
272
|
+
})
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### video / audio
|
|
110
276
|
|
|
111
|
-
|
|
112
|
-
| ----------- | ---------------------------------- | ----------- | ---------------------------------- |
|
|
113
|
-
| `audio` | `HTMLAudioElement` | new element | Existing audio element to load into |
|
|
114
|
-
| `loadEvent` | `'canplay'` \| `'canplaythrough'` | `'canplay'` | Event that signals load completion |
|
|
277
|
+
Load via `HTMLVideoElement` / `HTMLAudioElement`.
|
|
115
278
|
|
|
116
279
|
```ts
|
|
117
|
-
|
|
118
|
-
type: 'audio',
|
|
119
|
-
loaderArgs: { loadEvent: 'canplaythrough' },
|
|
120
|
-
})
|
|
280
|
+
import { VideoLoader, AudioLoader } from '@joycostudio/susano'
|
|
121
281
|
```
|
|
122
282
|
|
|
123
|
-
|
|
283
|
+
| `loaderArgs` | Type | Default | Description |
|
|
284
|
+
| --------------- | --------------------------------- | ------------- | ------------------------------------ |
|
|
285
|
+
| `video`/`audio` | element | new element | Existing element to load into. |
|
|
286
|
+
| `loadEvent` | `'canplay' \| 'canplaythrough'` | `'canplay'` | Event that signals load completion. |
|
|
124
287
|
|
|
125
|
-
|
|
288
|
+
```ts
|
|
289
|
+
susano.load('/intro.mp4', { type: 'video', loaderArgs: { loadEvent: 'canplaythrough' } })
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### generic
|
|
126
293
|
|
|
127
|
-
|
|
128
|
-
| -------- | ------------- | ------------------------------------ |
|
|
129
|
-
| `loadFn` | `GenericLoadFn` | **Required.** Custom load function |
|
|
294
|
+
Fully custom content production. You provide `loadFn`; Susano handles dedup, progress, and promises.
|
|
130
295
|
|
|
131
|
-
|
|
296
|
+
```ts
|
|
297
|
+
import { GenericLoader, type GenericLoadFn } from '@joycostudio/susano'
|
|
298
|
+
|
|
299
|
+
type GenericLoadFn = (ctx: {
|
|
300
|
+
url: string
|
|
301
|
+
done: (content: any) => void
|
|
302
|
+
error: (error: Error) => void
|
|
303
|
+
progress: (value: number) => void // 0 → 1
|
|
304
|
+
}) => void
|
|
305
|
+
```
|
|
132
306
|
|
|
133
307
|
```ts
|
|
134
|
-
susano.
|
|
308
|
+
susano.load('/data.json', {
|
|
135
309
|
type: 'generic',
|
|
136
310
|
loaderArgs: {
|
|
137
|
-
loadFn: ({ url, done, error
|
|
138
|
-
fetch(url)
|
|
139
|
-
.then((res) => res.json())
|
|
140
|
-
.then((data) => done(data))
|
|
141
|
-
.catch((err) => error(err))
|
|
311
|
+
loadFn: ({ url, done, error }) => {
|
|
312
|
+
fetch(url).then((r) => r.json()).then(done).catch(error)
|
|
142
313
|
},
|
|
143
314
|
},
|
|
144
315
|
})
|
|
145
316
|
```
|
|
146
317
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
The `susano` instance extends `TinyEmitter` and emits:
|
|
318
|
+
---
|
|
150
319
|
|
|
151
|
-
|
|
152
|
-
| ----------- | ---------------------------------------------------- | ------------------------- |
|
|
153
|
-
| `start` | `susano` | Batch loading started |
|
|
154
|
-
| `progress` | `{ value: number, item: SusanoLoader, susano }` | An asset finished loading |
|
|
155
|
-
| `completed` | `susano` | All assets loaded |
|
|
320
|
+
## Recipes
|
|
156
321
|
|
|
157
|
-
|
|
322
|
+
### Awaiting a single result
|
|
158
323
|
|
|
159
|
-
|
|
324
|
+
```ts
|
|
325
|
+
const img = susano.load('/hero.png', { type: 'image' })
|
|
326
|
+
document.body.appendChild(await img.promise)
|
|
327
|
+
```
|
|
160
328
|
|
|
161
|
-
|
|
329
|
+
### Awaiting a whole batch
|
|
162
330
|
|
|
163
331
|
```ts
|
|
164
|
-
|
|
165
|
-
susano.
|
|
166
|
-
susano.
|
|
332
|
+
const [a, b] = await Promise.all([
|
|
333
|
+
susano.load('/a.png', { type: 'image' }),
|
|
334
|
+
susano.load('/b.mp4', { type: 'video' }),
|
|
335
|
+
])
|
|
336
|
+
// …or drive a progress bar with a batch over the same URLs (dedupes the fetches):
|
|
337
|
+
await susano.batch([
|
|
338
|
+
{ url: '/a.png', type: 'image' },
|
|
339
|
+
{ url: '/b.mp4', type: 'video' },
|
|
340
|
+
]).promise
|
|
167
341
|
```
|
|
168
342
|
|
|
169
|
-
|
|
343
|
+
### Per-call postprocess, shared fetch
|
|
170
344
|
|
|
171
345
|
```ts
|
|
172
|
-
|
|
346
|
+
// Both share one /noise.png fetch; each gets its own result.
|
|
347
|
+
const bitmap = await susano.load('/noise.png', { type: 'image', postprocess: (i) => createImageBitmap(i) })
|
|
348
|
+
const raw = await susano.load('/noise.png', { type: 'image' }) // raw HTMLImageElement, shared fetch
|
|
173
349
|
```
|
|
174
350
|
|
|
175
|
-
|
|
351
|
+
### Fetch with streamed progress
|
|
176
352
|
|
|
177
|
-
|
|
353
|
+
```ts
|
|
354
|
+
susano.load('/big.bin', {
|
|
355
|
+
type: 'generic',
|
|
356
|
+
loaderArgs: {
|
|
357
|
+
loadFn: async ({ url, done, error, progress }) => {
|
|
358
|
+
try {
|
|
359
|
+
const res = await fetch(url)
|
|
360
|
+
const total = Number(res.headers.get('content-length')) || 0
|
|
361
|
+
const reader = res.body!.getReader()
|
|
362
|
+
const chunks: Uint8Array[] = []
|
|
363
|
+
let received = 0
|
|
364
|
+
while (true) {
|
|
365
|
+
const { done: d, value } = await reader.read()
|
|
366
|
+
if (d) break
|
|
367
|
+
chunks.push(value)
|
|
368
|
+
received += value.length
|
|
369
|
+
if (total) progress(received / total)
|
|
370
|
+
}
|
|
371
|
+
done(new Blob(chunks))
|
|
372
|
+
} catch (e) {
|
|
373
|
+
error(e as Error)
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
})
|
|
378
|
+
```
|
|
178
379
|
|
|
179
|
-
|
|
380
|
+
### Custom loader type
|
|
180
381
|
|
|
181
|
-
|
|
382
|
+
A custom loader implements `protected _load()` — the raw content fetch. The base owns the promise, dedup, and status; call `_onLoaded()` / `_onError()` / `_onProgress()` when the work settles. (If your loader's job is to *produce* a transformed value, that value becomes the cached content here.)
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
import { Susano, SusanoLoader } from '@joycostudio/susano'
|
|
386
|
+
|
|
387
|
+
class JSONLoader extends SusanoLoader<unknown> {
|
|
388
|
+
protected _load() {
|
|
389
|
+
fetch(this.url)
|
|
390
|
+
.then((res) => res.json())
|
|
391
|
+
.then((content) => {
|
|
392
|
+
this.content = content
|
|
393
|
+
this._onLoaded()
|
|
394
|
+
})
|
|
395
|
+
.catch((e) => this._onError(e))
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const s = new Susano({ json: JSONLoader })
|
|
400
|
+
|
|
401
|
+
await s.batch([{ url: '/config.json', type: 'json' }]).promise
|
|
402
|
+
```
|
|
182
403
|
|
|
183
|
-
|
|
404
|
+
### Lazy load + batch, same URL
|
|
184
405
|
|
|
185
|
-
|
|
406
|
+
`load()` and `batch()` share one content loader per URL — so an eager preload and a later manifest batch share the fetch.
|
|
186
407
|
|
|
187
|
-
|
|
408
|
+
```ts
|
|
409
|
+
susano.load('/hero.png', { type: 'image' }) // fires immediately
|
|
410
|
+
const batch = susano.batch([{ url: '/hero.png', type: 'image' }]) // joins it — no second fetch
|
|
188
411
|
|
|
189
|
-
|
|
190
|
-
|
|
412
|
+
await batch.promise
|
|
413
|
+
susano.get('/hero.png') // the one shared content loader both used
|
|
191
414
|
```
|
|
192
415
|
|
|
193
|
-
|
|
416
|
+
---
|
|
194
417
|
|
|
195
|
-
|
|
196
|
-
2. Choose whether it's a major/minor/patch bump
|
|
197
|
-
3. Provide a summary of the changes
|
|
418
|
+
## Package exports
|
|
198
419
|
|
|
199
|
-
|
|
420
|
+
| Import path | Contents |
|
|
421
|
+
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
422
|
+
| `@joycostudio/susano` | `susano` (default instance), `Susano`, `Batch`, `SusanoLoader`, `ImageLoader`, `VideoLoader`, `AudioLoader`, `GenericLoader`, `VERSION`, and all types (`LoadOptions`, `BatchEntry`, `BatchItem`, `BatchHandlers`, `BatchProgressEventArgs`, `BatchErrorEventArgs`, `ContentOf`, …). |
|
|
200
423
|
|
|
201
|
-
|
|
424
|
+
---
|
|
202
425
|
|
|
203
|
-
|
|
204
|
-
# 1. Create new versions of packages
|
|
205
|
-
pnpm version:package
|
|
426
|
+
## Development
|
|
206
427
|
|
|
207
|
-
|
|
208
|
-
pnpm
|
|
428
|
+
```bash
|
|
429
|
+
pnpm install
|
|
430
|
+
pnpm test # Vitest suite (run); pnpm test:watch for watch mode
|
|
431
|
+
pnpm typecheck # tsc --noEmit
|
|
432
|
+
pnpm build # tsup bundle
|
|
209
433
|
```
|
|
210
434
|
|
|
211
|
-
|
|
435
|
+
- [`docs/architecture.md`](./docs/architecture.md) — how Susano is built and why (content-vs-result, the dedup state machine, batch completion), with diagrams.
|
|
436
|
+
- [`docs/testing.md`](./docs/testing.md) — the test setup (Vitest + jsdom), conventions, and patterns, written to be replicated across JOYCO libraries.
|