@forgeax/engine-ddc 0.1.6 → 0.1.19

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/src/session.ts CHANGED
@@ -1,6 +1,13 @@
1
1
  import type { DdcEntry, StagedDdcEntry } from './entry-store.js';
2
2
  import { DdcEntryStore } from './entry-store.js';
3
- import type { DdcCommitResult, DdcHead, DdcLease } from './lifecycle.js';
3
+ import type {
4
+ DdcCommitResult,
5
+ DdcHead,
6
+ DdcLease,
7
+ DdcRestoreFence,
8
+ DdcRestoreResult,
9
+ DdcRollbackSnapshot,
10
+ } from './lifecycle.js';
4
11
  import { DdcLifecycle } from './lifecycle.js';
5
12
 
6
13
  export interface DdcSessionOptions {
@@ -11,7 +18,7 @@ export interface DdcSessionOptions {
11
18
  export interface DdcGenerationCandidate {
12
19
  readonly generation: number;
13
20
  readonly lease: DdcLease;
14
- readonly previousHead: DdcHead;
21
+ readonly previousHead: DdcRollbackSnapshot;
15
22
  }
16
23
 
17
24
  export interface DdcGenerationEntryCandidate extends DdcGenerationCandidate {
@@ -47,6 +54,7 @@ export class DdcGenerationSession {
47
54
  private readonly lifecycle: DdcLifecycle;
48
55
  private readonly entries: DdcEntryStore;
49
56
  private readonly candidates = new Map<string, MutableDdcGenerationCandidate>();
57
+ private readonly restoreFences = new WeakMap<DdcGenerationCandidate, DdcRestoreFence>();
50
58
  private readonly heartbeatTimers = new Map<string, HeartbeatTimer>();
51
59
  private readonly counters: MutableMetrics = {
52
60
  hitCount: 0,
@@ -69,12 +77,14 @@ export class DdcGenerationSession {
69
77
  }
70
78
 
71
79
  public async beginCandidate(guid: string, desiredKey: string): Promise<DdcGenerationCandidate> {
72
- const head = await this.inspect(guid, desiredKey);
73
- const lease = await this.lifecycle.begin(guid, desiredKey);
80
+ this.assertOpen();
81
+ const { lease, previousHead } = await this.lifecycle.beginWithSnapshot(guid, desiredKey);
82
+ if (previousHead.state === 'current') this.counters.hitCount += 1;
83
+ else this.counters.missCount += 1;
74
84
  const candidate: MutableDdcGenerationCandidate = {
75
85
  generation: this.generation,
76
86
  lease,
77
- previousHead: head,
87
+ previousHead,
78
88
  };
79
89
  this.candidates.set(lease.attempt, candidate);
80
90
  this.scheduleHeartbeat(candidate);
@@ -95,10 +105,11 @@ export class DdcGenerationSession {
95
105
  this.candidates.set(candidate.lease.attempt, entryCandidate);
96
106
  return entryCandidate;
97
107
  } catch (error) {
98
- await this.lifecycle.fail(candidate.lease, {
108
+ const fence = await this.lifecycle.fail(candidate.lease, {
99
109
  code: 'ddc-entry-stage-failed',
100
110
  detail: error instanceof Error ? error.message : String(error),
101
111
  });
112
+ if (fence !== undefined) this.restoreFences.set(candidate, fence);
102
113
  this.stopHeartbeat(candidate);
103
114
  this.candidates.delete(candidate.lease.attempt);
104
115
  this.counters.writeFailureCount += 1;
@@ -121,6 +132,8 @@ export class DdcGenerationSession {
121
132
  const registered = this.assertCandidate(candidate);
122
133
  try {
123
134
  const result = await this.lifecycle.commit(registered.lease, validatedKey);
135
+ if (result.restoreFence !== undefined)
136
+ this.restoreFences.set(registered, result.restoreFence);
124
137
  if (result.result === 'invalid') this.counters.corruptCount += 1;
125
138
  this.stopHeartbeat(registered);
126
139
  this.candidates.delete(registered.lease.attempt);
@@ -140,15 +153,18 @@ export class DdcGenerationSession {
140
153
  try {
141
154
  await this.entries.publish(candidate.staged);
142
155
  const result = await this.lifecycle.commit(registered.lease, validatedKey);
156
+ if (result.restoreFence !== undefined)
157
+ this.restoreFences.set(registered, result.restoreFence);
143
158
  this.stopHeartbeat(registered);
144
159
  this.candidates.delete(registered.lease.attempt);
145
160
  return result;
146
161
  } catch (error) {
147
162
  this.stopHeartbeat(registered);
148
- await this.lifecycle.fail(registered.lease, {
163
+ const fence = await this.lifecycle.fail(registered.lease, {
149
164
  code: 'ddc-entry-publish-failed',
150
165
  detail: error instanceof Error ? error.message : String(error),
151
166
  });
167
+ if (fence !== undefined) this.restoreFences.set(registered, fence);
152
168
  await this.entries.discard(candidate.staged).catch(() => {});
153
169
  this.candidates.delete(registered.lease.attempt);
154
170
  this.counters.writeFailureCount += 1;
@@ -173,21 +189,31 @@ export class DdcGenerationSession {
173
189
  await Promise.all([this.discardCandidate(candidate), this.entries.discard(candidate.staged)]);
174
190
  }
175
191
 
176
- public async restoreEntry(candidate: DdcGenerationEntryCandidate): Promise<void> {
192
+ public async restoreEntry(candidate: DdcGenerationEntryCandidate): Promise<DdcRestoreResult> {
177
193
  const registered = this.candidates.get(candidate.lease.attempt);
194
+ const ownerLease = registered?.lease ?? candidate.lease;
195
+ const fence = this.restoreFences.get(candidate);
196
+ this.stopHeartbeat(ownerLease);
197
+ let result: DdcRestoreResult = {
198
+ result: 'not-owner',
199
+ revision: candidate.previousHead.revision ?? 0,
200
+ ...(candidate.previousHead.generation === undefined
201
+ ? {}
202
+ : { generation: candidate.previousHead.generation }),
203
+ };
178
204
  let restoreError: unknown;
179
205
  try {
180
- await this.lifecycle.restore(candidate.previousHead);
206
+ result = await this.lifecycle.restoreIfCurrent(candidate.previousHead, ownerLease, fence);
181
207
  } catch (error) {
182
208
  restoreError = error;
183
209
  } finally {
184
210
  await this.entries.discard(candidate.staged).catch(() => {});
185
211
  if (registered !== undefined) {
186
- this.stopHeartbeat(registered);
187
212
  this.candidates.delete(registered.lease.attempt);
188
213
  }
189
214
  }
190
215
  if (restoreError !== undefined) throw restoreError;
216
+ return result;
191
217
  }
192
218
 
193
219
  public metrics(): DdcSessionMetrics {
@@ -245,10 +271,11 @@ export class DdcGenerationSession {
245
271
  this.heartbeatTimers.set(attempt, timer);
246
272
  }
247
273
 
248
- private stopHeartbeat(candidate: DdcGenerationCandidate): void {
249
- const timer = this.heartbeatTimers.get(candidate.lease.attempt);
274
+ private stopHeartbeat(candidate: DdcGenerationCandidate | DdcLease): void {
275
+ const attempt = 'lease' in candidate ? candidate.lease.attempt : candidate.attempt;
276
+ const timer = this.heartbeatTimers.get(attempt);
250
277
  if (timer === undefined) return;
251
278
  clearTimeout(timer);
252
- this.heartbeatTimers.delete(candidate.lease.attempt);
279
+ this.heartbeatTimers.delete(attempt);
253
280
  }
254
281
  }