[Storage]

Script library backends: local (IndexedDB), cloud (Worker /api/scripts), and git (GitHub/GitLab).

Storage

Abstract

Storage plugins persist the user’s Pine library and drafts. Built-ins: local, cloud, git — registered from frontend/src/storage/catalog.ts. High-level UI/editor APIs go through frontend/src/storage/service.ts, which always dual-writes drafts to local for crash recovery.

Dynamic kind: 'storage' install via URL is not supported (loader.ts throws).

Conceptual model

Built-in plugins

local (frontend/src/storage/local.ts)

ConcernDetail
PrimaryIndexedDB pynescript.axis.storage — stores scripts, kv
FallbacklocalStorage keys pynescript.axis.library.v1, draft keys
MigrationLegacy SuperChart keys (pynescript.superchart.library.v1, editor docs)
MemoryIn-process maps when neither IDB nor LS exist (tests/SSR)
CapabilitiesOffline-friendly

cloud (frontend/src/storage/cloud.ts)

ConcernDetail
Transportfetch to Worker /api/scripts
AuthAuthorization: Bearer <api_key> (pn_…)
Configendpoint (default http://127.0.0.1:8787), apiKey
ConcurrencyIf-Match revision headers on write
PartitionWorker hashes key → userId; rows scoped per user

git (frontend/src/storage/git.ts)

ConcernDetail
ProvidersGitHub Contents API / GitLab repository files
Configprovider, token, owner, repo, branch, basePath (default pine-library), commit template
Commit boundaryExplicit Save only — not every keystroke
DraftssaveDraft / loadDraft are no-ops; local dual-write handles drafts
CapabilitiesneedsNetwork, needsAuth

Interface surface

Contract: StoragePlugin.

service.ts API (UI-facing)

FunctionBehavior
listScripts(prefix?)Active backend list
readScript / writeScript / removeScriptCRUD + log
saveDraft / loadDraftLocal first; also active if supported
exportLibraryJson / importLibraryJsonPortable backup
getStorageStatusConnected / dirty / remote hints

Catalog helpers

ensureStoragesRegistered()
getStorage(id)
listStorages()
registerDynamicStorage(plugin)  // in-process only (tests / future)
unregisterDynamicStorage(id)

Internals

PathRole
storage/catalog.tsRegistration
storage/local.tsIDB + LS
storage/idb.tsPromise wrappers for IDB
storage/cloud.tsWorker client
storage/git.tsOrchestration
storage/git-github.ts / git-gitlab.tsProvider APIs
storage/git-config.tsConfig normalize
storage/service.tsFacade
worker/src/scripts.tsCloud API implementation
worker/schemas/scripts.sqlD1 schema

D1 schema (cloud)

PRIMARY KEY (user_id, id)
-- scripts + script_drafts tables

Without D1, Worker keeps per-isolate in-memory maps (fine for wrangler dev).

Invariants & edge cases

  1. Draft recovery — never rely solely on remote drafts; service always hits local.
  2. Git PAT scope — GitHub contents:write; GitLab api / write_repository.
  3. Revisions — local uses local-${timestamp}; cloud/git use remote revisions for conflict detection.
  4. Active defaultgetActiveStorageId()local when unset.

Worked examples

Export / import

const docs = await exportLibraryJson();
// …move machine…
await importLibraryJson(docs, { forceNewIds: true });

Point cloud at local Worker

  1. cd frontend/worker && npm run dev:8787
  2. Manager → storage cloud
  3. Config endpoint http://127.0.0.1:8787, key from /api/keys or ALLOW_OPEN_KEYS=1 demo key

Failure modes

ErrorCause
Cloud storage requires an API keyEmpty apiKey
401 NO_KEY / INVALID_KEYAuth path; see Worker auth
409 on writeIf-Match revision conflict
Git 401/404Token, owner/repo, or basePath wrong
Quota errors on localStorageLarge libraries should prefer IDB (automatic when available)

See also