@yagejs/save 0.5.0 → 0.6.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 +72 -19
- package/dist/index.cjs +414 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +163 -19
- package/dist/index.d.ts +163 -19
- package/dist/index.js +391 -22
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# @yagejs/save
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Persistence for the [YAGE](https://yage.dev) 2D game engine — typed reactive
|
|
4
|
+
stores plus a snapshot path for full-scene quicksave.
|
|
4
5
|
|
|
5
6
|
## Install
|
|
6
7
|
|
|
@@ -8,40 +9,92 @@ Save and load game state for the [YAGE](https://yage.dev) 2D game engine.
|
|
|
8
9
|
npm install @yagejs/save
|
|
9
10
|
```
|
|
10
11
|
|
|
11
|
-
##
|
|
12
|
+
## Stores + Save instance (primary path)
|
|
13
|
+
|
|
14
|
+
Most save data is *intentional* — settings, save slots, world facts,
|
|
15
|
+
progression. Define typed stores at module scope, construct one `Save`
|
|
16
|
+
instance, register it via the plugin.
|
|
12
17
|
|
|
13
18
|
```ts
|
|
14
19
|
import { Engine } from "@yagejs/core";
|
|
15
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
defineStore, defineSet,
|
|
22
|
+
createSave, SavePlugin, localStorageAdapter,
|
|
23
|
+
} from "@yagejs/save";
|
|
24
|
+
|
|
25
|
+
interface Settings { music: number; sfx: number }
|
|
26
|
+
interface RunData { chapter: number; position: { x: number; y: number } }
|
|
27
|
+
|
|
28
|
+
const settings = defineStore<Settings>("settings", {
|
|
29
|
+
defaults: () => ({ music: 0.8, sfx: 1.0 }),
|
|
30
|
+
});
|
|
31
|
+
const opened = defineSet<string>("world.opened");
|
|
32
|
+
const saves = defineStore<RunData>("saves", {
|
|
33
|
+
defaults: () => ({ chapter: 1, position: { x: 0, y: 0 } }),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const save = createSave({ adapter: localStorageAdapter() });
|
|
37
|
+
|
|
38
|
+
await save.restoreAll([settings, opened, saves]);
|
|
39
|
+
save.autoPersist(settings);
|
|
16
40
|
|
|
17
41
|
const engine = new Engine();
|
|
18
|
-
engine.use(new SavePlugin());
|
|
42
|
+
engine.use(new SavePlugin({ save }));
|
|
19
43
|
```
|
|
20
44
|
|
|
21
|
-
|
|
45
|
+
In-game components resolve the registered Save through `SaveServiceKey`:
|
|
22
46
|
|
|
23
47
|
```ts
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
48
|
+
import { SaveServiceKey } from "@yagejs/save";
|
|
49
|
+
|
|
50
|
+
class CheckpointOnRest extends Component {
|
|
51
|
+
setup() {
|
|
52
|
+
this.entity.on(Rested, async () => {
|
|
53
|
+
const save = this.use(SaveServiceKey);
|
|
54
|
+
await save.saveSlot(saves, "auto");
|
|
55
|
+
});
|
|
56
|
+
}
|
|
29
57
|
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Save slots with typed metadata:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
interface RunMeta { location: string; playtime: number }
|
|
64
|
+
await save.saveSlot<RunMeta>(saves, "manual-1", { metadata: { /* … */ } });
|
|
65
|
+
const slots = await save.listSlots<RunMeta>(saves);
|
|
66
|
+
await save.loadSlot(saves, "manual-1");
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Snapshot path (advanced)
|
|
70
|
+
|
|
71
|
+
Full-scene serialization via `@serializable` decorators. Use for quicksave
|
|
72
|
+
that captures every entity, component, and active process.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { serializable } from "@yagejs/core";
|
|
76
|
+
import { SnapshotPlugin, SnapshotServiceKey } from "@yagejs/save";
|
|
30
77
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
await saveService.save("slot-1");
|
|
78
|
+
@serializable
|
|
79
|
+
class Player extends Entity { /* ... */ }
|
|
34
80
|
|
|
35
|
-
|
|
36
|
-
|
|
81
|
+
engine.use(new SnapshotPlugin());
|
|
82
|
+
const snap = engine.context.resolve(SnapshotServiceKey);
|
|
83
|
+
snap.saveSnapshot("slot-1");
|
|
84
|
+
await snap.loadSnapshot("slot-1");
|
|
37
85
|
```
|
|
38
86
|
|
|
39
87
|
## What's in the box
|
|
40
88
|
|
|
41
|
-
- **
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
|
|
89
|
+
- **Stores** — `defineStore`, `defineSet`, `defineMap`, `defineCounter`
|
|
90
|
+
(re-exported from `@yagejs/core`).
|
|
91
|
+
- **Save** — `createSave({ adapter })` with `persist`, `restore`, `saveSlot`,
|
|
92
|
+
`loadSlot`, `listSlots`, `deleteSlot`, `autoPersist`.
|
|
93
|
+
- **Adapters** — `localStorageAdapter`, `memoryAdapter`. Implement
|
|
94
|
+
`SaveAdapter` for IndexedDB, files, cloud, etc.
|
|
95
|
+
- **Codecs** — `jsonCodec`, `setCodec`, `mapCodec`, `dateCodec`.
|
|
96
|
+
- **Snapshot system** — `SnapshotPlugin`, `SnapshotService`, `@serializable`
|
|
97
|
+
decorator (in `@yagejs/core`), snapshot contributors.
|
|
45
98
|
|
|
46
99
|
## Docs
|
|
47
100
|
|
package/dist/index.cjs
CHANGED
|
@@ -21,19 +21,375 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
// src/index.ts
|
|
22
22
|
var index_exports = {};
|
|
23
23
|
__export(index_exports, {
|
|
24
|
-
|
|
24
|
+
InvalidKeyError: () => InvalidKeyError,
|
|
25
|
+
LocalStorageSnapshotStorage: () => LocalStorageSnapshotStorage,
|
|
26
|
+
Save: () => Save,
|
|
25
27
|
SavePlugin: () => SavePlugin,
|
|
26
|
-
SaveService: () => SaveService,
|
|
27
28
|
SaveServiceKey: () => SaveServiceKey,
|
|
28
|
-
|
|
29
|
+
SlotNotFoundError: () => SlotNotFoundError,
|
|
30
|
+
SnapshotPlugin: () => SnapshotPlugin,
|
|
31
|
+
SnapshotService: () => SnapshotService,
|
|
32
|
+
SnapshotServiceKey: () => SnapshotServiceKey,
|
|
33
|
+
StoreMigrationMissingError: () => import_core5.StoreMigrationMissingError,
|
|
34
|
+
StoreVersionTooNewError: () => import_core5.StoreVersionTooNewError,
|
|
35
|
+
VERSION: () => import_core4.VERSION,
|
|
36
|
+
createSave: () => createSave,
|
|
37
|
+
dateCodec: () => import_core5.dateCodec,
|
|
38
|
+
defineCounter: () => import_core5.defineCounter,
|
|
39
|
+
defineMap: () => import_core5.defineMap,
|
|
40
|
+
defineSet: () => import_core5.defineSet,
|
|
41
|
+
defineStore: () => import_core5.defineStore,
|
|
42
|
+
jsonCodec: () => import_core5.jsonCodec,
|
|
43
|
+
localStorageAdapter: () => localStorageAdapter,
|
|
44
|
+
mapCodec: () => import_core5.mapCodec,
|
|
45
|
+
memoryAdapter: () => memoryAdapter,
|
|
46
|
+
setCodec: () => import_core5.setCodec
|
|
29
47
|
});
|
|
30
48
|
module.exports = __toCommonJS(index_exports);
|
|
31
|
-
var
|
|
49
|
+
var import_core4 = require("@yagejs/core");
|
|
50
|
+
var import_core5 = require("@yagejs/core");
|
|
51
|
+
|
|
52
|
+
// src/Save.ts
|
|
53
|
+
var SlotNotFoundError = class extends Error {
|
|
54
|
+
static {
|
|
55
|
+
__name(this, "SlotNotFoundError");
|
|
56
|
+
}
|
|
57
|
+
storeId;
|
|
58
|
+
slot;
|
|
59
|
+
constructor(storeId, slot) {
|
|
60
|
+
super(`No save found for store "${storeId}" in slot "${slot}".`);
|
|
61
|
+
this.name = "SlotNotFoundError";
|
|
62
|
+
this.storeId = storeId;
|
|
63
|
+
this.slot = slot;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var InvalidKeyError = class extends Error {
|
|
67
|
+
static {
|
|
68
|
+
__name(this, "InvalidKeyError");
|
|
69
|
+
}
|
|
70
|
+
constructor(message) {
|
|
71
|
+
super(message);
|
|
72
|
+
this.name = "InvalidKeyError";
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
var SEP = "/";
|
|
76
|
+
var DOC_TAG = "d";
|
|
77
|
+
var SLOT_TAG = "s";
|
|
78
|
+
var MANIFEST_TAG = "m";
|
|
79
|
+
function validateStoreId(id) {
|
|
80
|
+
if (id.length === 0) {
|
|
81
|
+
throw new InvalidKeyError("Save: store id must be non-empty.");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
__name(validateStoreId, "validateStoreId");
|
|
85
|
+
function validateSlotName(slot) {
|
|
86
|
+
if (slot.length === 0) {
|
|
87
|
+
throw new InvalidKeyError("Save: slot name must be non-empty.");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
__name(validateSlotName, "validateSlotName");
|
|
91
|
+
function docKey(id) {
|
|
92
|
+
validateStoreId(id);
|
|
93
|
+
return `${encodeURIComponent(id)}${SEP}${DOC_TAG}`;
|
|
94
|
+
}
|
|
95
|
+
__name(docKey, "docKey");
|
|
96
|
+
function slotKey(id, slot) {
|
|
97
|
+
validateStoreId(id);
|
|
98
|
+
validateSlotName(slot);
|
|
99
|
+
return `${encodeURIComponent(id)}${SEP}${SLOT_TAG}${SEP}${encodeURIComponent(slot)}`;
|
|
100
|
+
}
|
|
101
|
+
__name(slotKey, "slotKey");
|
|
102
|
+
function manifestKey(id) {
|
|
103
|
+
validateStoreId(id);
|
|
104
|
+
return `${encodeURIComponent(id)}${SEP}${MANIFEST_TAG}`;
|
|
105
|
+
}
|
|
106
|
+
__name(manifestKey, "manifestKey");
|
|
107
|
+
async function readManifest(adapter, id) {
|
|
108
|
+
const raw = await adapter.read(manifestKey(id));
|
|
109
|
+
if (raw == null) return { version: 1, slots: {} };
|
|
110
|
+
try {
|
|
111
|
+
const parsed = JSON.parse(raw);
|
|
112
|
+
if (parsed && typeof parsed === "object" && parsed.slots) {
|
|
113
|
+
return { version: 1, slots: parsed.slots };
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
return { version: 1, slots: {} };
|
|
118
|
+
}
|
|
119
|
+
__name(readManifest, "readManifest");
|
|
120
|
+
async function writeManifest(adapter, id, manifest) {
|
|
121
|
+
await adapter.write(manifestKey(id), JSON.stringify(manifest));
|
|
122
|
+
}
|
|
123
|
+
__name(writeManifest, "writeManifest");
|
|
124
|
+
var Save = class {
|
|
125
|
+
static {
|
|
126
|
+
__name(this, "Save");
|
|
127
|
+
}
|
|
128
|
+
adapter;
|
|
129
|
+
/**
|
|
130
|
+
* Per-store manifest update queue. Two concurrent saveSlot/deleteSlot calls
|
|
131
|
+
* against the same store would otherwise read-modify-write the manifest
|
|
132
|
+
* blindly — the later writer wins and the earlier change is silently lost.
|
|
133
|
+
* Funnelling manifest mutations through a per-store promise chain serializes
|
|
134
|
+
* them while leaving slot data writes (which target distinct keys)
|
|
135
|
+
* unaffected. Per-store, not global, because manifests for different stores
|
|
136
|
+
* never collide.
|
|
137
|
+
*/
|
|
138
|
+
manifestQueues = /* @__PURE__ */ new Map();
|
|
139
|
+
constructor(opts) {
|
|
140
|
+
this.adapter = opts.adapter;
|
|
141
|
+
}
|
|
142
|
+
/** Persist the store as an unslotted document. */
|
|
143
|
+
async persist(store) {
|
|
144
|
+
const payload = store.serialize();
|
|
145
|
+
await this.adapter.write(docKey(store.id), JSON.stringify(payload));
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Restore an unslotted document into the store. No-op when the document
|
|
149
|
+
* doesn't exist — the store keeps its current (default) value.
|
|
150
|
+
*/
|
|
151
|
+
async restore(store) {
|
|
152
|
+
const raw = await this.adapter.read(docKey(store.id));
|
|
153
|
+
if (raw == null) return;
|
|
154
|
+
store.hydrate(JSON.parse(raw));
|
|
155
|
+
}
|
|
156
|
+
/** Restore many stores in parallel. */
|
|
157
|
+
async restoreAll(stores) {
|
|
158
|
+
await Promise.all(stores.map((s) => this.restore(s)));
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Save the store into a named slot. The slot manifest is updated with the
|
|
162
|
+
* timestamp and optional metadata.
|
|
163
|
+
*/
|
|
164
|
+
async saveSlot(store, slot, opts) {
|
|
165
|
+
const payload = store.serialize();
|
|
166
|
+
await this.adapter.write(slotKey(store.id, slot), JSON.stringify(payload));
|
|
167
|
+
const entry = {
|
|
168
|
+
name: slot,
|
|
169
|
+
savedAt: Date.now()
|
|
170
|
+
};
|
|
171
|
+
if (opts?.metadata !== void 0) entry.metadata = opts.metadata;
|
|
172
|
+
await this.updateManifest(store.id, (manifest) => {
|
|
173
|
+
manifest.slots[slot] = entry;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/** Load a slot into the store. Throws `SlotNotFoundError` when missing. */
|
|
177
|
+
async loadSlot(store, slot) {
|
|
178
|
+
const raw = await this.adapter.read(slotKey(store.id, slot));
|
|
179
|
+
if (raw == null) throw new SlotNotFoundError(store.id, slot);
|
|
180
|
+
store.hydrate(JSON.parse(raw));
|
|
181
|
+
}
|
|
182
|
+
/** List slots for a store, optionally filtered by prefix. */
|
|
183
|
+
async listSlots(store, opts) {
|
|
184
|
+
const manifest = await readManifest(this.adapter, store.id);
|
|
185
|
+
const entries = Object.values(manifest.slots);
|
|
186
|
+
const filtered = opts?.prefix !== void 0 ? entries.filter((e) => e.name.startsWith(opts.prefix)) : entries;
|
|
187
|
+
return filtered.map((e) => {
|
|
188
|
+
const info = { name: e.name, savedAt: e.savedAt };
|
|
189
|
+
if (e.metadata !== void 0) info.metadata = e.metadata;
|
|
190
|
+
return info;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
/** Delete a slot. No-op when the slot doesn't exist. */
|
|
194
|
+
async deleteSlot(store, slot) {
|
|
195
|
+
await this.adapter.delete(slotKey(store.id, slot));
|
|
196
|
+
await this.updateManifest(store.id, (manifest) => {
|
|
197
|
+
if (!(slot in manifest.slots)) return;
|
|
198
|
+
const next = {};
|
|
199
|
+
for (const [name, entry] of Object.entries(manifest.slots)) {
|
|
200
|
+
if (name !== slot) next[name] = entry;
|
|
201
|
+
}
|
|
202
|
+
manifest.slots = next;
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Read-modify-write the manifest for a store, serialized through the
|
|
207
|
+
* per-store queue. The mutator runs on the freshly-read manifest; if it's
|
|
208
|
+
* a no-op the write is skipped (the mutator can opt out by leaving the
|
|
209
|
+
* passed manifest unchanged — but we always write back to keep the contract
|
|
210
|
+
* predictable; a no-op write is cheap).
|
|
211
|
+
*/
|
|
212
|
+
updateManifest(storeId, mutate) {
|
|
213
|
+
const prev = this.manifestQueues.get(storeId) ?? Promise.resolve();
|
|
214
|
+
const next = prev.then(async () => {
|
|
215
|
+
const manifest = await readManifest(this.adapter, storeId);
|
|
216
|
+
mutate(manifest);
|
|
217
|
+
await writeManifest(this.adapter, storeId, manifest);
|
|
218
|
+
});
|
|
219
|
+
this.manifestQueues.set(
|
|
220
|
+
storeId,
|
|
221
|
+
next.catch(() => void 0)
|
|
222
|
+
);
|
|
223
|
+
return next;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Subscribe to the store and persist on every change, coalesced to a single
|
|
227
|
+
* in-flight write per store. Returns a stop function — call it to
|
|
228
|
+
* unsubscribe.
|
|
229
|
+
*
|
|
230
|
+
* Writes are serialized: while a `persist()` is in flight, further changes
|
|
231
|
+
* mark the store dirty and trigger one more write *after* the current one
|
|
232
|
+
* resolves. The last-set state always wins, even on a slow async adapter,
|
|
233
|
+
* because each flush re-reads `store.serialize()` rather than capturing the
|
|
234
|
+
* value at scheduling time. Multiple synchronous `set` calls collapse into
|
|
235
|
+
* one write because the dirty flag is consumed atomically.
|
|
236
|
+
*
|
|
237
|
+
* `setTimeout` is intentionally not used here: `Save` runs alongside the
|
|
238
|
+
* page lifecycle, not the engine loop, and may be active before the engine
|
|
239
|
+
* starts or after it stops (e.g. settings menus on a paused game). Using
|
|
240
|
+
* engine-time processes here would tie persistence to a running scheduler.
|
|
241
|
+
*/
|
|
242
|
+
autoPersist(store) {
|
|
243
|
+
let inFlight = false;
|
|
244
|
+
let dirty = false;
|
|
245
|
+
let stopped = false;
|
|
246
|
+
const flush = /* @__PURE__ */ __name(async () => {
|
|
247
|
+
while (dirty && !stopped) {
|
|
248
|
+
dirty = false;
|
|
249
|
+
try {
|
|
250
|
+
await this.persist(store);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
console.error(
|
|
253
|
+
`autoPersist: failed to persist store "${store.id}":`,
|
|
254
|
+
err
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
inFlight = false;
|
|
259
|
+
}, "flush");
|
|
260
|
+
const off = store.subscribe(() => {
|
|
261
|
+
if (stopped) return;
|
|
262
|
+
dirty = true;
|
|
263
|
+
if (inFlight) return;
|
|
264
|
+
inFlight = true;
|
|
265
|
+
queueMicrotask(() => {
|
|
266
|
+
if (stopped) {
|
|
267
|
+
inFlight = false;
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
void flush();
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
return () => {
|
|
274
|
+
stopped = true;
|
|
275
|
+
off();
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
function createSave(opts) {
|
|
280
|
+
return new Save(opts);
|
|
281
|
+
}
|
|
282
|
+
__name(createSave, "createSave");
|
|
283
|
+
|
|
284
|
+
// src/keys.ts
|
|
285
|
+
var import_core = require("@yagejs/core");
|
|
286
|
+
var SaveServiceKey = new import_core.ServiceKey("save");
|
|
287
|
+
|
|
288
|
+
// src/SavePlugin.ts
|
|
289
|
+
var SavePlugin = class {
|
|
290
|
+
static {
|
|
291
|
+
__name(this, "SavePlugin");
|
|
292
|
+
}
|
|
293
|
+
name = "save";
|
|
294
|
+
version = "1.0.0";
|
|
295
|
+
options;
|
|
296
|
+
constructor(options) {
|
|
297
|
+
this.options = options;
|
|
298
|
+
}
|
|
299
|
+
install(context) {
|
|
300
|
+
context.register(SaveServiceKey, this.options.save);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// src/adapters/memory.ts
|
|
305
|
+
function memoryAdapter() {
|
|
306
|
+
const data = /* @__PURE__ */ new Map();
|
|
307
|
+
return {
|
|
308
|
+
async read(key) {
|
|
309
|
+
return data.get(key) ?? null;
|
|
310
|
+
},
|
|
311
|
+
async write(key, value) {
|
|
312
|
+
data.set(key, value);
|
|
313
|
+
},
|
|
314
|
+
async delete(key) {
|
|
315
|
+
data.delete(key);
|
|
316
|
+
},
|
|
317
|
+
async list(prefix) {
|
|
318
|
+
const out = [];
|
|
319
|
+
for (const k of data.keys()) {
|
|
320
|
+
if (k.startsWith(prefix)) out.push(k);
|
|
321
|
+
}
|
|
322
|
+
return out;
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
__name(memoryAdapter, "memoryAdapter");
|
|
327
|
+
|
|
328
|
+
// src/adapters/localStorage.ts
|
|
329
|
+
function localStorageAdapter(opts = {}) {
|
|
330
|
+
const namespace = opts.namespace ?? "yage";
|
|
331
|
+
const prefix = `${namespace}:`;
|
|
332
|
+
const ls = /* @__PURE__ */ __name(() => {
|
|
333
|
+
if (typeof window === "undefined") {
|
|
334
|
+
throw new Error(
|
|
335
|
+
"localStorageAdapter: window is not available in this environment."
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
const storage = window.localStorage;
|
|
340
|
+
if (!storage) {
|
|
341
|
+
throw new Error(
|
|
342
|
+
"localStorageAdapter: window.localStorage is not available in this environment."
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
return storage;
|
|
346
|
+
} catch (err) {
|
|
347
|
+
throw new Error(
|
|
348
|
+
"localStorageAdapter: window.localStorage is not available in this environment.",
|
|
349
|
+
{ cause: err }
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
}, "ls");
|
|
353
|
+
return {
|
|
354
|
+
async read(key) {
|
|
355
|
+
return ls().getItem(prefix + key);
|
|
356
|
+
},
|
|
357
|
+
async write(key, value) {
|
|
358
|
+
try {
|
|
359
|
+
ls().setItem(prefix + key, value);
|
|
360
|
+
} catch (err) {
|
|
361
|
+
if (err instanceof DOMException && (err.name === "QuotaExceededError" || err.name === "NS_ERROR_DOM_QUOTA_REACHED")) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`localStorageAdapter: quota exceeded while writing "${prefix + key}". Consider deleting old slots or using a different adapter.`,
|
|
364
|
+
{ cause: err }
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
throw err;
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
async delete(key) {
|
|
371
|
+
ls().removeItem(prefix + key);
|
|
372
|
+
},
|
|
373
|
+
async list(keyPrefix) {
|
|
374
|
+
const storage = ls();
|
|
375
|
+
const full = prefix + keyPrefix;
|
|
376
|
+
const out = [];
|
|
377
|
+
for (let i = 0; i < storage.length; i += 1) {
|
|
378
|
+
const k = storage.key(i);
|
|
379
|
+
if (k != null && k.startsWith(full)) {
|
|
380
|
+
out.push(k.slice(prefix.length));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return out;
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
__name(localStorageAdapter, "localStorageAdapter");
|
|
32
388
|
|
|
33
|
-
// src/
|
|
34
|
-
var
|
|
389
|
+
// src/snapshot/LocalStorageSnapshotStorage.ts
|
|
390
|
+
var LocalStorageSnapshotStorage = class {
|
|
35
391
|
static {
|
|
36
|
-
__name(this, "
|
|
392
|
+
__name(this, "LocalStorageSnapshotStorage");
|
|
37
393
|
}
|
|
38
394
|
load(key) {
|
|
39
395
|
return localStorage.getItem(key);
|
|
@@ -56,8 +412,8 @@ var LocalStorageSaveStorage = class {
|
|
|
56
412
|
}
|
|
57
413
|
};
|
|
58
414
|
|
|
59
|
-
// src/
|
|
60
|
-
var
|
|
415
|
+
// src/snapshot/SnapshotService.ts
|
|
416
|
+
var import_core2 = require("@yagejs/core");
|
|
61
417
|
var SNAPSHOT_VERSION = 4;
|
|
62
418
|
var COMPONENT_ORDER = [
|
|
63
419
|
"Transform",
|
|
@@ -71,9 +427,9 @@ var COMPONENT_ORDER = [
|
|
|
71
427
|
"ParticleEmitterComponent",
|
|
72
428
|
"TilemapComponent"
|
|
73
429
|
];
|
|
74
|
-
var
|
|
430
|
+
var SnapshotService = class {
|
|
75
431
|
static {
|
|
76
|
-
__name(this, "
|
|
432
|
+
__name(this, "SnapshotService");
|
|
77
433
|
}
|
|
78
434
|
storage;
|
|
79
435
|
context;
|
|
@@ -192,15 +548,15 @@ var SaveService = class {
|
|
|
192
548
|
}
|
|
193
549
|
}
|
|
194
550
|
buildSnapshot() {
|
|
195
|
-
const sceneManager = this.context.resolve(
|
|
551
|
+
const sceneManager = this.context.resolve(import_core2.SceneManagerKey);
|
|
196
552
|
const scenes = [];
|
|
197
553
|
for (const scene of sceneManager.all) {
|
|
198
|
-
if (!(0,
|
|
199
|
-
const type = (0,
|
|
554
|
+
if (!(0, import_core2.isSerializable)(scene)) continue;
|
|
555
|
+
const type = (0, import_core2.getSerializableType)(scene);
|
|
200
556
|
if (!type) continue;
|
|
201
557
|
const entities = [];
|
|
202
558
|
for (const entity of scene.getEntities()) {
|
|
203
|
-
if (!(0,
|
|
559
|
+
if (!(0, import_core2.isSerializable)(entity)) continue;
|
|
204
560
|
entities.push(this.serializeEntity(entity));
|
|
205
561
|
}
|
|
206
562
|
const userData = scene.serialize?.();
|
|
@@ -222,7 +578,7 @@ var SaveService = class {
|
|
|
222
578
|
}
|
|
223
579
|
} catch (err) {
|
|
224
580
|
console.error(
|
|
225
|
-
`
|
|
581
|
+
`SnapshotService: contributor "${key}" serialize failed; omitting its extras from this snapshot.`,
|
|
226
582
|
err
|
|
227
583
|
);
|
|
228
584
|
}
|
|
@@ -245,10 +601,10 @@ var SaveService = class {
|
|
|
245
601
|
`Save version mismatch: expected ${SNAPSHOT_VERSION}, got ${snapshot.version}.`
|
|
246
602
|
);
|
|
247
603
|
}
|
|
248
|
-
const sceneManager = this.context.resolve(
|
|
604
|
+
const sceneManager = this.context.resolve(import_core2.SceneManagerKey);
|
|
249
605
|
await sceneManager.popAll();
|
|
250
606
|
for (const entry of snapshot.scenes) {
|
|
251
|
-
const SceneClass =
|
|
607
|
+
const SceneClass = import_core2.SerializableRegistry.get(entry.type);
|
|
252
608
|
if (!SceneClass) {
|
|
253
609
|
throw new Error(
|
|
254
610
|
`Cannot load scene type "${entry.type}". Ensure the scene class is decorated with @serializable.`
|
|
@@ -269,7 +625,7 @@ var SaveService = class {
|
|
|
269
625
|
await contributor.restore(extras[key]);
|
|
270
626
|
} catch (err) {
|
|
271
627
|
console.error(
|
|
272
|
-
`
|
|
628
|
+
`SnapshotService: contributor "${key}" restore failed; continuing.`,
|
|
273
629
|
err
|
|
274
630
|
);
|
|
275
631
|
}
|
|
@@ -277,7 +633,7 @@ var SaveService = class {
|
|
|
277
633
|
for (const key of Object.keys(extras)) {
|
|
278
634
|
if (!this.contributors.has(key)) {
|
|
279
635
|
console.warn(
|
|
280
|
-
`
|
|
636
|
+
`SnapshotService: snapshot contains extras for "${key}" but no contributor is registered \u2014 skipping.`
|
|
281
637
|
);
|
|
282
638
|
}
|
|
283
639
|
}
|
|
@@ -289,7 +645,7 @@ var SaveService = class {
|
|
|
289
645
|
const idMap = /* @__PURE__ */ new Map();
|
|
290
646
|
const entityEntries = [];
|
|
291
647
|
for (const entityEntry of entry.entities) {
|
|
292
|
-
const EntityClass =
|
|
648
|
+
const EntityClass = import_core2.SerializableRegistry.get(entityEntry.type);
|
|
293
649
|
if (!EntityClass) {
|
|
294
650
|
console.warn(
|
|
295
651
|
`Entity type "${entityEntry.type}" not found in registry \u2014 skipping.`
|
|
@@ -331,14 +687,14 @@ var SaveService = class {
|
|
|
331
687
|
scene.afterRestore?.(entry.userData, resolver);
|
|
332
688
|
}
|
|
333
689
|
serializeEntity(entity) {
|
|
334
|
-
const type = (0,
|
|
690
|
+
const type = (0, import_core2.getSerializableType)(entity);
|
|
335
691
|
if (!type) throw new Error("Entity is not serializable");
|
|
336
692
|
const components = [];
|
|
337
693
|
for (const component of entity.getAll()) {
|
|
338
694
|
if (typeof component.serialize !== "function") continue;
|
|
339
695
|
const data = component.serialize();
|
|
340
696
|
if (data == null) continue;
|
|
341
|
-
const compType = (0,
|
|
697
|
+
const compType = (0, import_core2.getSerializableType)(component) ?? component.constructor.name;
|
|
342
698
|
components.push({ type: compType, data });
|
|
343
699
|
}
|
|
344
700
|
const userData = entity.serialize?.();
|
|
@@ -348,7 +704,7 @@ var SaveService = class {
|
|
|
348
704
|
components,
|
|
349
705
|
userData
|
|
350
706
|
};
|
|
351
|
-
if (entity.parent && (0,
|
|
707
|
+
if (entity.parent && (0, import_core2.isSerializable)(entity.parent)) {
|
|
352
708
|
result.parentId = entity.parent.id;
|
|
353
709
|
for (const [name, child] of entity.parent.children) {
|
|
354
710
|
if (child === entity) {
|
|
@@ -371,7 +727,7 @@ var SaveService = class {
|
|
|
371
727
|
});
|
|
372
728
|
const restored = [];
|
|
373
729
|
for (const snap of sorted) {
|
|
374
|
-
const CompClass =
|
|
730
|
+
const CompClass = import_core2.SerializableRegistry.get(snap.type);
|
|
375
731
|
if (!CompClass || typeof CompClass.fromSnapshot !== "function") {
|
|
376
732
|
continue;
|
|
377
733
|
}
|
|
@@ -383,33 +739,53 @@ var SaveService = class {
|
|
|
383
739
|
}
|
|
384
740
|
};
|
|
385
741
|
|
|
386
|
-
// src/keys.ts
|
|
387
|
-
var
|
|
388
|
-
var
|
|
742
|
+
// src/snapshot/keys.ts
|
|
743
|
+
var import_core3 = require("@yagejs/core");
|
|
744
|
+
var SnapshotServiceKey = new import_core3.ServiceKey(
|
|
745
|
+
"snapshotService"
|
|
746
|
+
);
|
|
389
747
|
|
|
390
|
-
// src/
|
|
391
|
-
var
|
|
748
|
+
// src/snapshot/SnapshotPlugin.ts
|
|
749
|
+
var SnapshotPlugin = class {
|
|
392
750
|
static {
|
|
393
|
-
__name(this, "
|
|
751
|
+
__name(this, "SnapshotPlugin");
|
|
394
752
|
}
|
|
395
|
-
name = "
|
|
753
|
+
name = "snapshot";
|
|
396
754
|
version = "1.0.0";
|
|
397
755
|
options;
|
|
398
756
|
constructor(options) {
|
|
399
757
|
this.options = options ?? {};
|
|
400
758
|
}
|
|
401
759
|
install(context) {
|
|
402
|
-
const storage = this.options.storage ?? new
|
|
403
|
-
const service = new
|
|
404
|
-
context.register(
|
|
760
|
+
const storage = this.options.storage ?? new LocalStorageSnapshotStorage();
|
|
761
|
+
const service = new SnapshotService(storage, context, this.options.namespace);
|
|
762
|
+
context.register(SnapshotServiceKey, service);
|
|
405
763
|
}
|
|
406
764
|
};
|
|
407
765
|
// Annotate the CommonJS export names for ESM import in node:
|
|
408
766
|
0 && (module.exports = {
|
|
409
|
-
|
|
767
|
+
InvalidKeyError,
|
|
768
|
+
LocalStorageSnapshotStorage,
|
|
769
|
+
Save,
|
|
410
770
|
SavePlugin,
|
|
411
|
-
SaveService,
|
|
412
771
|
SaveServiceKey,
|
|
413
|
-
|
|
772
|
+
SlotNotFoundError,
|
|
773
|
+
SnapshotPlugin,
|
|
774
|
+
SnapshotService,
|
|
775
|
+
SnapshotServiceKey,
|
|
776
|
+
StoreMigrationMissingError,
|
|
777
|
+
StoreVersionTooNewError,
|
|
778
|
+
VERSION,
|
|
779
|
+
createSave,
|
|
780
|
+
dateCodec,
|
|
781
|
+
defineCounter,
|
|
782
|
+
defineMap,
|
|
783
|
+
defineSet,
|
|
784
|
+
defineStore,
|
|
785
|
+
jsonCodec,
|
|
786
|
+
localStorageAdapter,
|
|
787
|
+
mapCodec,
|
|
788
|
+
memoryAdapter,
|
|
789
|
+
setCodec
|
|
414
790
|
});
|
|
415
791
|
//# sourceMappingURL=index.cjs.map
|