Skip to content

fix(loader): persist entry config only after a committed update - #77

Open
tdwhere123 wants to merge 2 commits into
cordiverse:mainfrom
tdwhere123:fix/loader-persist-committed-update
Open

fix(loader): persist entry config only after a committed update#77
tdwhere123 wants to merge 2 commits into
cordiverse:mainfrom
tdwhere123:fix/loader-persist-committed-update

Conversation

@tdwhere123

Copy link
Copy Markdown

Frame

Cordis today is a declarative lifecycle + dependency-graph kernel with cooperative composability: plugins declare effects, and correctness of unload depends on those plugins (or the host APIs they used) supplying inverses. Host-mediated paths (ctx.timeout / ctx.interval, ctx.on, ctx.provide) already invert themselves. Bare setInterval / sockets / files do not. This PR does not claim that unload clears those, and it does not turn Core into a capability or resource-ownership kernel.

That split is the useful one for later work:

Layer Question This PR
Outer Can the runtime own side effects so correctness does not depend on plugin-authored inverses? No. Research only.
Inner Even when every plugin cooperates, is Desired → Committed → Persistence honest? Yes. Loader persistence only.

Desired is the payload someone asked for (fiber.update(config)). Committed is the payload the runtime actually accepted (inner assigned a new fiber.config, or Group assigned subgroup.data). Persistence is what tree.write() will later round-trip as the graph.

Today those three are implicit. Inner assigns fiber.config then restart() (packages/core/src/fiber.ts). Persistence used to run before next(), so a veto could write Desired while runtime stayed on the old object. This PR makes one local invariant true:

Only a committed entry transition may hit Loader persistence.

Cite this PR for later inner-layer work (Include same-path intercept, HMR mixed generation, a first-class Reconcile / Desired≠Committed model). Do not cite it as effect-ownership being closed — that remains disposer tracking, not resource ownership.

Problem

The persist hook on internal/update wrote entry.options.config and called tree.write() before next(). An empty fiber-local hook is a waterfall veto. Runtime then kept the old config; disk/in-memory persistence already had the new one.

Group is a second, real commit path: @cordisjs/plugin-group swallows next() and runs EntryGroup.update. That is not a veto, so Group still has to persist — but the YAML group: true flag is not “Group intercept ran.”

cordisjs/cordis#19 (child ctx.plugin() + dispose() must not disable the parent entry) already holds on current main via internal/plugin case 3. This PR only locks it; it does not retouch that hook (#66 occupies the later inject-meta edit).

Change

Persist after next() (and after a thenable settles). Then write only if the payload was accepted:

  • ordinary plugin: this.config !== previous, where previous is snapshotted before next() (inner assigned a new reference)
  • Group: entry.subgroup?.data === config (EntryGroup.update assigns this.data = config synchronously) — not YAML group: true
  • failed apply: this.state === FiberState.FAILED skips the write (inner may already have assigned fiber.config)
  • noSave and child-fiber updates still skip the writer

Same-path Include intercept also swallows next() and has no Group signal. This PR treats that as non-commit and does not edit Include (#47 / #57 / #74 already occupy that file). Old persist-before-next() wrote Include plugin config even when Include ignored the new object; stopping that write is intentional.

Tests: packages/loader/tests/persist-order.spec.ts. MockLoader write() snapshots with structuredClone and counts calls, so loader.data is not an alias of live root.data. Empty internal/update hooks in index.spec.ts now call next() (an empty hook is a veto; the old self-update tests were documenting persist-before-veto).

Test plan

  • yarn test loader — ordinary commit, local/global/async veto, Group with and without YAML group, Group-shaped intercept veto, same-reference veto, FAILED restart, delayed next(), same-turn fiber.update does not write yet, noSave, child dispose (Loader 将子插件 dispose 视为整个插件 dispose #19 lock)
  • veto a group-flagged entry: persistence stays old
  • Group plugin without group: true: persistence still updates when intercept assigns subgroup.data
  • plugin apply throws on reload: FiberState.FAILED, tree.write not called
  • yarn test include still green (no Include source change)

Out of scope

  • events.ts / fiber.ts / Include implementation
  • HMR mixed-generation reload (#66 inserts into partialReload)
  • SES / capability / an ownership ledger in Core
  • claiming Fiber.effect tracking means unload owns the real world

Write the loader tree after the internal/update chain commits so a veto
cannot leave persistence ahead of the live fiber.
YAML group and config identity were not commit. Snapshot the previous
fiber.config, require subgroup.data === config for Group, and skip FAILED apply.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant