@zhin.js/plugin-runtime 1.1.17 → 1.1.18

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/lib/snapshot.js CHANGED
@@ -11,18 +11,20 @@ export class GenerationConflictError extends Error {
11
11
  }
12
12
  const snapshotLeaseAuthority = Symbol('SnapshotLeaseAuthority');
13
13
  export class SnapshotLease {
14
+ #released = false;
14
15
  value;
15
16
  releaseRecord;
16
- retirementSignal;
17
- #released = false;
18
- constructor(value, releaseRecord, authority,
19
17
  /** Aborted on publication of a replacement or Root close, before lease disposal. */
20
- retirementSignal) {
21
- this.value = value;
22
- this.releaseRecord = releaseRecord;
23
- this.retirementSignal = retirementSignal;
18
+ retirementSignal;
19
+ /** Root closure only; shared by leases from every generation and never aborted by HMR. */
20
+ stopSignal;
21
+ constructor(input, authority) {
24
22
  if (authority !== snapshotLeaseAuthority)
25
23
  throw new TypeError('SnapshotLease is owned by SnapshotStore');
24
+ this.value = input.value;
25
+ this.releaseRecord = input.releaseRecord;
26
+ this.retirementSignal = input.retirementSignal;
27
+ this.stopSignal = input.stopSignal;
26
28
  }
27
29
  get active() {
28
30
  return !this.#released;
@@ -38,11 +40,14 @@ export class SnapshotStore {
38
40
  onDisposalError;
39
41
  #current;
40
42
  #retired = new Set();
41
- #leases = new WeakSet();
43
+ #leases = new WeakMap();
44
+ #rootResource;
42
45
  #closed = false;
46
+ #stop = new AbortController();
43
47
  constructor(initial, onDisposalError = () => undefined, ...state) {
44
48
  this.onDisposalError = onDisposalError;
45
49
  const initialState = state[0];
50
+ this.#rootResource = state[1];
46
51
  const snapshot = createSnapshotView(0, initial);
47
52
  const committed = Object.freeze({ snapshot, state: initialState });
48
53
  const admissions = collectSnapshotAdmissions(snapshot);
@@ -57,7 +62,8 @@ export class SnapshotStore {
57
62
  retired: false,
58
63
  };
59
64
  this.#current = record;
60
- replaceGenerationAdmissions(new Set(), admissions, this, () => undefined, () => this.#retain(record));
65
+ replaceGenerationAdmissions({ previous: new Set(), next: admissions, owner: this,
66
+ publishNext: () => undefined, acquireNext: () => this.#retain(record), stopSignal: this.#stop.signal });
61
67
  }
62
68
  get current() {
63
69
  return this.#current.snapshot;
@@ -69,14 +75,43 @@ export class SnapshotStore {
69
75
  acquire() {
70
76
  if (this.#closed)
71
77
  throw new Error('SnapshotStore is closed');
72
- const record = this.#current;
73
- const lease = new SnapshotLease(record.snapshot, this.#retain(record), snapshotLeaseAuthority, record.retirement.signal);
74
- this.#leases.add(lease);
78
+ return this.#lease(this.#current);
79
+ }
80
+ retain(lease) {
81
+ if (this.#closed)
82
+ throw new Error('SnapshotStore is closed');
83
+ const record = this.#leases.get(lease);
84
+ if (!record || !lease.active)
85
+ throw new Error('Snapshot retention requires an active owned lease');
86
+ return this.#lease(record);
87
+ }
88
+ #lease(record) {
89
+ const releaseRecord = this.#retain(record);
90
+ const lease = new SnapshotLease({
91
+ value: record.snapshot,
92
+ releaseRecord: () => {
93
+ this.#leases.set(lease, null);
94
+ releaseRecord();
95
+ },
96
+ retirementSignal: record.retirement.signal,
97
+ stopSignal: this.#stop.signal,
98
+ }, snapshotLeaseAuthority);
99
+ this.#leases.set(lease, record);
75
100
  return lease;
76
101
  }
77
102
  owns(lease) {
78
103
  return this.#leases.has(lease);
79
104
  }
105
+ rootResource(lease, token) {
106
+ const record = this.#leases.get(lease);
107
+ if (!record || !lease.active)
108
+ throw new Error('Root resource access requires an active owned SnapshotLease');
109
+ // Manually authored snapshots retain their explicit resource contract. Real
110
+ // Runtime Roots inject the same-generation Scope resolver, including privacy checks.
111
+ return (this.#rootResource
112
+ ? this.#rootResource(record.state, record.snapshot.root, token)
113
+ : record.snapshot.resources.get(record.snapshot.root)?.get(token.id));
114
+ }
80
115
  commit(expectedGeneration, prepared) {
81
116
  // The caller owns a candidate until this method succeeds. This lets the
82
117
  // transaction authority undo handoff state before disposing a rejection.
@@ -101,7 +136,8 @@ export class SnapshotStore {
101
136
  leases: 0,
102
137
  retired: false,
103
138
  };
104
- replaceGenerationAdmissions(previous.admissions, admissions, this, () => { this.#current = next; }, () => this.#retain(next));
139
+ replaceGenerationAdmissions({ previous: previous.admissions, next: admissions, owner: this,
140
+ publishNext: () => { this.#current = next; }, acquireNext: () => this.#retain(next), stopSignal: this.#stop.signal });
105
141
  // Commit only swaps the active pointer. The previous record may still be
106
142
  // serving in-flight work, so disposal is deferred to its final lease.
107
143
  previous.retired = true;
@@ -114,7 +150,10 @@ export class SnapshotStore {
114
150
  if (this.#closed)
115
151
  return;
116
152
  this.#closed = true;
117
- replaceGenerationAdmissions(this.#current.admissions, new Set(), this, () => undefined);
153
+ replaceGenerationAdmissions({ previous: this.#current.admissions, next: new Set(), owner: this,
154
+ publishNext: () => undefined, stopSignal: this.#stop.signal });
155
+ // Stop observers may synchronously release transports; no gate may admit fresh work then.
156
+ this.#stop.abort();
118
157
  this.#current.retired = true;
119
158
  this.#current.retirement.abort();
120
159
  this.#retired.add(this.#current);
@@ -226,3 +265,10 @@ class ReadonlyMapView {
226
265
  function readonlyMap(values) {
227
266
  return new ReadonlyMapView(values);
228
267
  }
268
+ /** Compatibility for Host embedders that implement the original minimal SnapshotReader. */
269
+ export function readLeasedRootResource(reader, lease, token) {
270
+ if (!reader.owns(lease) || !lease.active)
271
+ throw new Error('Root resource access requires an active owned SnapshotLease');
272
+ return reader.rootResource ? reader.rootResource(lease, token)
273
+ : lease.value.resources.get(lease.value.root)?.get(token.id);
274
+ }
package/lib/token.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { DisposeStack, type Dispose } from './dispose.js';
2
- import { type PluginId, type TokenId } from './identity.js';
2
+ import { type FeatureId, type PluginId, type TokenId } from './identity.js';
3
3
  export interface Token<T> {
4
4
  readonly id: TokenId;
5
5
  readonly description?: string;
@@ -7,6 +7,10 @@ export interface Token<T> {
7
7
  }
8
8
  export declare function createToken<T>(id: string, description?: string): Token<T>;
9
9
  export declare function createRootPrivateToken<T>(id: string, description?: string): Token<T>;
10
+ /** A Root-private capability readable only while projecting its owning Feature. */
11
+ export declare function createFeaturePrivateToken<T>(id: string, feature: FeatureId, description?: string): Token<T>;
12
+ /** @internal The actual candidate Root Scope never enters the Feature context. */
13
+ export declare function readFeaturePrivateResource<T>(scope: Pick<Scope, 'has' | 'use'>, feature: FeatureId, token: Token<T>): T | undefined;
10
14
  /** @internal Ends a facade's candidate declarations on seal or failed setup disposal. */
11
15
  export declare const scopeSetupFinalizer: unique symbol;
12
16
  export declare class Scope {
package/lib/token.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { DisposeStack } from './dispose.js';
2
2
  import { tokenId } from './identity.js';
3
3
  const rootPrivateTokens = new WeakSet();
4
+ const featurePrivateTokens = new WeakMap();
4
5
  export function createToken(id, description) {
5
6
  return Object.freeze({ id: tokenId(id), description });
6
7
  }
@@ -9,6 +10,18 @@ export function createRootPrivateToken(id, description) {
9
10
  rootPrivateTokens.add(token);
10
11
  return Object.freeze(token);
11
12
  }
13
+ /** A Root-private capability readable only while projecting its owning Feature. */
14
+ export function createFeaturePrivateToken(id, feature, description) {
15
+ const token = createRootPrivateToken(id, description);
16
+ featurePrivateTokens.set(token, feature);
17
+ return token;
18
+ }
19
+ /** @internal The actual candidate Root Scope never enters the Feature context. */
20
+ export function readFeaturePrivateResource(scope, feature, token) {
21
+ if (featurePrivateTokens.get(token) !== feature)
22
+ throw new Error(`Feature cannot access Root resource ${token.id}`);
23
+ return scope.has(token) ? scope.use(token) : undefined;
24
+ }
12
25
  /** @internal Ends a facade's candidate declarations on seal or failed setup disposal. */
13
26
  export const scopeSetupFinalizer = Symbol('ScopeSetupFinalizer');
14
27
  export class Scope {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/plugin-runtime",
3
- "version": "1.1.17",
3
+ "version": "1.1.18",
4
4
  "description": "Zero-dependency Plugin tree, Scope and atomic generation runtime for Zhin",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",