@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 CHANGED
@@ -1,6 +1,7 @@
1
1
  # @yagejs/save
2
2
 
3
- Save and load game state for the [YAGE](https://yage.dev) 2D game engine.
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
- ## Usage
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 { SavePlugin, SaveServiceKey } from "@yagejs/save";
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
- Mark components as serializable and save the world:
45
+ In-game components resolve the registered Save through `SaveServiceKey`:
22
46
 
23
47
  ```ts
24
- import { serializable } from "@yagejs/core";
25
-
26
- @serializable("player-stats")
27
- class PlayerStats extends Component {
28
- constructor(public hp = 100, public xp = 0) { super(); }
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
- // Save
32
- const saveService = engine.context.resolve(SaveServiceKey);
33
- await saveService.save("slot-1");
78
+ @serializable
79
+ class Player extends Entity { /* ... */ }
34
80
 
35
- // Load
36
- await saveService.load("slot-1");
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
- - **SavePlugin / SaveService** - snapshot the entire engine state
42
- - **Pluggable storage** - `LocalStorageSaveStorage` included; implement `SaveStorage` for custom backends (IndexedDB, files, remote, etc.)
43
- - **@serializable decorator** - opt components into save/load
44
- - **Typed slots** - multiple save slots with metadata
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
- LocalStorageSaveStorage: () => LocalStorageSaveStorage,
24
+ InvalidKeyError: () => InvalidKeyError,
25
+ LocalStorageSnapshotStorage: () => LocalStorageSnapshotStorage,
26
+ Save: () => Save,
25
27
  SavePlugin: () => SavePlugin,
26
- SaveService: () => SaveService,
27
28
  SaveServiceKey: () => SaveServiceKey,
28
- VERSION: () => import_core3.VERSION
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 import_core3 = require("@yagejs/core");
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/LocalStorageAdapter.ts
34
- var LocalStorageSaveStorage = class {
389
+ // src/snapshot/LocalStorageSnapshotStorage.ts
390
+ var LocalStorageSnapshotStorage = class {
35
391
  static {
36
- __name(this, "LocalStorageSaveStorage");
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/SaveService.ts
60
- var import_core = require("@yagejs/core");
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 SaveService = class {
430
+ var SnapshotService = class {
75
431
  static {
76
- __name(this, "SaveService");
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(import_core.SceneManagerKey);
551
+ const sceneManager = this.context.resolve(import_core2.SceneManagerKey);
196
552
  const scenes = [];
197
553
  for (const scene of sceneManager.all) {
198
- if (!(0, import_core.isSerializable)(scene)) continue;
199
- const type = (0, import_core.getSerializableType)(scene);
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, import_core.isSerializable)(entity)) continue;
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
- `SaveService: contributor "${key}" serialize failed; omitting its extras from this snapshot.`,
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(import_core.SceneManagerKey);
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 = import_core.SerializableRegistry.get(entry.type);
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
- `SaveService: contributor "${key}" restore failed; continuing.`,
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
- `SaveService: snapshot contains extras for "${key}" but no contributor is registered \u2014 skipping.`
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 = import_core.SerializableRegistry.get(entityEntry.type);
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, import_core.getSerializableType)(entity);
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, import_core.getSerializableType)(component) ?? component.constructor.name;
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, import_core.isSerializable)(entity.parent)) {
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 = import_core.SerializableRegistry.get(snap.type);
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 import_core2 = require("@yagejs/core");
388
- var SaveServiceKey = new import_core2.ServiceKey("saveService");
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/SavePlugin.ts
391
- var SavePlugin = class {
748
+ // src/snapshot/SnapshotPlugin.ts
749
+ var SnapshotPlugin = class {
392
750
  static {
393
- __name(this, "SavePlugin");
751
+ __name(this, "SnapshotPlugin");
394
752
  }
395
- name = "save";
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 LocalStorageSaveStorage();
403
- const service = new SaveService(storage, context, this.options.namespace);
404
- context.register(SaveServiceKey, service);
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
- LocalStorageSaveStorage,
767
+ InvalidKeyError,
768
+ LocalStorageSnapshotStorage,
769
+ Save,
410
770
  SavePlugin,
411
- SaveService,
412
771
  SaveServiceKey,
413
- VERSION
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