Skip to content

#506 Stores: one link per Store, connections bind to providers only #509

Open

Description

@yifanxuaaa

Sub-issue of #506. Owner direction 2026-10-30: each local SQLite Store links to at most one remote file, and a remote file is linked by at most one Store of the home. A connection (MinIO, Cloudflare R2; many of each) binds to the provider only, never to a Store.

Spec: core/docs/issues/506/links.md on branch issue506-store-backends-remote (PR #507). The Connect design that uses it lives in Connect's own repository (docs/ui/storage-links.md).

Why

The runtime's stores.json keeps a list of copies per Store (one per remote), while the native remote-state/<store> keeps one last transfer. They disagree:

  • team-examples was pushed to local-minio, then to r2. A new push to local-minio is refused as "the remote copy moved" although MinIO holds exactly what was pushed: the check compares against R2's record.
  • A clone is checked at a path named after the clone, where nothing was pushed, so every clone reads "missing".
  • The object path is the Store's name, so a Store cannot track a file of another name.

Model

  • Connection: provider, endpoint or account, bucket, prefix, credential reference. No Store field.
  • SQLite Store: link: { remote, file, linked, last } | null replaces remote and copies. file names the object <prefix>/<file>/store.sqlite.
  • One to one per home, checked under the registry write lock.
  • PostgreSQL Stores: unchanged, never linked.

Calls

Call Does
store.start_publish(store, { remote, file }) an unlinked Store to a new file, linked; Exists if the file is there or linked, Linked if the Store already is
store.start_push(store, { force }) to its link; NotLinked; Conflict against the link's own record
store.start_pull(store, { confirm }) from its link; NotLinked
store.start_send(store, { remote, file }) a copy to a new, unlinked file; the Store's link does not change
store.start_load(remote, { file, store }) a new local Store linked to the file; Linked if already linked; replaces start_clone
store.unlink(store, { confirm }) drops the link; the file stays
store.check(store) HEAD of the linked file
remote.list_stores(remote) each file gains linkedBy
remote.remove(name) InUse while a Store links to it

Removed: start_clone, the remote argument of push and pull, copies, the Store-level remote field. The native record gains file and ms times.

Phases

  • 1. Engine and runtime: the calls above, native file in remote-state, tests (one-to-one, refusals, the team-examples conflict case), README §6 updated. Bundle with layerfs-node/postgresql,layerfs-daemon/postgresql,layerfs-node/tls.
  • 2. Connect (private): host view and routes on one link; Storage page with one Linked-to cell per Store, a Connections section with each bucket's files (Load / Open), Publish, Send a copy, Load, Unlink and push-conflict dialogs; Clone and copy lists removed.
  • 3. Settle today's data (after 1 and 2 work): choose each Store's link from evidence (native record plus remote sha256), write the new records, give every link a baseline push (conflict-checked, never forced), fix the running Connect host's empty heads, and post the before/after table here.

Done when

  • Every SQLite Store is linked and in sync, or local only.
  • No link reads unknown or missing, and no file is linked twice.
  • Every file in each connection reads linked or not loaded.
  • Connect's Storage page shows exactly that.
  • Each commit records production LOC, and every test command is bounded at 120 s.

Activity

  1. yifanxuaaa commented on Oct 29, 2026

    @yifanxuaaa
    ContributorAuthor

    Phase 3: today's data settled (~/.layerfs-connect/home, 2026-10-30)

    Engine and runtime: f65efb4 on PR #507. Connect's host and Storage page are rebuilt for one link per Store (Connect's own repository, not committed there). Connect now runs the worker bundle build-linux-a1b3c1d6… (source f65efb4, release, layerfs-node/postgresql,layerfs-daemon/postgresql,layerfs-node/tls).

    Evidence (step 1): the HEAD sha256 of every file, read through remote.list_stores:

    File sha256 Matches
    r2 / r2-examples b440dfca… r2-examples's record (push); r2-examples-clone pulled it
    r2 / services c5497495… services's record (push)
    r2 / team-examples dc2903cb… team-examples's record (push); team-examples-r2 pulled it
    local-minio / services 2be72dc7… services' older MinIO push (no record)
    local-minio / team-examples dc2903cb… team-examples' older MinIO push; team-examples-copy's record (pull)

    Before and after (after = Connect's /api/storage following a check):

    Store Before After
    team-examples copies on local-minio and r2; record r2 linked r2 / team-examples, in sync
    services copies on local-minio and r2; record r2 linked r2 / services, in sync
    r2-examples copy on r2 linked r2 / r2-examples, in sync
    team-examples-copy clone of local-minio / team-examples, read missing linked local-minio / team-examples (no other Store claims it), in sync
    team-examples-r2 clone of r2 / team-examples, read missing local only (team-examples claims the file)
    r2-examples-clone clone of r2 / r2-examples, read missing local only (r2-examples claims the file)
    dsa, heavy-check, playground local local only
    connect-test-13321, -73419, -84622, -9697 local local only, untouched (owner's decision)
    team-examples-pg, services-pg PostgreSQL Live, unchanged
    File After
    r2 / r2-examples, r2 / services, r2 / team-examples linked by the Store of the same name
    local-minio / team-examples linked by team-examples-copy
    local-minio / services not loaded

    No link reads unknown or missing, and no file is linked twice.

    How (steps 3–4): the four links were written with the runtime's own registry.link under the registry lock (stores.json is now version 2; the version 1 file stays as stores.v1.json). Each native record was rewritten with file and ms times. The six records from before #509 are kept in home/remote-state.v1/; the two clones' records were moved there, not deleted. Each linked Store was then pushed through Connect with the conflict check (never forced): all four were accepted, with no refusals. Each pushed sha256 equals the one before, so the local Stores had not changed; the push recorded heads, and the next check read in sync from equal heads on both sides.

    Step 5, the host's empty heads: not reproduced, cause not found. After the worker swap, the running Connect host and a fresh process both read heads for all 13 SQLite Stores, including with every Store held open by the worker. The old host process was gone before this work started, so it could not be inspected. What is known:

    • runtime.store.stat was already in the old bundle (00665aa721).
    • The worker client re-reads endpoint.json on every call, so a stale endpoint is not the cause.
    • api/runtime/src/store/calls.js locals() turns any stat failure into heads: null for every Store, with no trace. That is why the cause left no evidence.

    Making that failure visible is the next step if the gap returns.

    Not done here: no screenshot of the Storage page (checked through the same host API the page renders); stores_real.mjs and storage_real.mjs E2E not run. The first R2 check after the worker swap, and one R2 listing from a fresh process, failed with "connection closed before the reply" (Unknown, not resent); the next calls succeeded.

  2. added theissue type on Oct 31, 2026
  3. added this to the LayerFS 2.0 milestone on Oct 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions