@voidhash/mimic-effect 1.0.0-beta.7 → 1.0.0-beta.9
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/.turbo/turbo-build.log +94 -94
- package/dist/DocumentInstance.cjs +257 -0
- package/dist/DocumentInstance.d.cts +74 -0
- package/dist/DocumentInstance.d.cts.map +1 -0
- package/dist/DocumentInstance.d.mts +74 -0
- package/dist/DocumentInstance.d.mts.map +1 -0
- package/dist/DocumentInstance.mjs +258 -0
- package/dist/DocumentInstance.mjs.map +1 -0
- package/dist/MimicClusterServerEngine.cjs +19 -203
- package/dist/MimicClusterServerEngine.d.cts.map +1 -1
- package/dist/MimicClusterServerEngine.d.mts.map +1 -1
- package/dist/MimicClusterServerEngine.mjs +24 -208
- package/dist/MimicClusterServerEngine.mjs.map +1 -1
- package/dist/MimicServerEngine.cjs +72 -10
- package/dist/MimicServerEngine.d.cts +12 -7
- package/dist/MimicServerEngine.d.cts.map +1 -1
- package/dist/MimicServerEngine.d.mts +12 -7
- package/dist/MimicServerEngine.d.mts.map +1 -1
- package/dist/MimicServerEngine.mjs +74 -12
- package/dist/MimicServerEngine.mjs.map +1 -1
- package/dist/Protocol.d.cts +1 -1
- package/dist/Protocol.d.mts +1 -1
- package/dist/index.cjs +2 -4
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/package.json +3 -3
- package/src/DocumentInstance.ts +516 -0
- package/src/MimicClusterServerEngine.ts +40 -357
- package/src/MimicServerEngine.ts +172 -36
- package/src/index.ts +3 -4
- package/tests/DocumentInstance.test.ts +669 -0
- package/dist/DocumentManager.cjs +0 -299
- package/dist/DocumentManager.d.cts +0 -67
- package/dist/DocumentManager.d.cts.map +0 -1
- package/dist/DocumentManager.d.mts +0 -67
- package/dist/DocumentManager.d.mts.map +0 -1
- package/dist/DocumentManager.mjs +0 -297
- package/dist/DocumentManager.mjs.map +0 -1
- package/src/DocumentManager.ts +0 -616
- package/tests/DocumentManager.test.ts +0 -335
|
@@ -13,32 +13,29 @@ import {
|
|
|
13
13
|
HashMap,
|
|
14
14
|
Layer,
|
|
15
15
|
Metric,
|
|
16
|
-
Option,
|
|
17
16
|
PubSub,
|
|
18
17
|
Ref,
|
|
19
18
|
Schema,
|
|
20
|
-
Scope,
|
|
21
19
|
Stream,
|
|
22
20
|
} from "effect";
|
|
23
21
|
import { Entity, Sharding } from "@effect/cluster";
|
|
24
22
|
import { Rpc } from "@effect/rpc";
|
|
25
|
-
import {
|
|
26
|
-
import { ServerDocument } from "@voidhash/mimic/server";
|
|
23
|
+
import { type Primitive, type Transaction } from "@voidhash/mimic";
|
|
27
24
|
import type {
|
|
28
25
|
MimicClusterServerEngineConfig,
|
|
29
26
|
PresenceEntry,
|
|
30
27
|
PresenceEvent,
|
|
31
|
-
PresenceSnapshot,
|
|
32
28
|
ResolvedClusterConfig,
|
|
33
|
-
StoredDocument,
|
|
34
|
-
WalEntry,
|
|
35
29
|
} from "./Types";
|
|
36
30
|
import type * as Protocol from "./Protocol";
|
|
37
31
|
import { ColdStorageTag, type ColdStorage } from "./ColdStorage";
|
|
38
32
|
import { HotStorageTag, type HotStorage } from "./HotStorage";
|
|
39
33
|
import { MimicAuthServiceTag } from "./MimicAuthService";
|
|
40
34
|
import { MimicServerEngineTag, type MimicServerEngine } from "./MimicServerEngine";
|
|
41
|
-
import
|
|
35
|
+
import {
|
|
36
|
+
DocumentInstance,
|
|
37
|
+
type DocumentInstance as DocumentInstanceInterface,
|
|
38
|
+
} from "./DocumentInstance";
|
|
42
39
|
import * as Metrics from "./Metrics";
|
|
43
40
|
|
|
44
41
|
// =============================================================================
|
|
@@ -172,16 +169,12 @@ const MimicDocumentEntity = Entity.make("MimicDocument", [
|
|
|
172
169
|
// =============================================================================
|
|
173
170
|
|
|
174
171
|
/**
|
|
175
|
-
*
|
|
172
|
+
* Entity state that wraps DocumentInstance and adds presence management
|
|
176
173
|
*/
|
|
177
|
-
interface
|
|
178
|
-
readonly
|
|
179
|
-
readonly broadcastPubSub: PubSub.PubSub<Protocol.ServerMessage>;
|
|
174
|
+
interface EntityState<TSchema extends Primitive.AnyPrimitive> {
|
|
175
|
+
readonly instance: DocumentInstanceInterface<TSchema>;
|
|
180
176
|
readonly presences: HashMap.HashMap<string, PresenceEntry>;
|
|
181
177
|
readonly presencePubSub: PubSub.PubSub<PresenceEvent>;
|
|
182
|
-
readonly lastSnapshotVersion: number;
|
|
183
|
-
readonly lastSnapshotTime: number;
|
|
184
|
-
readonly transactionsSinceSnapshot: number;
|
|
185
178
|
}
|
|
186
179
|
|
|
187
180
|
// =============================================================================
|
|
@@ -262,283 +255,37 @@ const createEntityHandler = <TSchema extends Primitive.AnyPrimitive>(
|
|
|
262
255
|
const address = yield* Entity.CurrentAddress;
|
|
263
256
|
const documentId = address.entityId;
|
|
264
257
|
|
|
265
|
-
//
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
return (
|
|
278
|
-
config.initial as (ctx: {
|
|
279
|
-
documentId: string;
|
|
280
|
-
}) => Effect.Effect<Primitive.InferSetInput<TSchema>>
|
|
281
|
-
)({ documentId });
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
return Effect.succeed(
|
|
285
|
-
config.initial as Primitive.InferSetInput<TSchema>
|
|
286
|
-
);
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
// Load snapshot from ColdStorage (fatal if unavailable - entity cannot start)
|
|
290
|
-
const storedDoc = yield* coldStorage.load(documentId).pipe(
|
|
291
|
-
Effect.orDie // Entity cannot initialize without storage
|
|
292
|
-
);
|
|
293
|
-
|
|
294
|
-
let initialState: Primitive.InferSetInput<TSchema> | undefined;
|
|
295
|
-
let initialVersion = 0;
|
|
296
|
-
|
|
297
|
-
if (storedDoc) {
|
|
298
|
-
initialState =
|
|
299
|
-
storedDoc.state as Primitive.InferSetInput<TSchema>;
|
|
300
|
-
initialVersion = storedDoc.version;
|
|
301
|
-
} else {
|
|
302
|
-
initialState = yield* computeInitialState();
|
|
303
|
-
}
|
|
258
|
+
// Create DocumentInstance (fatal if unavailable - entity cannot start)
|
|
259
|
+
const instance = yield* DocumentInstance.make(
|
|
260
|
+
documentId,
|
|
261
|
+
{
|
|
262
|
+
schema: config.schema,
|
|
263
|
+
initial: config.initial,
|
|
264
|
+
maxTransactionHistory: config.maxTransactionHistory,
|
|
265
|
+
snapshot: config.snapshot,
|
|
266
|
+
},
|
|
267
|
+
coldStorage,
|
|
268
|
+
hotStorage
|
|
269
|
+
).pipe(Effect.orDie);
|
|
304
270
|
|
|
305
|
-
// Create
|
|
306
|
-
const broadcastPubSub = yield* PubSub.unbounded<Protocol.ServerMessage>();
|
|
271
|
+
// Create presence PubSub and state ref
|
|
307
272
|
const presencePubSub = yield* PubSub.unbounded<PresenceEvent>();
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const stateRef = yield* Ref.make<EntityDocumentState<TSchema>>({
|
|
311
|
-
document: undefined as unknown as ServerDocument.ServerDocument<TSchema>,
|
|
312
|
-
broadcastPubSub,
|
|
273
|
+
const stateRef = yield* Ref.make<EntityState<TSchema>>({
|
|
274
|
+
instance,
|
|
313
275
|
presences: HashMap.empty(),
|
|
314
276
|
presencePubSub,
|
|
315
|
-
lastSnapshotVersion: initialVersion,
|
|
316
|
-
lastSnapshotTime: Date.now(),
|
|
317
|
-
transactionsSinceSnapshot: 0,
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
// Create ServerDocument with callbacks
|
|
321
|
-
const document = ServerDocument.make({
|
|
322
|
-
schema: config.schema,
|
|
323
|
-
initialState,
|
|
324
|
-
initialVersion,
|
|
325
|
-
maxTransactionHistory: config.maxTransactionHistory,
|
|
326
|
-
onBroadcast: (message: ServerDocument.TransactionMessage) => {
|
|
327
|
-
Effect.runSync(
|
|
328
|
-
PubSub.publish(broadcastPubSub, {
|
|
329
|
-
type: "transaction",
|
|
330
|
-
transaction: message.transaction,
|
|
331
|
-
version: message.version,
|
|
332
|
-
} as Protocol.ServerMessage)
|
|
333
|
-
);
|
|
334
|
-
},
|
|
335
|
-
onRejection: (transactionId: string, reason: string) => {
|
|
336
|
-
Effect.runSync(
|
|
337
|
-
PubSub.publish(broadcastPubSub, {
|
|
338
|
-
type: "error",
|
|
339
|
-
transactionId,
|
|
340
|
-
reason,
|
|
341
|
-
} as Protocol.ServerMessage)
|
|
342
|
-
);
|
|
343
|
-
},
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
// Update state with document
|
|
347
|
-
yield* Ref.update(stateRef, (s) => ({ ...s, document }));
|
|
348
|
-
|
|
349
|
-
// Load WAL entries (fatal if unavailable - entity cannot start)
|
|
350
|
-
const walEntries = yield* hotStorage.getEntries(documentId, initialVersion).pipe(
|
|
351
|
-
Effect.orDie // Entity cannot initialize without storage
|
|
352
|
-
);
|
|
353
|
-
|
|
354
|
-
// Verify WAL continuity (warning only, non-blocking)
|
|
355
|
-
if (walEntries.length > 0) {
|
|
356
|
-
const firstWalVersion = walEntries[0]!.version;
|
|
357
|
-
const expectedFirst = initialVersion + 1;
|
|
358
|
-
|
|
359
|
-
if (firstWalVersion !== expectedFirst) {
|
|
360
|
-
yield* Effect.logWarning("WAL version gap detected", {
|
|
361
|
-
documentId,
|
|
362
|
-
snapshotVersion: initialVersion,
|
|
363
|
-
firstWalVersion,
|
|
364
|
-
expectedFirst,
|
|
365
|
-
});
|
|
366
|
-
yield* Metric.increment(Metrics.storageVersionGaps);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// Check internal gaps
|
|
370
|
-
for (let i = 1; i < walEntries.length; i++) {
|
|
371
|
-
const prev = walEntries[i - 1]!.version;
|
|
372
|
-
const curr = walEntries[i]!.version;
|
|
373
|
-
if (curr !== prev + 1) {
|
|
374
|
-
yield* Effect.logWarning("WAL internal gap detected", {
|
|
375
|
-
documentId,
|
|
376
|
-
previousVersion: prev,
|
|
377
|
-
currentVersion: curr,
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Replay WAL entries
|
|
384
|
-
for (const entry of walEntries) {
|
|
385
|
-
const result = document.submit(entry.transaction);
|
|
386
|
-
if (!result.success) {
|
|
387
|
-
yield* Effect.logWarning("Skipping corrupted WAL entry", {
|
|
388
|
-
documentId,
|
|
389
|
-
version: entry.version,
|
|
390
|
-
reason: result.reason,
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// Track metrics
|
|
396
|
-
if (storedDoc) {
|
|
397
|
-
yield* Metric.increment(Metrics.documentsRestored);
|
|
398
|
-
} else {
|
|
399
|
-
yield* Metric.increment(Metrics.documentsCreated);
|
|
400
|
-
}
|
|
401
|
-
yield* Metric.incrementBy(Metrics.documentsActive, 1);
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* Save snapshot to ColdStorage derived from WAL entries.
|
|
405
|
-
* This ensures snapshots are always based on durable WAL data.
|
|
406
|
-
* Idempotent: skips save if already snapshotted at target version.
|
|
407
|
-
* Truncate failures are non-fatal and will be retried on next snapshot.
|
|
408
|
-
*/
|
|
409
|
-
const saveSnapshot = Effect.fn("cluster.document.snapshot.save")(
|
|
410
|
-
function* (targetVersion: number) {
|
|
411
|
-
const state = yield* Ref.get(stateRef);
|
|
412
|
-
|
|
413
|
-
// Idempotency check: skip if already snapshotted at this version
|
|
414
|
-
if (targetVersion <= state.lastSnapshotVersion) {
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
const snapshotStartTime = Date.now();
|
|
419
|
-
|
|
420
|
-
// Load base snapshot from cold storage (best effort - log error but don't crash entity)
|
|
421
|
-
const baseSnapshotResult = yield* Effect.either(coldStorage.load(documentId));
|
|
422
|
-
if (baseSnapshotResult._tag === "Left") {
|
|
423
|
-
yield* Effect.logError("Failed to load base snapshot for WAL replay", {
|
|
424
|
-
documentId,
|
|
425
|
-
error: baseSnapshotResult.left,
|
|
426
|
-
});
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
const baseSnapshot = baseSnapshotResult.right;
|
|
430
|
-
const baseVersion = baseSnapshot?.version ?? 0;
|
|
431
|
-
const baseState = baseSnapshot?.state as Primitive.InferState<TSchema> | undefined;
|
|
432
|
-
|
|
433
|
-
// Load WAL entries from base to target
|
|
434
|
-
const walEntriesResult = yield* Effect.either(hotStorage.getEntries(documentId, baseVersion));
|
|
435
|
-
if (walEntriesResult._tag === "Left") {
|
|
436
|
-
yield* Effect.logError("Failed to load WAL entries for snapshot", {
|
|
437
|
-
documentId,
|
|
438
|
-
error: walEntriesResult.left,
|
|
439
|
-
});
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
const walEntries = walEntriesResult.right;
|
|
443
|
-
const relevantEntries = walEntries.filter(e => e.version <= targetVersion);
|
|
444
|
-
|
|
445
|
-
if (relevantEntries.length === 0 && !baseSnapshot) {
|
|
446
|
-
// Nothing to snapshot
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Rebuild state by replaying WAL on base
|
|
451
|
-
let snapshotState: Primitive.InferState<TSchema> | undefined = baseState;
|
|
452
|
-
for (const entry of relevantEntries) {
|
|
453
|
-
// Create a temporary document to apply the transaction
|
|
454
|
-
const tempDoc = Document.make(config.schema, { initialState: snapshotState });
|
|
455
|
-
tempDoc.apply(entry.transaction.ops);
|
|
456
|
-
snapshotState = tempDoc.get();
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
if (snapshotState === undefined) {
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
const snapshotVersion = relevantEntries.length > 0
|
|
464
|
-
? relevantEntries[relevantEntries.length - 1]!.version
|
|
465
|
-
: baseVersion;
|
|
466
|
-
|
|
467
|
-
// Re-check before saving (in case another snapshot completed while we were working)
|
|
468
|
-
// This prevents a slower snapshot from overwriting a more recent one
|
|
469
|
-
const currentState = yield* Ref.get(stateRef);
|
|
470
|
-
if (snapshotVersion <= currentState.lastSnapshotVersion) {
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const storedDocument: StoredDocument = {
|
|
475
|
-
state: snapshotState,
|
|
476
|
-
version: snapshotVersion,
|
|
477
|
-
schemaVersion: SCHEMA_VERSION,
|
|
478
|
-
savedAt: Date.now(),
|
|
479
|
-
};
|
|
480
|
-
|
|
481
|
-
// Save to ColdStorage (best effort - log error but don't crash entity)
|
|
482
|
-
yield* Effect.catchAll(
|
|
483
|
-
coldStorage.save(documentId, storedDocument),
|
|
484
|
-
(e) =>
|
|
485
|
-
Effect.logError("Failed to save snapshot", { documentId, error: e })
|
|
486
|
-
);
|
|
487
|
-
|
|
488
|
-
const snapshotDuration = Date.now() - snapshotStartTime;
|
|
489
|
-
yield* Metric.increment(Metrics.storageSnapshots);
|
|
490
|
-
yield* Metric.update(Metrics.storageSnapshotLatency, snapshotDuration);
|
|
491
|
-
|
|
492
|
-
// Update tracking BEFORE truncate (for idempotency on retry)
|
|
493
|
-
yield* Ref.update(stateRef, (s) => ({
|
|
494
|
-
...s,
|
|
495
|
-
lastSnapshotVersion: snapshotVersion,
|
|
496
|
-
lastSnapshotTime: Date.now(),
|
|
497
|
-
transactionsSinceSnapshot: 0,
|
|
498
|
-
}));
|
|
499
|
-
|
|
500
|
-
// Truncate WAL - non-fatal, will be retried on next snapshot
|
|
501
|
-
yield* Effect.catchAll(hotStorage.truncate(documentId, snapshotVersion), (e) =>
|
|
502
|
-
Effect.logWarning("WAL truncate failed - will retry on next snapshot", {
|
|
503
|
-
documentId,
|
|
504
|
-
version: snapshotVersion,
|
|
505
|
-
error: e,
|
|
506
|
-
})
|
|
507
|
-
);
|
|
508
|
-
}
|
|
509
|
-
);
|
|
510
|
-
|
|
511
|
-
/**
|
|
512
|
-
* Check if snapshot should be triggered
|
|
513
|
-
*/
|
|
514
|
-
const checkSnapshotTriggers = Effect.fn(
|
|
515
|
-
"cluster.document.snapshot.check-triggers"
|
|
516
|
-
)(function* () {
|
|
517
|
-
const state = yield* Ref.get(stateRef);
|
|
518
|
-
const now = Date.now();
|
|
519
|
-
const currentVersion = state.document.getVersion();
|
|
520
|
-
|
|
521
|
-
const intervalMs = Duration.toMillis(config.snapshot.interval);
|
|
522
|
-
const threshold = config.snapshot.transactionThreshold;
|
|
523
|
-
|
|
524
|
-
if (state.transactionsSinceSnapshot >= threshold) {
|
|
525
|
-
yield* saveSnapshot(currentVersion);
|
|
526
|
-
return;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
if (now - state.lastSnapshotTime >= intervalMs) {
|
|
530
|
-
yield* saveSnapshot(currentVersion);
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
277
|
});
|
|
534
278
|
|
|
535
279
|
// Cleanup on entity finalization
|
|
536
280
|
yield* Effect.addFinalizer(() =>
|
|
537
281
|
Effect.fn("cluster.entity.finalize")(function* () {
|
|
538
|
-
//
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
282
|
+
// Best effort save - don't fail shutdown if storage is unavailable
|
|
283
|
+
yield* Effect.catchAll(instance.saveSnapshot(), (e) =>
|
|
284
|
+
Effect.logError("Failed to save snapshot during entity finalization", {
|
|
285
|
+
documentId,
|
|
286
|
+
error: e,
|
|
287
|
+
})
|
|
288
|
+
);
|
|
542
289
|
yield* Metric.incrementBy(Metrics.documentsActive, -1);
|
|
543
290
|
yield* Metric.increment(Metrics.documentsEvicted);
|
|
544
291
|
yield* Effect.logDebug("Entity finalized", { documentId });
|
|
@@ -550,90 +297,26 @@ const createEntityHandler = <TSchema extends Primitive.AnyPrimitive>(
|
|
|
550
297
|
Submit: Effect.fn("cluster.document.transaction.submit")(function* ({
|
|
551
298
|
payload,
|
|
552
299
|
}) {
|
|
553
|
-
const submitStartTime = Date.now();
|
|
554
|
-
const state = yield* Ref.get(stateRef);
|
|
555
|
-
|
|
556
300
|
// Decode transaction
|
|
557
301
|
const transaction = decodeTransaction(payload.transaction);
|
|
558
302
|
|
|
559
|
-
//
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
return {
|
|
569
|
-
success: false as const,
|
|
570
|
-
reason: validation.reason,
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// Phase 2: Append to WAL with gap check (BEFORE state mutation)
|
|
575
|
-
const walEntry: WalEntry = {
|
|
576
|
-
transaction,
|
|
577
|
-
version: validation.nextVersion,
|
|
578
|
-
timestamp: Date.now(),
|
|
579
|
-
};
|
|
580
|
-
|
|
581
|
-
const appendResult = yield* Effect.either(
|
|
582
|
-
hotStorage.appendWithCheck(documentId, walEntry, validation.nextVersion)
|
|
303
|
+
// Use DocumentInstance's submit method, catching storage errors
|
|
304
|
+
return yield* instance.submit(transaction).pipe(
|
|
305
|
+
Effect.catchAll((error) =>
|
|
306
|
+
Effect.succeed({
|
|
307
|
+
success: false as const,
|
|
308
|
+
reason: `Storage error: ${String(error)}`,
|
|
309
|
+
})
|
|
310
|
+
)
|
|
583
311
|
);
|
|
584
|
-
|
|
585
|
-
if (appendResult._tag === "Left") {
|
|
586
|
-
// WAL append failed - do NOT apply, state unchanged
|
|
587
|
-
yield* Effect.logError("WAL append failed", {
|
|
588
|
-
documentId,
|
|
589
|
-
version: validation.nextVersion,
|
|
590
|
-
error: appendResult.left,
|
|
591
|
-
});
|
|
592
|
-
yield* Metric.increment(Metrics.walAppendFailures);
|
|
593
|
-
|
|
594
|
-
const latency = Date.now() - submitStartTime;
|
|
595
|
-
yield* Metric.update(Metrics.transactionsLatency, latency);
|
|
596
|
-
|
|
597
|
-
// Return failure - client must retry
|
|
598
|
-
return {
|
|
599
|
-
success: false as const,
|
|
600
|
-
reason: "Storage unavailable. Please retry.",
|
|
601
|
-
};
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// Phase 3: Apply (state mutation + broadcast)
|
|
605
|
-
state.document.apply(transaction);
|
|
606
|
-
|
|
607
|
-
// Track metrics
|
|
608
|
-
const latency = Date.now() - submitStartTime;
|
|
609
|
-
yield* Metric.update(Metrics.transactionsLatency, latency);
|
|
610
|
-
yield* Metric.increment(Metrics.transactionsProcessed);
|
|
611
|
-
yield* Metric.increment(Metrics.storageWalAppends);
|
|
612
|
-
|
|
613
|
-
// Increment transaction count
|
|
614
|
-
yield* Ref.update(stateRef, (s) => ({
|
|
615
|
-
...s,
|
|
616
|
-
transactionsSinceSnapshot: s.transactionsSinceSnapshot + 1,
|
|
617
|
-
}));
|
|
618
|
-
|
|
619
|
-
// Check snapshot triggers
|
|
620
|
-
yield* checkSnapshotTriggers();
|
|
621
|
-
|
|
622
|
-
return {
|
|
623
|
-
success: true as const,
|
|
624
|
-
version: validation.nextVersion,
|
|
625
|
-
};
|
|
626
312
|
}),
|
|
627
313
|
|
|
628
314
|
GetSnapshot: Effect.fn("cluster.document.snapshot.get")(function* () {
|
|
629
|
-
|
|
630
|
-
return state.document.getSnapshot();
|
|
315
|
+
return instance.getSnapshot();
|
|
631
316
|
}),
|
|
632
317
|
|
|
633
318
|
Touch: Effect.fn("cluster.document.touch")(function* () {
|
|
634
|
-
|
|
635
|
-
// Just update last activity time conceptually
|
|
636
|
-
return void 0;
|
|
319
|
+
yield* instance.touch();
|
|
637
320
|
}),
|
|
638
321
|
|
|
639
322
|
SetPresence: Effect.fn("cluster.presence.set")(function* ({ payload }) {
|