@jolly-pixel/asset-server 1.0.0 → 2.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 +21 -21
- package/README.md +83 -83
- package/dist/catalog/CatalogExtension.d.ts +0 -1
- package/dist/catalog/CatalogExtension.d.ts.map +1 -1
- package/dist/catalog/CatalogExtension.js +0 -3
- package/dist/constants.js +4 -4
- package/dist/rooms/AssetRoomExtension.d.ts +0 -1
- package/dist/rooms/AssetRoomExtension.d.ts.map +1 -1
- package/dist/rooms/AssetRoomExtension.js +0 -3
- package/docs/AssetBackend.md +73 -73
- package/docs/AssetKinds.md +204 -204
- package/docs/AssetWriter.md +62 -62
- package/docs/Catalog.md +71 -71
- package/docs/Rooms.md +70 -70
- package/docs/Sync.md +146 -146
- package/docs/Workspace.md +172 -172
- package/package.json +6 -5
package/docs/AssetKinds.md
CHANGED
|
@@ -1,204 +1,204 @@
|
|
|
1
|
-
# Asset kinds
|
|
2
|
-
|
|
3
|
-
An `AssetKindHandler` defines how one asset type is recognized, folded into
|
|
4
|
-
state and serialized.
|
|
5
|
-
|
|
6
|
-
```ts
|
|
7
|
-
interface AssetKindHandler<TState = unknown, TCommand = unknown> {
|
|
8
|
-
readonly kind: string;
|
|
9
|
-
readonly match: readonly string[];
|
|
10
|
-
readonly snapshot?: SnapshotPolicy;
|
|
11
|
-
|
|
12
|
-
create(assetId: string): TState;
|
|
13
|
-
apply(state: TState, event: Event): void;
|
|
14
|
-
serialize(state: TState): Promise<Uint8Array>;
|
|
15
|
-
live?(binding: AssetRoomBinding<TState>): AssetLiveProtocol<TCommand>;
|
|
16
|
-
createExtension?(binding: AssetRoomBinding<TState>): Extension;
|
|
17
|
-
}
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
Handlers are checked in registration order. `match` contains globs matched
|
|
21
|
-
against root-relative POSIX paths. The built-in `binary` handler receives any
|
|
22
|
-
path that no registered handler claims.
|
|
23
|
-
|
|
24
|
-
`apply` receives lifecycle events and domain events from the asset stream.
|
|
25
|
-
`serialize` returns the bytes stored by the asset source. A handler that
|
|
26
|
-
supports live editing provides `live`; other kinds have no dynamic editing
|
|
27
|
-
room.
|
|
28
|
-
|
|
29
|
-
`apply` must reset the existing `TState` in place for `asset.created`,
|
|
30
|
-
`asset.updated` and `asset.deleted`. Each event is a complete checkpoint.
|
|
31
|
-
Replay creates a fresh state, resumes at the newest checkpoint and folds later
|
|
32
|
-
events. Reassigning the `state` parameter has no effect because `apply` returns
|
|
33
|
-
`void` and the store retains the value returned by `create`.
|
|
34
|
-
|
|
35
|
-
`TState` defaults to `unknown`, so a handler declared without it must narrow
|
|
36
|
-
its own state before use. Pass the state type to keep `create`, `apply` and
|
|
37
|
-
`serialize` checked against each other.
|
|
38
|
-
|
|
39
|
-
## Reading lifecycle payloads
|
|
40
|
-
|
|
41
|
-
`event.eventData` is typed `unknown` by the event store, because the store
|
|
42
|
-
holds any domain. Parse it with `parseAssetEvent` rather than asserting a
|
|
43
|
-
shape: it validates the payload against a JSON Schema for its event type and
|
|
44
|
-
returns a `Result` carrying the parsed event, or the reason it was refused.
|
|
45
|
-
|
|
46
|
-
```ts
|
|
47
|
-
apply(state: MyState, event: Event): void {
|
|
48
|
-
const parsed = parseAssetEvent(event);
|
|
49
|
-
if (parsed.ok && parsed.val.eventType === ASSET_UPDATED) {
|
|
50
|
-
// eventData is AssetWriteData here
|
|
51
|
-
state.bytes = decodeContent(parsed.val.eventData.content);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
Payloads read straight from persistence are parsed JSON, so a corrupt row
|
|
57
|
-
would otherwise reach the fold unchecked.
|
|
58
|
-
|
|
59
|
-
## Snapshot policy
|
|
60
|
-
|
|
61
|
-
```ts
|
|
62
|
-
interface SnapshotPolicy {
|
|
63
|
-
delay?: number;
|
|
64
|
-
maxDelay?: number;
|
|
65
|
-
}
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
`delay` is the quiet period after the latest domain event. `maxDelay` limits
|
|
69
|
-
the time since the first unsnapshotted event. Backend defaults are `2_000` ms
|
|
70
|
-
and `30_000` ms. A handler can override either value through `snapshot`.
|
|
71
|
-
|
|
72
|
-
`delay: 0` schedules the snapshot for the next timer turn. Lifecycle events
|
|
73
|
-
do not schedule snapshots.
|
|
74
|
-
|
|
75
|
-
## Registry
|
|
76
|
-
|
|
77
|
-
```ts
|
|
78
|
-
const kinds = new AssetKindRegistry([pixelArtHandler]);
|
|
79
|
-
|
|
80
|
-
kinds.register(voxelHandler);
|
|
81
|
-
kinds.resolve("textures/grass.png");
|
|
82
|
-
kinds.get("pixelart");
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
Registering the same kind twice throws. The reserved `binary` fallback cannot
|
|
86
|
-
be replaced.
|
|
87
|
-
|
|
88
|
-
## Built-in kinds
|
|
89
|
-
|
|
90
|
-
`binary` is the reserved fallback. `texture` is a shipped handler that claims
|
|
91
|
-
image files so a runtime `AssetType` of the same name can resolve them:
|
|
92
|
-
|
|
93
|
-
```ts
|
|
94
|
-
import { textureAssetHandler } from "@jolly-pixel/asset-server";
|
|
95
|
-
|
|
96
|
-
const kinds = new AssetKindRegistry([textureAssetHandler()]);
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
Its state is the file's bytes, exactly like `binary`, and it has no `live`
|
|
100
|
-
protocol, so texture assets get no editing room. The kind exists to
|
|
101
|
-
name the record: `AssetCatalog.resolve()` rejects a record whose kind does not
|
|
102
|
-
match its reference, and nothing on the browser side loads `binary`. Pass
|
|
103
|
-
`match` to narrow the globs from the default image extensions.
|
|
104
|
-
|
|
105
|
-
## Kinds shipped by other packages
|
|
106
|
-
|
|
107
|
-
Two handlers live with the domain they serialize rather than here, because
|
|
108
|
-
asset-server does not depend on the renderers:
|
|
109
|
-
|
|
110
|
-
```ts
|
|
111
|
-
import { pixelArtAssetHandler } from "@jolly-pixel/pixel-draw.renderer/asset/index.ts";
|
|
112
|
-
import { voxelMapAssetHandler } from "@jolly-pixel/voxel.renderer/asset/index.ts";
|
|
113
|
-
|
|
114
|
-
await createAssetBackend({
|
|
115
|
-
source,
|
|
116
|
-
eventStore,
|
|
117
|
-
handlers: [
|
|
118
|
-
pixelArtAssetHandler(),
|
|
119
|
-
voxelMapAssetHandler(),
|
|
120
|
-
textureAssetHandler()
|
|
121
|
-
]
|
|
122
|
-
});
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
Both take `@jolly-pixel/asset-server` as an optional peer dependency, so a
|
|
126
|
-
browser-only consumer of either renderer never installs it.
|
|
127
|
-
|
|
128
|
-
## Writing an editable kind
|
|
129
|
-
|
|
130
|
-
A kind with live editing has two halves that must not overlap: `apply` is the
|
|
131
|
-
only writer of state, and `live` describes a room that appends without
|
|
132
|
-
writing. `AssetRoomExtension` hosts the protocol, so a kind supplies only
|
|
133
|
-
what is specific to it:
|
|
134
|
-
|
|
135
|
-
```ts
|
|
136
|
-
interface AssetLiveProtocol<TCommand = unknown> {
|
|
137
|
-
readonly commandEventType: string;
|
|
138
|
-
readonly actions: readonly string[];
|
|
139
|
-
|
|
140
|
-
parse(payload: unknown): TCommand | null;
|
|
141
|
-
snapshot(): unknown;
|
|
142
|
-
arbitrate(
|
|
143
|
-
command: TCommand,
|
|
144
|
-
clientId: string
|
|
145
|
-
): AssetArbitration<TCommand> | null;
|
|
146
|
-
broadcast?(command: TCommand): AssetRoomMessage;
|
|
147
|
-
}
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
`live` runs once per room, so per-room state such as a conflict tracker
|
|
151
|
-
belongs in the returned protocol rather than in the handler:
|
|
152
|
-
|
|
153
|
-
```ts
|
|
154
|
-
live(binding) {
|
|
155
|
-
const arbiter = new MyArbiter({ conflictResolver });
|
|
156
|
-
const { state } = binding;
|
|
157
|
-
|
|
158
|
-
return {
|
|
159
|
-
commandEventType: MY_COMMAND,
|
|
160
|
-
actions: MY_ACTIONS,
|
|
161
|
-
parse: (payload) => isMyCommand(payload) ? payload : null,
|
|
162
|
-
snapshot: () => state.toJSON(),
|
|
163
|
-
arbitrate(command, clientId) {
|
|
164
|
-
const admitted = arbiter.admit(command);
|
|
165
|
-
if (admitted === null) {
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
command: admitted,
|
|
171
|
-
commit: () => arbiter.record(admitted)
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
The room parses the payload, arbitrates it, appends
|
|
179
|
-
`arbitration.command` under `commandEventType`, then calls
|
|
180
|
-
`arbitration.commit` and broadcasts. `commit` runs only after the append
|
|
181
|
-
lands, so a conflict tracker never records a command the store refused. The
|
|
182
|
-
append folds through `apply` before it resolves, so state is current by the
|
|
183
|
-
time peers hear about the change.
|
|
184
|
-
|
|
185
|
-
`broadcast` overrides the default `{ type: "command", data: command }`
|
|
186
|
-
envelope. `voxel-map` uses it to answer a `world-replace` with a full
|
|
187
|
-
snapshot.
|
|
188
|
-
|
|
189
|
-
`actions` names the commands the kind accepts. A configured rights table
|
|
190
|
-
checks each message under `${kind}.${action}`; a payload naming no declared
|
|
191
|
-
action is checked under `${kind}.invalid`.
|
|
192
|
-
|
|
193
|
-
A room that also mutated the state would apply every command twice: once
|
|
194
|
-
itself and once through the fold. Absolute writes survive that, but a command
|
|
195
|
-
carrying a delta does not. `voxel-map`'s `offset-updated` is exactly such a
|
|
196
|
-
command, which is why both shipped kinds keep the halves separate.
|
|
197
|
-
|
|
198
|
-
`createExtension` remains as an escape hatch for a room protocol `live`
|
|
199
|
-
cannot express, and takes precedence over it. It returns a `network.Extension`
|
|
200
|
-
whose `id` must equal the room name.
|
|
201
|
-
|
|
202
|
-
`apply` must never throw. Its event is already persisted, so a fold that
|
|
203
|
-
aborts would break every later replay. Both shipped handlers catch, log and
|
|
204
|
-
keep the last good state.
|
|
1
|
+
# Asset kinds
|
|
2
|
+
|
|
3
|
+
An `AssetKindHandler` defines how one asset type is recognized, folded into
|
|
4
|
+
state and serialized.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
interface AssetKindHandler<TState = unknown, TCommand = unknown> {
|
|
8
|
+
readonly kind: string;
|
|
9
|
+
readonly match: readonly string[];
|
|
10
|
+
readonly snapshot?: SnapshotPolicy;
|
|
11
|
+
|
|
12
|
+
create(assetId: string): TState;
|
|
13
|
+
apply(state: TState, event: Event): void;
|
|
14
|
+
serialize(state: TState): Promise<Uint8Array>;
|
|
15
|
+
live?(binding: AssetRoomBinding<TState>): AssetLiveProtocol<TCommand>;
|
|
16
|
+
createExtension?(binding: AssetRoomBinding<TState>): Extension;
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Handlers are checked in registration order. `match` contains globs matched
|
|
21
|
+
against root-relative POSIX paths. The built-in `binary` handler receives any
|
|
22
|
+
path that no registered handler claims.
|
|
23
|
+
|
|
24
|
+
`apply` receives lifecycle events and domain events from the asset stream.
|
|
25
|
+
`serialize` returns the bytes stored by the asset source. A handler that
|
|
26
|
+
supports live editing provides `live`; other kinds have no dynamic editing
|
|
27
|
+
room.
|
|
28
|
+
|
|
29
|
+
`apply` must reset the existing `TState` in place for `asset.created`,
|
|
30
|
+
`asset.updated` and `asset.deleted`. Each event is a complete checkpoint.
|
|
31
|
+
Replay creates a fresh state, resumes at the newest checkpoint and folds later
|
|
32
|
+
events. Reassigning the `state` parameter has no effect because `apply` returns
|
|
33
|
+
`void` and the store retains the value returned by `create`.
|
|
34
|
+
|
|
35
|
+
`TState` defaults to `unknown`, so a handler declared without it must narrow
|
|
36
|
+
its own state before use. Pass the state type to keep `create`, `apply` and
|
|
37
|
+
`serialize` checked against each other.
|
|
38
|
+
|
|
39
|
+
## Reading lifecycle payloads
|
|
40
|
+
|
|
41
|
+
`event.eventData` is typed `unknown` by the event store, because the store
|
|
42
|
+
holds any domain. Parse it with `parseAssetEvent` rather than asserting a
|
|
43
|
+
shape: it validates the payload against a JSON Schema for its event type and
|
|
44
|
+
returns a `Result` carrying the parsed event, or the reason it was refused.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
apply(state: MyState, event: Event): void {
|
|
48
|
+
const parsed = parseAssetEvent(event);
|
|
49
|
+
if (parsed.ok && parsed.val.eventType === ASSET_UPDATED) {
|
|
50
|
+
// eventData is AssetWriteData here
|
|
51
|
+
state.bytes = decodeContent(parsed.val.eventData.content);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Payloads read straight from persistence are parsed JSON, so a corrupt row
|
|
57
|
+
would otherwise reach the fold unchecked.
|
|
58
|
+
|
|
59
|
+
## Snapshot policy
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
interface SnapshotPolicy {
|
|
63
|
+
delay?: number;
|
|
64
|
+
maxDelay?: number;
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`delay` is the quiet period after the latest domain event. `maxDelay` limits
|
|
69
|
+
the time since the first unsnapshotted event. Backend defaults are `2_000` ms
|
|
70
|
+
and `30_000` ms. A handler can override either value through `snapshot`.
|
|
71
|
+
|
|
72
|
+
`delay: 0` schedules the snapshot for the next timer turn. Lifecycle events
|
|
73
|
+
do not schedule snapshots.
|
|
74
|
+
|
|
75
|
+
## Registry
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
const kinds = new AssetKindRegistry([pixelArtHandler]);
|
|
79
|
+
|
|
80
|
+
kinds.register(voxelHandler);
|
|
81
|
+
kinds.resolve("textures/grass.png");
|
|
82
|
+
kinds.get("pixelart");
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Registering the same kind twice throws. The reserved `binary` fallback cannot
|
|
86
|
+
be replaced.
|
|
87
|
+
|
|
88
|
+
## Built-in kinds
|
|
89
|
+
|
|
90
|
+
`binary` is the reserved fallback. `texture` is a shipped handler that claims
|
|
91
|
+
image files so a runtime `AssetType` of the same name can resolve them:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { textureAssetHandler } from "@jolly-pixel/asset-server";
|
|
95
|
+
|
|
96
|
+
const kinds = new AssetKindRegistry([textureAssetHandler()]);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Its state is the file's bytes, exactly like `binary`, and it has no `live`
|
|
100
|
+
protocol, so texture assets get no editing room. The kind exists to
|
|
101
|
+
name the record: `AssetCatalog.resolve()` rejects a record whose kind does not
|
|
102
|
+
match its reference, and nothing on the browser side loads `binary`. Pass
|
|
103
|
+
`match` to narrow the globs from the default image extensions.
|
|
104
|
+
|
|
105
|
+
## Kinds shipped by other packages
|
|
106
|
+
|
|
107
|
+
Two handlers live with the domain they serialize rather than here, because
|
|
108
|
+
asset-server does not depend on the renderers:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { pixelArtAssetHandler } from "@jolly-pixel/pixel-draw.renderer/asset/index.ts";
|
|
112
|
+
import { voxelMapAssetHandler } from "@jolly-pixel/voxel.renderer/asset/index.ts";
|
|
113
|
+
|
|
114
|
+
await createAssetBackend({
|
|
115
|
+
source,
|
|
116
|
+
eventStore,
|
|
117
|
+
handlers: [
|
|
118
|
+
pixelArtAssetHandler(),
|
|
119
|
+
voxelMapAssetHandler(),
|
|
120
|
+
textureAssetHandler()
|
|
121
|
+
]
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Both take `@jolly-pixel/asset-server` as an optional peer dependency, so a
|
|
126
|
+
browser-only consumer of either renderer never installs it.
|
|
127
|
+
|
|
128
|
+
## Writing an editable kind
|
|
129
|
+
|
|
130
|
+
A kind with live editing has two halves that must not overlap: `apply` is the
|
|
131
|
+
only writer of state, and `live` describes a room that appends without
|
|
132
|
+
writing. `AssetRoomExtension` hosts the protocol, so a kind supplies only
|
|
133
|
+
what is specific to it:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
interface AssetLiveProtocol<TCommand = unknown> {
|
|
137
|
+
readonly commandEventType: string;
|
|
138
|
+
readonly actions: readonly string[];
|
|
139
|
+
|
|
140
|
+
parse(payload: unknown): TCommand | null;
|
|
141
|
+
snapshot(): unknown;
|
|
142
|
+
arbitrate(
|
|
143
|
+
command: TCommand,
|
|
144
|
+
clientId: string
|
|
145
|
+
): AssetArbitration<TCommand> | null;
|
|
146
|
+
broadcast?(command: TCommand): AssetRoomMessage;
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`live` runs once per room, so per-room state such as a conflict tracker
|
|
151
|
+
belongs in the returned protocol rather than in the handler:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
live(binding) {
|
|
155
|
+
const arbiter = new MyArbiter({ conflictResolver });
|
|
156
|
+
const { state } = binding;
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
commandEventType: MY_COMMAND,
|
|
160
|
+
actions: MY_ACTIONS,
|
|
161
|
+
parse: (payload) => isMyCommand(payload) ? payload : null,
|
|
162
|
+
snapshot: () => state.toJSON(),
|
|
163
|
+
arbitrate(command, clientId) {
|
|
164
|
+
const admitted = arbiter.admit(command);
|
|
165
|
+
if (admitted === null) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
command: admitted,
|
|
171
|
+
commit: () => arbiter.record(admitted)
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The room parses the payload, arbitrates it, appends
|
|
179
|
+
`arbitration.command` under `commandEventType`, then calls
|
|
180
|
+
`arbitration.commit` and broadcasts. `commit` runs only after the append
|
|
181
|
+
lands, so a conflict tracker never records a command the store refused. The
|
|
182
|
+
append folds through `apply` before it resolves, so state is current by the
|
|
183
|
+
time peers hear about the change.
|
|
184
|
+
|
|
185
|
+
`broadcast` overrides the default `{ type: "command", data: command }`
|
|
186
|
+
envelope. `voxel-map` uses it to answer a `world-replace` with a full
|
|
187
|
+
snapshot.
|
|
188
|
+
|
|
189
|
+
`actions` names the commands the kind accepts. A configured rights table
|
|
190
|
+
checks each message under `${kind}.${action}`; a payload naming no declared
|
|
191
|
+
action is checked under `${kind}.invalid`.
|
|
192
|
+
|
|
193
|
+
A room that also mutated the state would apply every command twice: once
|
|
194
|
+
itself and once through the fold. Absolute writes survive that, but a command
|
|
195
|
+
carrying a delta does not. `voxel-map`'s `offset-updated` is exactly such a
|
|
196
|
+
command, which is why both shipped kinds keep the halves separate.
|
|
197
|
+
|
|
198
|
+
`createExtension` remains as an escape hatch for a room protocol `live`
|
|
199
|
+
cannot express, and takes precedence over it. It returns a `network.Extension`
|
|
200
|
+
whose `id` must equal the room name.
|
|
201
|
+
|
|
202
|
+
`apply` must never throw. Its event is already persisted, so a fold that
|
|
203
|
+
aborts would break every later replay. Both shipped handlers catch, log and
|
|
204
|
+
keep the last good state.
|
package/docs/AssetWriter.md
CHANGED
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
# AssetWriter
|
|
2
|
-
|
|
3
|
-
Use `backend.writer` to change assets. Each method appends an asset lifecycle
|
|
4
|
-
event before the backend updates the catalog and source.
|
|
5
|
-
|
|
6
|
-
```ts
|
|
7
|
-
writer.create(input: CreateAssetInput): Promise<Result<Event, Error>>
|
|
8
|
-
writer.update(input: UpdateAssetInput): Promise<Result<Event, Error>>
|
|
9
|
-
writer.rename(input: RenameAssetInput): Promise<Result<Event, Error>>
|
|
10
|
-
writer.remove(input: DeleteAssetInput): Promise<Result<Event, Error>>
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Every input requires an event-store actor:
|
|
14
|
-
|
|
15
|
-
```ts
|
|
16
|
-
const actor = { type: "user", id: "alice" } as const;
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Create
|
|
20
|
-
|
|
21
|
-
```ts
|
|
22
|
-
const result = await backend.writer.create({
|
|
23
|
-
path: "textures/grass.png",
|
|
24
|
-
data: pngBytes,
|
|
25
|
-
actor
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const event = result.unwrap();
|
|
29
|
-
console.log(event.assetId);
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
```ts
|
|
33
|
-
interface CreateAssetInput {
|
|
34
|
-
path: string;
|
|
35
|
-
data: Uint8Array;
|
|
36
|
-
actor: Actor;
|
|
37
|
-
kind?: string;
|
|
38
|
-
assetId?: string;
|
|
39
|
-
}
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
The backend generates an asset ID when `assetId` is omitted. It resolves the
|
|
43
|
-
kind from the registered path globs when `kind` is omitted.
|
|
44
|
-
|
|
45
|
-
## Update, rename and remove
|
|
46
|
-
|
|
47
|
-
```ts
|
|
48
|
-
await backend.writer.update({ assetId, data: nextBytes, actor });
|
|
49
|
-
await backend.writer.rename({ assetId, to: "textures/ground.png", actor });
|
|
50
|
-
await backend.writer.remove({ assetId, actor });
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
These operations return an error result when the asset ID is unknown. Paths
|
|
54
|
-
are
|
|
55
|
-
[root-relative POSIX paths](../../asset-source/docs/AssetSource.md#paths); one
|
|
56
|
-
that escapes the
|
|
57
|
-
source root, or that names the `.jollypixel/` state directory, throws
|
|
58
|
-
`AssetPathEscapeError`.
|
|
59
|
-
|
|
60
|
-
Call `backend.flush(assetId)` when the caller must wait for the resulting
|
|
61
|
-
source write. The `alreadyProjected` input option is reserved for source-backed
|
|
62
|
-
reconciliation, where the bytes already exist in the source.
|
|
1
|
+
# AssetWriter
|
|
2
|
+
|
|
3
|
+
Use `backend.writer` to change assets. Each method appends an asset lifecycle
|
|
4
|
+
event before the backend updates the catalog and source.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
writer.create(input: CreateAssetInput): Promise<Result<Event, Error>>
|
|
8
|
+
writer.update(input: UpdateAssetInput): Promise<Result<Event, Error>>
|
|
9
|
+
writer.rename(input: RenameAssetInput): Promise<Result<Event, Error>>
|
|
10
|
+
writer.remove(input: DeleteAssetInput): Promise<Result<Event, Error>>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Every input requires an event-store actor:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
const actor = { type: "user", id: "alice" } as const;
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Create
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
const result = await backend.writer.create({
|
|
23
|
+
path: "textures/grass.png",
|
|
24
|
+
data: pngBytes,
|
|
25
|
+
actor
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const event = result.unwrap();
|
|
29
|
+
console.log(event.assetId);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
interface CreateAssetInput {
|
|
34
|
+
path: string;
|
|
35
|
+
data: Uint8Array;
|
|
36
|
+
actor: Actor;
|
|
37
|
+
kind?: string;
|
|
38
|
+
assetId?: string;
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The backend generates an asset ID when `assetId` is omitted. It resolves the
|
|
43
|
+
kind from the registered path globs when `kind` is omitted.
|
|
44
|
+
|
|
45
|
+
## Update, rename and remove
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
await backend.writer.update({ assetId, data: nextBytes, actor });
|
|
49
|
+
await backend.writer.rename({ assetId, to: "textures/ground.png", actor });
|
|
50
|
+
await backend.writer.remove({ assetId, actor });
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
These operations return an error result when the asset ID is unknown. Paths
|
|
54
|
+
are
|
|
55
|
+
[root-relative POSIX paths](../../asset-source/docs/AssetSource.md#paths); one
|
|
56
|
+
that escapes the
|
|
57
|
+
source root, or that names the `.jollypixel/` state directory, throws
|
|
58
|
+
`AssetPathEscapeError`.
|
|
59
|
+
|
|
60
|
+
Call `backend.flush(assetId)` when the caller must wait for the resulting
|
|
61
|
+
source write. The `alreadyProjected` input option is reserved for source-backed
|
|
62
|
+
reconciliation, where the bytes already exist in the source.
|
package/docs/Catalog.md
CHANGED
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
# Catalog
|
|
2
|
-
|
|
3
|
-
`CatalogProjection` folds asset lifecycle events into an
|
|
4
|
-
`@jolly-pixel/asset` catalog.
|
|
5
|
-
|
|
6
|
-
```ts
|
|
7
|
-
const projection = new CatalogProjection({ eventStore });
|
|
8
|
-
projection.load();
|
|
9
|
-
projection.start();
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
- `load()` folds each asset's newest lifecycle checkpoint and the events
|
|
13
|
-
after it. See [Replay](./Sync.md#replay).
|
|
14
|
-
- `catalog` exposes the current `AssetCatalog`.
|
|
15
|
-
- `size` is the number of cataloged assets.
|
|
16
|
-
- `snapshot()` returns `AssetManifestData`.
|
|
17
|
-
- `changed` is emitted for each recognized lifecycle event applied to the
|
|
18
|
-
catalog. A deleted asset has `record: null`.
|
|
19
|
-
- `close()` stops following appended events and removes listeners.
|
|
20
|
-
|
|
21
|
-
`apply(event)` returns `false` and changes nothing for events outside the
|
|
22
|
-
`asset.` prefix and for lifecycle events whose payload does not match their
|
|
23
|
-
type. See [Typed payloads](./Sync.md#typed-payloads).
|
|
24
|
-
|
|
25
|
-
Each catalog record uses the asset content hash as its `revision`.
|
|
26
|
-
|
|
27
|
-
## Network room
|
|
28
|
-
|
|
29
|
-
`CatalogExtension` provides a read-only `asset-catalog` room. A client receives
|
|
30
|
-
a snapshot when it joins and catalog changes while it remains connected.
|
|
31
|
-
|
|
32
|
-
```ts
|
|
33
|
-
server.register(new CatalogExtension({ projection }));
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
```ts
|
|
37
|
-
{ type: "catalog:snapshot", manifest: AssetManifestData }
|
|
38
|
-
{ type: "catalog:changed", change: { eventType, assetId, record } }
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
`createAssetBackend().attach(server)` registers this room for the usual setup.
|
|
42
|
-
|
|
43
|
-
## HTTP handler
|
|
44
|
-
|
|
45
|
-
```ts
|
|
46
|
-
import { createCatalogHandler } from "@jolly-pixel/asset-server/catalog";
|
|
47
|
-
|
|
48
|
-
const handler = createCatalogHandler({
|
|
49
|
-
projection,
|
|
50
|
-
path: "/__jollypixel/catalog"
|
|
51
|
-
});
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
The default path is `/__jollypixel/catalog`. `GET` returns the JSON snapshot
|
|
55
|
-
and `HEAD` returns the same headers without a body. Other methods on that path
|
|
56
|
-
receive `405` with `Allow: GET, HEAD`. Requests for another path are passed to
|
|
57
|
-
`next()`.
|
|
58
|
-
|
|
59
|
-
## Vite plugin
|
|
60
|
-
|
|
61
|
-
```ts
|
|
62
|
-
import {
|
|
63
|
-
createAssetCatalogPlugin
|
|
64
|
-
} from "@jolly-pixel/asset-server/plugins/vite.ts";
|
|
65
|
-
|
|
66
|
-
export default {
|
|
67
|
-
plugins: [createAssetCatalogPlugin({ projection })]
|
|
68
|
-
};
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
The plugin accepts the same optional `path` override as the HTTP handler.
|
|
1
|
+
# Catalog
|
|
2
|
+
|
|
3
|
+
`CatalogProjection` folds asset lifecycle events into an
|
|
4
|
+
`@jolly-pixel/asset` catalog.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
const projection = new CatalogProjection({ eventStore });
|
|
8
|
+
projection.load();
|
|
9
|
+
projection.start();
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
- `load()` folds each asset's newest lifecycle checkpoint and the events
|
|
13
|
+
after it. See [Replay](./Sync.md#replay).
|
|
14
|
+
- `catalog` exposes the current `AssetCatalog`.
|
|
15
|
+
- `size` is the number of cataloged assets.
|
|
16
|
+
- `snapshot()` returns `AssetManifestData`.
|
|
17
|
+
- `changed` is emitted for each recognized lifecycle event applied to the
|
|
18
|
+
catalog. A deleted asset has `record: null`.
|
|
19
|
+
- `close()` stops following appended events and removes listeners.
|
|
20
|
+
|
|
21
|
+
`apply(event)` returns `false` and changes nothing for events outside the
|
|
22
|
+
`asset.` prefix and for lifecycle events whose payload does not match their
|
|
23
|
+
type. See [Typed payloads](./Sync.md#typed-payloads).
|
|
24
|
+
|
|
25
|
+
Each catalog record uses the asset content hash as its `revision`.
|
|
26
|
+
|
|
27
|
+
## Network room
|
|
28
|
+
|
|
29
|
+
`CatalogExtension` provides a read-only `asset-catalog` room. A client receives
|
|
30
|
+
a snapshot when it joins and catalog changes while it remains connected.
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
server.register(new CatalogExtension({ projection }));
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
{ type: "catalog:snapshot", manifest: AssetManifestData }
|
|
38
|
+
{ type: "catalog:changed", change: { eventType, assetId, record } }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`createAssetBackend().attach(server)` registers this room for the usual setup.
|
|
42
|
+
|
|
43
|
+
## HTTP handler
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { createCatalogHandler } from "@jolly-pixel/asset-server/catalog";
|
|
47
|
+
|
|
48
|
+
const handler = createCatalogHandler({
|
|
49
|
+
projection,
|
|
50
|
+
path: "/__jollypixel/catalog"
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The default path is `/__jollypixel/catalog`. `GET` returns the JSON snapshot
|
|
55
|
+
and `HEAD` returns the same headers without a body. Other methods on that path
|
|
56
|
+
receive `405` with `Allow: GET, HEAD`. Requests for another path are passed to
|
|
57
|
+
`next()`.
|
|
58
|
+
|
|
59
|
+
## Vite plugin
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import {
|
|
63
|
+
createAssetCatalogPlugin
|
|
64
|
+
} from "@jolly-pixel/asset-server/plugins/vite.ts";
|
|
65
|
+
|
|
66
|
+
export default {
|
|
67
|
+
plugins: [createAssetCatalogPlugin({ projection })]
|
|
68
|
+
};
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The plugin accepts the same optional `path` override as the HTTP handler.
|