{"_id":"@pixygon/savedata","name":"@pixygon/savedata","description":"Unified save data system for all Pixygon applications — auto-sync, conflict detection, ownership guard","license":"MIT","homepage":"https://github.com/pixygon/pixygon-packages#readme","repository":{"type":"git","url":"git+https://github.com/pixygon/pixygon-packages.git"},"keywords":["pixygon","savedata","save","sync","persistence"],"author":{"name":"Pixygon"},"bugs":{"url":"https://github.com/pixygon/pixygon-packages/issues"},"dist-tags":{"latest":"1.1.3"},"versions":{"1.0.0":{"name":"@pixygon/savedata","version":"1.0.0","keywords":["pixygon","savedata","save","sync","persistence"],"author":{"name":"Pixygon"},"license":"MIT","_id":"@pixygon/savedata@1.0.0","maintainers":[{"name":"imakestupidgames","email":"anders@pixygon.io"}],"homepage":"https://github.com/pixygon/pixygon-packages#readme","bugs":{"url":"https://github.com/pixygon/pixygon-packages/issues"},"dist":{"shasum":"dfb69fbc9f0ba786dbe252d13b9a8d74ab40afd9","unpackedSize":55002,"fileCount":10,"integrity":"sha512-va8N+rgpHU6zgUsvN//YvCfVpY6vaV0jeT1dxHb75hZFzUzArgpg2hprW1y0z7DuoANEySzF4tJ8KJGVVUafLA==","tarball":"https://ppm.pixygon.io/@pixygon/savedata/-/savedata-1.0.0.tgz"},"main":"dist/index.js","types":"dist/index.d.ts","module":"dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"}},"gitHead":"2b6196bb338b9700aa45226040e073f9c967f1c1","scripts":{"dev":"tsup src/index.ts --format cjs,esm --dts --watch","build":"tsup src/index.ts --format cjs,esm --dts --clean","typecheck":"tsc --noEmit"},"_npmUser":{"name":"imakestupidgames","email":"anders@pixygon.io"},"repository":{"url":"git+https://github.com/pixygon/pixygon-packages.git","type":"git"},"_npmVersion":"11.12.0","description":"Unified save data system for all Pixygon applications — auto-sync, conflict detection, ownership guard","directories":{},"_nodeVersion":"25.8.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","react":"^18.2.0","typescript":"^5.3.0","@types/react":"^18.2.0"},"peerDependencies":{"react":">=17.0.0"},"_npmOperationalInternal":{"tmp":"tmp/savedata_1.0.0_1777132937818_0.3460041537655647","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@pixygon/savedata","version":"1.1.0","keywords":["pixygon","savedata","save","sync","persistence"],"author":{"name":"Pixygon"},"license":"MIT","_id":"@pixygon/savedata@1.1.0","maintainers":[{"name":"imakestupidgames","email":"anders@pixygon.io"}],"homepage":"https://github.com/pixygon/pixygon-packages#readme","bugs":{"url":"https://github.com/pixygon/pixygon-packages/issues"},"dist":{"shasum":"0df22b0c0eeaccec592e44cfab3f979b245fb112","unpackedSize":58456,"fileCount":10,"integrity":"sha512-lHTR6JVx5rcE3/EA2lwubC2UO8ioxWLxUzApnabwuhxdzeqTbdxSw8eh+EdSAaXgXxQSPnGAbMdBYnyogzhoMw==","tarball":"https://ppm.pixygon.io/@pixygon/savedata/-/savedata-1.1.0.tgz"},"main":"dist/index.js","types":"dist/index.d.ts","module":"dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"}},"gitHead":"980ac7fc3f5696b4915b19f6836e94fb84d8bad3","scripts":{"dev":"tsup src/index.ts --format cjs,esm --dts --watch","build":"tsup src/index.ts --format cjs,esm --dts --clean","typecheck":"tsc --noEmit"},"_npmUser":{"name":"imakestupidgames","email":"anders@pixygon.io"},"repository":{"url":"git+https://github.com/pixygon/pixygon-packages.git","type":"git"},"_npmVersion":"11.12.0","description":"Unified save data system for all Pixygon applications — auto-sync, conflict detection, ownership guard","directories":{},"_nodeVersion":"25.8.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","react":"^18.2.0","typescript":"^5.3.0","@types/react":"^18.2.0"},"peerDependencies":{"react":">=17.0.0"},"_npmOperationalInternal":{"tmp":"tmp/savedata_1.1.0_1777135656240_0.37661700714683555","host":"s3://npm-registry-packages-npm-production"}},"1.1.1":{"name":"@pixygon/savedata","version":"1.1.1","keywords":["pixygon","savedata","save","sync","persistence"],"author":{"name":"Pixygon"},"license":"MIT","_id":"@pixygon/savedata@1.1.1","maintainers":[{"name":"imakestupidgames","email":"anders@pixygon.io"}],"homepage":"https://github.com/pixygon/pixygon-packages#readme","bugs":{"url":"https://github.com/pixygon/pixygon-packages/issues"},"dist":{"shasum":"2a521505c626fa73400aca6985d3e039af25f787","unpackedSize":60497,"fileCount":11,"integrity":"sha512-q4ctPTBrG1gzfRCP3L3UMpGCwwUMyJR8V4LbYIHRHIP52dshIBqB7F1aOHALfsYZ5eZyILuci3mmGfvC8sjVzA==","tarball":"https://ppm.pixygon.io/@pixygon/savedata/-/savedata-1.1.1.tgz"},"main":"dist/index.js","types":"dist/index.d.ts","module":"dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"}},"gitHead":"b128985580bd7300e80cac7b2693a2128fed5978","scripts":{"dev":"tsup src/index.ts --format cjs,esm --dts --watch","build":"tsup src/index.ts --format cjs,esm --dts --clean","typecheck":"tsc --noEmit"},"_npmUser":{"name":"imakestupidgames","email":"anders@pixygon.io"},"repository":{"url":"git+https://github.com/pixygon/pixygon-packages.git","type":"git"},"_npmVersion":"11.12.0","description":"Unified save data system for all Pixygon applications — auto-sync, conflict detection, ownership guard","directories":{},"_nodeVersion":"25.8.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","react":"^18.2.0","typescript":"^5.3.0","@types/react":"^18.2.0"},"peerDependencies":{"react":">=17.0.0"},"_npmOperationalInternal":{"tmp":"tmp/savedata_1.1.1_1777563836506_0.3701388497737137","host":"s3://npm-registry-packages-npm-production"}},"1.1.2":{"name":"@pixygon/savedata","version":"1.1.2","description":"Unified save data system for all Pixygon applications — auto-sync, conflict detection, ownership guard","main":"dist/index.js","module":"dist/index.mjs","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"}},"scripts":{"build":"tsup src/index.ts --format cjs,esm --dts --clean","dev":"tsup src/index.ts --format cjs,esm --dts --watch","typecheck":"tsc --noEmit"},"peerDependencies":{"react":">=17.0.0"},"devDependencies":{"@types/react":"^18.2.0","react":"^18.2.0","tsup":"^8.0.0","typescript":"^5.3.0"},"keywords":["pixygon","savedata","save","sync","persistence"],"author":{"name":"Pixygon"},"license":"MIT","publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/pixygon/pixygon-packages.git"},"gitHead":"659d78682bd763aadf0c2cb1cfaa00a90ce62907","_id":"@pixygon/savedata@1.1.2","bugs":{"url":"https://github.com/pixygon/pixygon-packages/issues"},"homepage":"https://github.com/pixygon/pixygon-packages#readme","_nodeVersion":"25.8.1","_npmVersion":"11.12.0","dist":{"integrity":"sha512-XcvE8+ralQGVI8kL9q4xhKbLn9Zp82KYVRXtNrTqqfPD0fdyvlNtxJRq5v1sZvvUIjBmpE0EUrZwCVTsf5vcvw==","shasum":"33e67724bba525643a7c3ab4bda10956185526f9","unpackedSize":61226,"fileCount":11,"tarball":"https://ppm.pixygon.io/@pixygon/savedata/-/savedata-1.1.2.tgz"},"_npmUser":{"name":"imakestupidgames","email":"anders@pixygon.io"},"directories":{},"maintainers":[{"name":"imakestupidgames","email":"anders@pixygon.io"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/savedata_1.1.2_1777653524446_0.04147916123281892"},"_hasShrinkwrap":false},"1.1.3":{"name":"@pixygon/savedata","version":"1.1.3","description":"Unified save data system for all Pixygon applications — auto-sync, conflict detection, ownership guard","main":"dist/index.js","module":"dist/index.mjs","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"}},"scripts":{"build":"tsup src/index.ts --format cjs,esm --dts --clean","dev":"tsup src/index.ts --format cjs,esm --dts --watch","typecheck":"tsc --noEmit","verify":"node verify.mjs"},"peerDependencies":{"react":">=17.0.0"},"devDependencies":{"@types/react":"^18.2.0","react":"^18.2.0","tsup":"^8.0.0","typescript":"^5.3.0"},"files":["dist","src","verify.mjs"],"keywords":["pixygon","savedata","save","sync","persistence"],"author":{"name":"Pixygon"},"license":"MIT","publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/pixygon/pixygon-packages.git"},"bin":{"pixygon-verify-savedata":"verify.mjs"},"dist":{"integrity":"sha512-8IatpBfhsd9D/3mhCW/WLccmhRbQXvVVOQEESNHD0bHChl52u1rp/HCZG81vyPci/gyi1ovLKuI3PvYrCUJBIA==","shasum":"42d647e0d8219ef46553dc2a2f1dece9844ceeec","tarball":"https://ppm.pixygon.io/@pixygon/savedata/-/savedata-1.1.3.tgz"},"readmeFilename":"README.md","_id":"@pixygon/savedata@1.1.3","bugs":{"url":"https://github.com/pixygon/pixygon-packages/issues"},"homepage":"https://github.com/pixygon/pixygon-packages#readme","_integrity":"sha512-8IatpBfhsd9D/3mhCW/WLccmhRbQXvVVOQEESNHD0bHChl52u1rp/HCZG81vyPci/gyi1ovLKuI3PvYrCUJBIA==","_resolved":"/home/pixygon/handoff/npm-stage-2026-09-30-raptor3/pixygon-savedata-1.1.3.tgz","_from":"file:pixygon-savedata-1.1.3.tgz","_nodeVersion":"26.10.0","_npmVersion":"12.1.0"}},"time":{"1.0.0":"2026-04-25T16:02:17.950Z","modified":"2026-09-30T23:31:30.068Z","created":"2026-04-25T16:02:17.950Z","1.1.0":"2026-04-25T16:47:36.378Z","1.1.1":"2026-04-30T15:43:56.656Z","1.1.2":"2026-05-01T16:38:44.683Z","1.1.3":"2026-09-30T23:31:30.068Z"},"readme":"# @pixygon/savedata\n\nUnified, server-synced save data for Pixygon apps — auto-save, dirty tracking,\nrevision-based conflict detection, and an ownership guard. **6 repos** depend\non it (Tastebud, Kartograf, CoreFeed, SoloBattle, pixygon.io, pixiel.ai), and\nseveral other pearls (`@pixygon/social`, `/quests`, `/inventory`, `/anima`)\nstore their state through the same endpoints.\n\nBacked by `/v1/savedata/:projectId/:userId` on the Pixygon API. The Unity\ncounterpart is `com.pixygon.saving` (related, deliberately separate).\n\n---\n\n## The mental model\n\nA **save** is `projectId → userId → slot → { section: {...}, section: {...} }`.\n\n- `projectId` is the **save namespace**, a free-form string the server keys on.\n  The estate convention is the repo's `.pixygon.json` **ObjectId**. Changing it\n  later orphans every existing save — treat it as permanent.\n- Apps normally mount **two** providers: the shared **global** project\n  `65481fec93ee5f866ade917f` (cross-app skills/XP/inventory) and the app's own.\n- Sections are independent; only dirty ones are sent.\n\n---\n\n## Retrofitting it into an existing app (≈15 minutes)\n\n### 1. Install\n\n```bash\nnpm i @pixygon/savedata\n```\n\nRequires `@pixygon/auth` (or any token source) — the config needs a live token\nand user id.\n\n### 2. Mount the provider(s) — inside AuthProvider\n\n```tsx\n// src/providers/SaveDataProvider.tsx — the estate pattern (Tastebud/Kartograf)\nimport { SaveProvider } from '@pixygon/savedata'\nimport { useToken, useAuth } from '@pixygon/auth'\nimport type { SaveConfig } from '@pixygon/savedata'\n\nconst API_URL = import.meta.env.VITE_API_URL || 'https://api.pixygon.com/v1'\n\nfunction GlobalSaveProvider({ children }: { children: ReactNode }) {\n  const { getAccessToken } = useToken()\n  const { user } = useAuth()\n  const config: SaveConfig = {\n    baseUrl: API_URL,\n    projectId: '65481fec93ee5f866ade917f',   // shared cross-app project\n    getToken: getAccessToken,\n    getUserId: () => (user as any)?._id ?? null,\n    autoSaveInterval: 15_000,\n  }\n  return <SaveProvider config={config}>{children}</SaveProvider>\n}\n\nfunction AppSaveProvider({ children }: { children: ReactNode }) {\n  /* same, with projectId = this repo's .pixygon.json ObjectId, 10s interval */\n}\n\nexport function SaveDataProvider({ children }: { children: ReactNode }) {\n  return <GlobalSaveProvider><AppSaveProvider>{children}</AppSaveProvider></GlobalSaveProvider>\n}\n```\n\nProviders nest; each gets its own React context keyed by `projectId`.\n\n**All four of `baseUrl`, `projectId`, `getToken`, `getUserId` are required.**\nWith no `getUserId` the provider never loads and never saves — silently.\n\n### 3. Read and write\n\n```tsx\nimport { useSavedata, useSavedataSection } from '@pixygon/savedata'\n\n// whole save for one namespace\nconst { data, isLoaded, syncStatus, revision, updateSection, mergeSection, forceSave, forceLoad } =\n  useSavedata<MySave>('69ee9b3f7a116f8ba90d0890')\n\nupdateSection('lists', { items })          // replace a section\nmergeSection('prefs', { theme: 'dark' })   // shallow-merge into a section\n\n// one section, with a default\nconst { data: skills, update, merge } =\n  useSavedataSection('skills', { skillXp: {}, totalXp: 0 }, '65481fec93ee5f866ade917f')\n```\n\n⚠ **`useSavedata('<id>')` with an id that has no mounted provider THROWS**\n(`no SaveProvider for projectId \"…\"`). With multiple providers mounted, always\npass the id explicitly — the no-arg form resolves to the *last mounted*\nprovider, which is order-dependent and easy to get wrong.\n\n### 4. Verify it worked\n\n```bash\nnode node_modules/@pixygon/savedata/verify.mjs\n# or:\npearl verify savedata\n```\n\nChecks: installed, `<SaveProvider>` mounted, all four `SaveConfig` fields\npresent, a non-placeholder `projectId` (flagging slugs and unexpected\nObjectIds), a real `baseUrl`, and that **every** `useSavedata(projectId)` call\ntargets a mounted provider. Exit 0 pass, 1 fail.\n\n**What verify cannot prove:** that a save round-trips. Do it by hand once:\nsign in → change something → wait for `syncStatus: 'synced'` → hard-reload →\nconfirm it survived. Watch the network tab for `409` (revision conflict) and\n`403` (ownership guard).\n\n---\n\n## API\n\n| Export | Use |\n|---|---|\n| `<SaveProvider config>` | the mount (one per save namespace) |\n| `useSavedata<T>(projectId?)` | `data, isLoaded, syncStatus, revision, lastSyncedAt, error, updateSection, mergeSection, forceSave, forceLoad` |\n| `useSavedataSection<T>(key, default, projectId?)` | `data, isLoaded, syncStatus, update, merge` |\n| `useSaveContext(projectId?)` | raw context (throws if not mounted) |\n| `loadFromServer` / `mergeToServer` / `updateSectionOnServer` | imperative, outside React |\n\n### SaveConfig\n\n```ts\n{\n  baseUrl: string                  // https://api.pixygon.com/v1\n  projectId: string                // the save namespace — permanent\n  getToken: () => string | null\n  getUserId: () => string | null\n  slot?: number                    // default 0\n  autoSaveInterval?: number        // default 10_000 ms (0 = off)\n  saveOnVisibilityHidden?: boolean // default true\n  saveOnBeforeUnload?: boolean     // default true\n  debug?: boolean\n}\n```\n\n`syncStatus`: `idle | loading | synced | saving | conflict | error`.\n\n## Safety features (and what they mean when they fire)\n\n- **Ownership guard** — saves are blocked when `getUserId()` changes mid-session\n  (account switch). Server-side `enforceOwnership` rejects cross-user writes\n  with `403`.\n- **Revision tracking** — the client sends `expectedRevision`; a `409` means\n  another tab/device wrote first. `syncStatus` becomes `conflict` and the\n  server's data comes back in the error payload.\n- **Merge-on-load** — for a same-user reload, local and server data are merged\n  (max wins) instead of blindly overwriting.\n- **Dirty tracking** — only changed sections are sent.\n\n## Gotchas\n\n- **The namespace is permanent.** Changing `projectId` silently orphans every\n  existing save. Never \"tidy\" an ObjectId into a slug.\n- **`keepalive` caps at ~60 KB.** Saves larger than that lose their\n  best-effort chance of surviving a tab close — call `forceSave()` earlier.\n- **The no-arg hook is order-dependent.** See step 3.\n- **Cross-app XP lives in the global project on purpose.** `@pixygon/social`'s\n  `useSkillsSync` writes to `65481fec93ee5f866ade917f` no matter which app it\n  runs in. Don't repoint it at your app.\n- **Some repos mount only the global provider** (e.g. CoreFeed) — that means\n  the app has no private save namespace. Fine if deliberate; check it is.\n\n## Publishing\n\n```bash\nnpm run build && npm publish --access public\n```\n","readmeFilename":"README.md"}