@voidhash/mimic-effect 1.0.0-beta.1 → 1.0.0-beta.10
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 +116 -74
- package/dist/ColdStorage.cjs +9 -5
- package/dist/ColdStorage.d.cts.map +1 -1
- package/dist/ColdStorage.d.mts.map +1 -1
- package/dist/ColdStorage.mjs +9 -5
- package/dist/ColdStorage.mjs.map +1 -1
- package/dist/DocumentInstance.cjs +263 -0
- package/dist/DocumentInstance.d.cts +78 -0
- package/dist/DocumentInstance.d.cts.map +1 -0
- package/dist/DocumentInstance.d.mts +78 -0
- package/dist/DocumentInstance.d.mts.map +1 -0
- package/dist/DocumentInstance.mjs +264 -0
- package/dist/DocumentInstance.mjs.map +1 -0
- package/dist/Errors.cjs +10 -1
- package/dist/Errors.d.cts +18 -3
- package/dist/Errors.d.cts.map +1 -1
- package/dist/Errors.d.mts +18 -3
- package/dist/Errors.d.mts.map +1 -1
- package/dist/Errors.mjs +9 -1
- package/dist/Errors.mjs.map +1 -1
- package/dist/HotStorage.cjs +39 -12
- package/dist/HotStorage.d.cts +17 -1
- package/dist/HotStorage.d.cts.map +1 -1
- package/dist/HotStorage.d.mts +17 -1
- package/dist/HotStorage.d.mts.map +1 -1
- package/dist/HotStorage.mjs +39 -12
- package/dist/HotStorage.mjs.map +1 -1
- package/dist/Metrics.cjs +29 -1
- package/dist/Metrics.d.cts +5 -0
- package/dist/Metrics.d.cts.map +1 -1
- package/dist/Metrics.d.mts +5 -0
- package/dist/Metrics.d.mts.map +1 -1
- package/dist/Metrics.mjs +26 -1
- package/dist/Metrics.mjs.map +1 -1
- package/dist/MimicClusterServerEngine.cjs +44 -139
- package/dist/MimicClusterServerEngine.d.cts.map +1 -1
- package/dist/MimicClusterServerEngine.d.mts +1 -1
- package/dist/MimicClusterServerEngine.d.mts.map +1 -1
- package/dist/MimicClusterServerEngine.mjs +46 -141
- package/dist/MimicClusterServerEngine.mjs.map +1 -1
- package/dist/MimicServer.cjs +20 -20
- package/dist/MimicServer.d.cts.map +1 -1
- package/dist/MimicServer.d.mts.map +1 -1
- package/dist/MimicServer.mjs +20 -20
- package/dist/MimicServer.mjs.map +1 -1
- package/dist/MimicServerEngine.cjs +92 -11
- package/dist/MimicServerEngine.d.cts +12 -4
- package/dist/MimicServerEngine.d.cts.map +1 -1
- package/dist/MimicServerEngine.d.mts +12 -4
- package/dist/MimicServerEngine.d.mts.map +1 -1
- package/dist/MimicServerEngine.mjs +94 -13
- package/dist/MimicServerEngine.mjs.map +1 -1
- package/dist/PresenceManager.cjs +5 -5
- package/dist/PresenceManager.d.cts.map +1 -1
- package/dist/PresenceManager.d.mts.map +1 -1
- package/dist/PresenceManager.mjs +5 -5
- package/dist/PresenceManager.mjs.map +1 -1
- package/dist/Protocol.d.cts +1 -1
- package/dist/Protocol.d.mts +1 -1
- package/dist/Types.d.cts +9 -2
- package/dist/Types.d.cts.map +1 -1
- package/dist/Types.d.mts +9 -2
- package/dist/Types.d.mts.map +1 -1
- package/dist/index.cjs +5 -6
- package/dist/index.d.cts +3 -3
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +3 -3
- package/dist/testing/ColdStorageTestSuite.cjs +508 -0
- package/dist/testing/ColdStorageTestSuite.d.cts +36 -0
- package/dist/testing/ColdStorageTestSuite.d.cts.map +1 -0
- package/dist/testing/ColdStorageTestSuite.d.mts +36 -0
- package/dist/testing/ColdStorageTestSuite.d.mts.map +1 -0
- package/dist/testing/ColdStorageTestSuite.mjs +508 -0
- package/dist/testing/ColdStorageTestSuite.mjs.map +1 -0
- package/dist/testing/FailingStorage.cjs +162 -0
- package/dist/testing/FailingStorage.d.cts +43 -0
- package/dist/testing/FailingStorage.d.cts.map +1 -0
- package/dist/testing/FailingStorage.d.mts +43 -0
- package/dist/testing/FailingStorage.d.mts.map +1 -0
- package/dist/testing/FailingStorage.mjs +163 -0
- package/dist/testing/FailingStorage.mjs.map +1 -0
- package/dist/testing/HotStorageTestSuite.cjs +820 -0
- package/dist/testing/HotStorageTestSuite.d.cts +42 -0
- package/dist/testing/HotStorageTestSuite.d.cts.map +1 -0
- package/dist/testing/HotStorageTestSuite.d.mts +42 -0
- package/dist/testing/HotStorageTestSuite.d.mts.map +1 -0
- package/dist/testing/HotStorageTestSuite.mjs +820 -0
- package/dist/testing/HotStorageTestSuite.mjs.map +1 -0
- package/dist/testing/StorageIntegrationTestSuite.cjs +487 -0
- package/dist/testing/StorageIntegrationTestSuite.d.cts +37 -0
- package/dist/testing/StorageIntegrationTestSuite.d.cts.map +1 -0
- package/dist/testing/StorageIntegrationTestSuite.d.mts +37 -0
- package/dist/testing/StorageIntegrationTestSuite.d.mts.map +1 -0
- package/dist/testing/StorageIntegrationTestSuite.mjs +487 -0
- package/dist/testing/StorageIntegrationTestSuite.mjs.map +1 -0
- package/dist/testing/assertions.cjs +117 -0
- package/dist/testing/assertions.mjs +112 -0
- package/dist/testing/assertions.mjs.map +1 -0
- package/dist/testing/index.cjs +14 -0
- package/dist/testing/index.d.cts +6 -0
- package/dist/testing/index.d.mts +6 -0
- package/dist/testing/index.mjs +7 -0
- package/dist/testing/types.cjs +15 -0
- package/dist/testing/types.d.cts +90 -0
- package/dist/testing/types.d.cts.map +1 -0
- package/dist/testing/types.d.mts +90 -0
- package/dist/testing/types.d.mts.map +1 -0
- package/dist/testing/types.mjs +16 -0
- package/dist/testing/types.mjs.map +1 -0
- package/package.json +8 -3
- package/src/ColdStorage.ts +21 -12
- package/src/DocumentInstance.ts +527 -0
- package/src/Errors.ts +15 -1
- package/src/HotStorage.ts +115 -24
- package/src/Metrics.ts +30 -0
- package/src/MimicClusterServerEngine.ts +120 -275
- package/src/MimicServer.ts +83 -75
- package/src/MimicServerEngine.ts +230 -30
- package/src/PresenceManager.ts +44 -34
- package/src/Types.ts +9 -2
- package/src/index.ts +5 -35
- package/src/testing/ColdStorageTestSuite.ts +589 -0
- package/src/testing/FailingStorage.ts +338 -0
- package/src/testing/HotStorageTestSuite.ts +1105 -0
- package/src/testing/StorageIntegrationTestSuite.ts +736 -0
- package/src/testing/assertions.ts +188 -0
- package/src/testing/index.ts +83 -0
- package/src/testing/types.ts +100 -0
- package/tests/ColdStorage.test.ts +8 -120
- package/tests/DocumentInstance.test.ts +669 -0
- package/tests/HotStorage.test.ts +7 -126
- package/tests/StorageIntegration.test.ts +259 -0
- package/tsdown.config.ts +1 -1
- package/dist/DocumentManager.cjs +0 -229
- package/dist/DocumentManager.d.cts +0 -59
- package/dist/DocumentManager.d.cts.map +0 -1
- package/dist/DocumentManager.d.mts +0 -59
- package/dist/DocumentManager.d.mts.map +0 -1
- package/dist/DocumentManager.mjs +0 -227
- package/dist/DocumentManager.mjs.map +0 -1
- package/src/DocumentManager.ts +0 -506
- package/tests/DocumentManager.test.ts +0 -335
|
@@ -13,32 +13,30 @@ import {
|
|
|
13
13
|
HashMap,
|
|
14
14
|
Layer,
|
|
15
15
|
Metric,
|
|
16
|
-
Option,
|
|
17
16
|
PubSub,
|
|
18
17
|
Ref,
|
|
18
|
+
Schedule,
|
|
19
19
|
Schema,
|
|
20
|
-
Scope,
|
|
21
20
|
Stream,
|
|
22
21
|
} from "effect";
|
|
23
22
|
import { Entity, Sharding } from "@effect/cluster";
|
|
24
23
|
import { Rpc } from "@effect/rpc";
|
|
25
|
-
import
|
|
26
|
-
import { ServerDocument } from "@voidhash/mimic/server";
|
|
24
|
+
import { type Primitive, type Transaction } from "@voidhash/mimic";
|
|
27
25
|
import type {
|
|
28
26
|
MimicClusterServerEngineConfig,
|
|
29
27
|
PresenceEntry,
|
|
30
28
|
PresenceEvent,
|
|
31
|
-
PresenceSnapshot,
|
|
32
29
|
ResolvedClusterConfig,
|
|
33
|
-
StoredDocument,
|
|
34
|
-
WalEntry,
|
|
35
30
|
} from "./Types";
|
|
36
31
|
import type * as Protocol from "./Protocol";
|
|
37
32
|
import { ColdStorageTag, type ColdStorage } from "./ColdStorage";
|
|
38
33
|
import { HotStorageTag, type HotStorage } from "./HotStorage";
|
|
39
34
|
import { MimicAuthServiceTag } from "./MimicAuthService";
|
|
40
35
|
import { MimicServerEngineTag, type MimicServerEngine } from "./MimicServerEngine";
|
|
41
|
-
import
|
|
36
|
+
import {
|
|
37
|
+
DocumentInstance,
|
|
38
|
+
type DocumentInstance as DocumentInstanceInterface,
|
|
39
|
+
} from "./DocumentInstance";
|
|
42
40
|
import * as Metrics from "./Metrics";
|
|
43
41
|
|
|
44
42
|
// =============================================================================
|
|
@@ -49,6 +47,7 @@ const DEFAULT_MAX_IDLE_TIME = Duration.minutes(5);
|
|
|
49
47
|
const DEFAULT_MAX_TRANSACTION_HISTORY = 1000;
|
|
50
48
|
const DEFAULT_SNAPSHOT_INTERVAL = Duration.minutes(5);
|
|
51
49
|
const DEFAULT_SNAPSHOT_THRESHOLD = 100;
|
|
50
|
+
const DEFAULT_SNAPSHOT_IDLE_TIMEOUT = Duration.seconds(30);
|
|
52
51
|
const DEFAULT_SHARD_GROUP = "mimic-documents";
|
|
53
52
|
|
|
54
53
|
// =============================================================================
|
|
@@ -172,16 +171,12 @@ const MimicDocumentEntity = Entity.make("MimicDocument", [
|
|
|
172
171
|
// =============================================================================
|
|
173
172
|
|
|
174
173
|
/**
|
|
175
|
-
*
|
|
174
|
+
* Entity state that wraps DocumentInstance and adds presence management
|
|
176
175
|
*/
|
|
177
|
-
interface
|
|
178
|
-
readonly
|
|
179
|
-
readonly broadcastPubSub: PubSub.PubSub<Protocol.ServerMessage>;
|
|
176
|
+
interface EntityState<TSchema extends Primitive.AnyPrimitive> {
|
|
177
|
+
readonly instance: DocumentInstanceInterface<TSchema>;
|
|
180
178
|
readonly presences: HashMap.HashMap<string, PresenceEntry>;
|
|
181
179
|
readonly presencePubSub: PubSub.PubSub<PresenceEvent>;
|
|
182
|
-
readonly lastSnapshotVersion: number;
|
|
183
|
-
readonly lastSnapshotTime: number;
|
|
184
|
-
readonly transactionsSinceSnapshot: number;
|
|
185
180
|
}
|
|
186
181
|
|
|
187
182
|
// =============================================================================
|
|
@@ -216,6 +211,9 @@ const resolveClusterConfig = <TSchema extends Primitive.AnyPrimitive>(
|
|
|
216
211
|
: DEFAULT_SNAPSHOT_INTERVAL,
|
|
217
212
|
transactionThreshold:
|
|
218
213
|
config.snapshot?.transactionThreshold ?? DEFAULT_SNAPSHOT_THRESHOLD,
|
|
214
|
+
idleTimeout: config.snapshot?.idleTimeout
|
|
215
|
+
? Duration.decode(config.snapshot.idleTimeout)
|
|
216
|
+
: DEFAULT_SNAPSHOT_IDLE_TIMEOUT,
|
|
219
217
|
},
|
|
220
218
|
shardGroup: config.shardGroup ?? DEFAULT_SHARD_GROUP,
|
|
221
219
|
});
|
|
@@ -257,258 +255,99 @@ const createEntityHandler = <TSchema extends Primitive.AnyPrimitive>(
|
|
|
257
255
|
coldStorage: ColdStorage,
|
|
258
256
|
hotStorage: HotStorage
|
|
259
257
|
) =>
|
|
260
|
-
Effect.
|
|
258
|
+
Effect.fn("cluster.entity.handler.create")(function* () {
|
|
261
259
|
// Get entity address to determine documentId
|
|
262
260
|
const address = yield* Entity.CurrentAddress;
|
|
263
261
|
const documentId = address.entityId;
|
|
264
262
|
|
|
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
|
|
290
|
-
const storedDoc = yield* Effect.catchAll(
|
|
291
|
-
coldStorage.load(documentId),
|
|
292
|
-
() => Effect.succeed(undefined)
|
|
293
|
-
);
|
|
294
|
-
|
|
295
|
-
let initialState: Primitive.InferSetInput<TSchema> | undefined;
|
|
296
|
-
let initialVersion = 0;
|
|
297
|
-
|
|
298
|
-
if (storedDoc) {
|
|
299
|
-
initialState =
|
|
300
|
-
storedDoc.state as Primitive.InferSetInput<TSchema>;
|
|
301
|
-
initialVersion = storedDoc.version;
|
|
302
|
-
} else {
|
|
303
|
-
initialState = yield* computeInitialState();
|
|
304
|
-
}
|
|
263
|
+
// Create DocumentInstance (fatal if unavailable - entity cannot start)
|
|
264
|
+
const instance = yield* DocumentInstance.make(
|
|
265
|
+
documentId,
|
|
266
|
+
{
|
|
267
|
+
schema: config.schema,
|
|
268
|
+
initial: config.initial,
|
|
269
|
+
maxTransactionHistory: config.maxTransactionHistory,
|
|
270
|
+
snapshot: config.snapshot,
|
|
271
|
+
},
|
|
272
|
+
coldStorage,
|
|
273
|
+
hotStorage
|
|
274
|
+
).pipe(Effect.orDie);
|
|
305
275
|
|
|
306
|
-
// Create
|
|
307
|
-
const broadcastPubSub = yield* PubSub.unbounded<Protocol.ServerMessage>();
|
|
276
|
+
// Create presence PubSub and state ref
|
|
308
277
|
const presencePubSub = yield* PubSub.unbounded<PresenceEvent>();
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const stateRef = yield* Ref.make<EntityDocumentState<TSchema>>({
|
|
312
|
-
document: undefined as unknown as ServerDocument.ServerDocument<TSchema>,
|
|
313
|
-
broadcastPubSub,
|
|
278
|
+
const stateRef = yield* Ref.make<EntityState<TSchema>>({
|
|
279
|
+
instance,
|
|
314
280
|
presences: HashMap.empty(),
|
|
315
281
|
presencePubSub,
|
|
316
|
-
lastSnapshotVersion: initialVersion,
|
|
317
|
-
lastSnapshotTime: Date.now(),
|
|
318
|
-
transactionsSinceSnapshot: 0,
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
// Create ServerDocument with callbacks
|
|
322
|
-
const document = ServerDocument.make({
|
|
323
|
-
schema: config.schema,
|
|
324
|
-
initialState,
|
|
325
|
-
initialVersion,
|
|
326
|
-
maxTransactionHistory: config.maxTransactionHistory,
|
|
327
|
-
onBroadcast: (message: ServerDocument.TransactionMessage) => {
|
|
328
|
-
Effect.runSync(
|
|
329
|
-
PubSub.publish(broadcastPubSub, {
|
|
330
|
-
type: "transaction",
|
|
331
|
-
transaction: message.transaction,
|
|
332
|
-
version: message.version,
|
|
333
|
-
} as Protocol.ServerMessage)
|
|
334
|
-
);
|
|
335
|
-
},
|
|
336
|
-
onRejection: (transactionId: string, reason: string) => {
|
|
337
|
-
Effect.runSync(
|
|
338
|
-
PubSub.publish(broadcastPubSub, {
|
|
339
|
-
type: "error",
|
|
340
|
-
transactionId,
|
|
341
|
-
reason,
|
|
342
|
-
} as Protocol.ServerMessage)
|
|
343
|
-
);
|
|
344
|
-
},
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
// Update state with document
|
|
348
|
-
yield* Ref.update(stateRef, (s) => ({ ...s, document }));
|
|
349
|
-
|
|
350
|
-
// Replay WAL entries
|
|
351
|
-
const walEntries = yield* Effect.catchAll(
|
|
352
|
-
hotStorage.getEntries(documentId, initialVersion),
|
|
353
|
-
() => Effect.succeed([] as WalEntry[])
|
|
354
|
-
);
|
|
355
|
-
|
|
356
|
-
for (const entry of walEntries) {
|
|
357
|
-
const result = document.submit(entry.transaction);
|
|
358
|
-
if (!result.success) {
|
|
359
|
-
yield* Effect.logWarning("Skipping corrupted WAL entry", {
|
|
360
|
-
documentId,
|
|
361
|
-
version: entry.version,
|
|
362
|
-
reason: result.reason,
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Track metrics
|
|
368
|
-
if (storedDoc) {
|
|
369
|
-
yield* Metric.increment(Metrics.documentsRestored);
|
|
370
|
-
} else {
|
|
371
|
-
yield* Metric.increment(Metrics.documentsCreated);
|
|
372
|
-
}
|
|
373
|
-
yield* Metric.incrementBy(Metrics.documentsActive, 1);
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Save snapshot to ColdStorage
|
|
377
|
-
*/
|
|
378
|
-
const saveSnapshot = Effect.gen(function* () {
|
|
379
|
-
const state = yield* Ref.get(stateRef);
|
|
380
|
-
const docState = state.document.get();
|
|
381
|
-
const version = state.document.getVersion();
|
|
382
|
-
|
|
383
|
-
if (docState === undefined) {
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const storedDocument: StoredDocument = {
|
|
388
|
-
state: docState,
|
|
389
|
-
version,
|
|
390
|
-
schemaVersion: SCHEMA_VERSION,
|
|
391
|
-
savedAt: Date.now(),
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
const snapshotStartTime = Date.now();
|
|
395
|
-
|
|
396
|
-
yield* Effect.catchAll(
|
|
397
|
-
coldStorage.save(documentId, storedDocument),
|
|
398
|
-
(e) =>
|
|
399
|
-
Effect.logError("Failed to save snapshot", { documentId, error: e })
|
|
400
|
-
);
|
|
401
|
-
|
|
402
|
-
const snapshotDuration = Date.now() - snapshotStartTime;
|
|
403
|
-
yield* Metric.increment(Metrics.storageSnapshots);
|
|
404
|
-
yield* Metric.update(Metrics.storageSnapshotLatency, snapshotDuration);
|
|
405
|
-
|
|
406
|
-
yield* Effect.catchAll(hotStorage.truncate(documentId, version), (e) =>
|
|
407
|
-
Effect.logError("Failed to truncate WAL", { documentId, error: e })
|
|
408
|
-
);
|
|
409
|
-
|
|
410
|
-
yield* Ref.update(stateRef, (s) => ({
|
|
411
|
-
...s,
|
|
412
|
-
lastSnapshotVersion: version,
|
|
413
|
-
lastSnapshotTime: Date.now(),
|
|
414
|
-
transactionsSinceSnapshot: 0,
|
|
415
|
-
}));
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
/**
|
|
419
|
-
* Check if snapshot should be triggered
|
|
420
|
-
*/
|
|
421
|
-
const checkSnapshotTriggers = Effect.gen(function* () {
|
|
422
|
-
const state = yield* Ref.get(stateRef);
|
|
423
|
-
const now = Date.now();
|
|
424
|
-
|
|
425
|
-
const intervalMs = Duration.toMillis(config.snapshot.interval);
|
|
426
|
-
const threshold = config.snapshot.transactionThreshold;
|
|
427
|
-
|
|
428
|
-
if (state.transactionsSinceSnapshot >= threshold) {
|
|
429
|
-
yield* saveSnapshot;
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
if (now - state.lastSnapshotTime >= intervalMs) {
|
|
434
|
-
yield* saveSnapshot;
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
282
|
});
|
|
438
283
|
|
|
439
284
|
// Cleanup on entity finalization
|
|
440
285
|
yield* Effect.addFinalizer(() =>
|
|
441
|
-
Effect.
|
|
442
|
-
//
|
|
443
|
-
yield* saveSnapshot
|
|
286
|
+
Effect.fn("cluster.entity.finalize")(function* () {
|
|
287
|
+
// Best effort save - don't fail shutdown if storage is unavailable
|
|
288
|
+
yield* Effect.catchAll(instance.saveSnapshot(), (e) =>
|
|
289
|
+
Effect.logError("Failed to save snapshot during entity finalization", {
|
|
290
|
+
documentId,
|
|
291
|
+
error: e,
|
|
292
|
+
})
|
|
293
|
+
);
|
|
444
294
|
yield* Metric.incrementBy(Metrics.documentsActive, -1);
|
|
445
295
|
yield* Metric.increment(Metrics.documentsEvicted);
|
|
446
296
|
yield* Effect.logDebug("Entity finalized", { documentId });
|
|
447
|
-
})
|
|
297
|
+
})()
|
|
448
298
|
);
|
|
449
299
|
|
|
450
|
-
//
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
// Submit to ServerDocument
|
|
460
|
-
const result = state.document.submit(transaction);
|
|
461
|
-
|
|
462
|
-
// Track latency
|
|
463
|
-
const latency = Date.now() - submitStartTime;
|
|
464
|
-
yield* Metric.update(Metrics.transactionsLatency, latency);
|
|
465
|
-
|
|
466
|
-
if (result.success) {
|
|
467
|
-
yield* Metric.increment(Metrics.transactionsProcessed);
|
|
468
|
-
|
|
469
|
-
// Append to WAL
|
|
470
|
-
const walEntry: WalEntry = {
|
|
471
|
-
transaction,
|
|
472
|
-
version: result.version,
|
|
473
|
-
timestamp: Date.now(),
|
|
474
|
-
};
|
|
475
|
-
|
|
476
|
-
yield* Effect.catchAll(hotStorage.append(documentId, walEntry), (e) =>
|
|
477
|
-
Effect.logError("Failed to append to WAL", {
|
|
300
|
+
// Start periodic snapshot fiber for this entity
|
|
301
|
+
const idleTimeoutMs = Duration.toMillis(config.snapshot.idleTimeout);
|
|
302
|
+
if (idleTimeoutMs > 0) {
|
|
303
|
+
const snapshotLoop = Effect.fn("cluster.entity.snapshot.loop")(function* () {
|
|
304
|
+
const needs = yield* instance.needsSnapshot();
|
|
305
|
+
if (needs) {
|
|
306
|
+
yield* Effect.catchAll(instance.saveSnapshot(), (e) =>
|
|
307
|
+
Effect.logWarning("Periodic snapshot failed in cluster entity", {
|
|
478
308
|
documentId,
|
|
479
309
|
error: e,
|
|
480
310
|
})
|
|
481
311
|
);
|
|
312
|
+
yield* Metric.increment(Metrics.storageIdleSnapshots);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
482
315
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
}));
|
|
316
|
+
// Run every idleTimeout
|
|
317
|
+
yield* snapshotLoop().pipe(
|
|
318
|
+
Effect.repeat(Schedule.spaced(config.snapshot.idleTimeout)),
|
|
319
|
+
Effect.fork
|
|
320
|
+
);
|
|
321
|
+
}
|
|
490
322
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
323
|
+
// Return RPC handlers
|
|
324
|
+
return {
|
|
325
|
+
Submit: Effect.fn("cluster.document.transaction.submit")(function* ({
|
|
326
|
+
payload,
|
|
327
|
+
}) {
|
|
328
|
+
// Decode transaction
|
|
329
|
+
const transaction = decodeTransaction(payload.transaction);
|
|
496
330
|
|
|
497
|
-
|
|
331
|
+
// Use DocumentInstance's submit method, catching storage errors
|
|
332
|
+
return yield* instance.submit(transaction).pipe(
|
|
333
|
+
Effect.catchAll((error) =>
|
|
334
|
+
Effect.succeed({
|
|
335
|
+
success: false as const,
|
|
336
|
+
reason: `Storage error: ${String(error)}`,
|
|
337
|
+
})
|
|
338
|
+
)
|
|
339
|
+
);
|
|
498
340
|
}),
|
|
499
341
|
|
|
500
|
-
GetSnapshot: Effect.
|
|
501
|
-
|
|
502
|
-
return state.document.getSnapshot();
|
|
342
|
+
GetSnapshot: Effect.fn("cluster.document.snapshot.get")(function* () {
|
|
343
|
+
return instance.getSnapshot();
|
|
503
344
|
}),
|
|
504
345
|
|
|
505
|
-
Touch: Effect.
|
|
506
|
-
|
|
507
|
-
// Just update last activity time conceptually
|
|
508
|
-
return void 0;
|
|
346
|
+
Touch: Effect.fn("cluster.document.touch")(function* () {
|
|
347
|
+
yield* instance.touch();
|
|
509
348
|
}),
|
|
510
349
|
|
|
511
|
-
SetPresence: Effect.
|
|
350
|
+
SetPresence: Effect.fn("cluster.presence.set")(function* ({ payload }) {
|
|
512
351
|
const { connectionId, entry } = payload;
|
|
513
352
|
|
|
514
353
|
yield* Ref.update(stateRef, (s) => ({
|
|
@@ -529,7 +368,9 @@ const createEntityHandler = <TSchema extends Primitive.AnyPrimitive>(
|
|
|
529
368
|
yield* PubSub.publish(state.presencePubSub, event);
|
|
530
369
|
}),
|
|
531
370
|
|
|
532
|
-
RemovePresence: Effect.
|
|
371
|
+
RemovePresence: Effect.fn("cluster.presence.remove")(function* ({
|
|
372
|
+
payload,
|
|
373
|
+
}) {
|
|
533
374
|
const { connectionId } = payload;
|
|
534
375
|
const state = yield* Ref.get(stateRef);
|
|
535
376
|
|
|
@@ -551,16 +392,18 @@ const createEntityHandler = <TSchema extends Primitive.AnyPrimitive>(
|
|
|
551
392
|
yield* PubSub.publish(state.presencePubSub, event);
|
|
552
393
|
}),
|
|
553
394
|
|
|
554
|
-
GetPresenceSnapshot: Effect.
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
395
|
+
GetPresenceSnapshot: Effect.fn("cluster.presence.snapshot.get")(
|
|
396
|
+
function* () {
|
|
397
|
+
const state = yield* Ref.get(stateRef);
|
|
398
|
+
const presences: Record<string, PresenceEntry> = {};
|
|
399
|
+
for (const [id, entry] of state.presences) {
|
|
400
|
+
presences[id] = entry;
|
|
401
|
+
}
|
|
402
|
+
return { presences };
|
|
559
403
|
}
|
|
560
|
-
|
|
561
|
-
}),
|
|
404
|
+
),
|
|
562
405
|
};
|
|
563
|
-
});
|
|
406
|
+
})();
|
|
564
407
|
|
|
565
408
|
// =============================================================================
|
|
566
409
|
// Subscription Store (for managing subscriptions at the gateway level)
|
|
@@ -585,7 +428,7 @@ class SubscriptionStoreTag extends Context.Tag(
|
|
|
585
428
|
|
|
586
429
|
const subscriptionStoreLayer = Layer.effect(
|
|
587
430
|
SubscriptionStoreTag,
|
|
588
|
-
Effect.
|
|
431
|
+
Effect.fn("cluster.subscriptions.layer.create")(function* () {
|
|
589
432
|
const documentPubSubs = yield* Ref.make(
|
|
590
433
|
HashMap.empty<string, PubSub.PubSub<Protocol.ServerMessage>>()
|
|
591
434
|
);
|
|
@@ -594,37 +437,39 @@ const subscriptionStoreLayer = Layer.effect(
|
|
|
594
437
|
);
|
|
595
438
|
|
|
596
439
|
return {
|
|
597
|
-
getOrCreatePubSub: (
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
440
|
+
getOrCreatePubSub: Effect.fn(
|
|
441
|
+
"cluster.subscriptions.pubsub.get-or-create"
|
|
442
|
+
)(function* (documentId: string) {
|
|
443
|
+
const current = yield* Ref.get(documentPubSubs);
|
|
444
|
+
const existing = HashMap.get(current, documentId);
|
|
445
|
+
if (existing._tag === "Some") {
|
|
446
|
+
return existing.value;
|
|
447
|
+
}
|
|
604
448
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
getOrCreatePresencePubSub: (documentId: string) =>
|
|
613
|
-
Effect.gen(function* () {
|
|
614
|
-
const current = yield* Ref.get(presencePubSubs);
|
|
615
|
-
const existing = HashMap.get(current, documentId);
|
|
616
|
-
if (existing._tag === "Some") {
|
|
617
|
-
return existing.value;
|
|
618
|
-
}
|
|
449
|
+
const pubsub = yield* PubSub.unbounded<Protocol.ServerMessage>();
|
|
450
|
+
yield* Ref.update(documentPubSubs, (map) =>
|
|
451
|
+
HashMap.set(map, documentId, pubsub)
|
|
452
|
+
);
|
|
453
|
+
return pubsub;
|
|
454
|
+
}),
|
|
619
455
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
456
|
+
getOrCreatePresencePubSub: Effect.fn(
|
|
457
|
+
"cluster.subscriptions.presence-pubsub.get-or-create"
|
|
458
|
+
)(function* (documentId: string) {
|
|
459
|
+
const current = yield* Ref.get(presencePubSubs);
|
|
460
|
+
const existing = HashMap.get(current, documentId);
|
|
461
|
+
if (existing._tag === "Some") {
|
|
462
|
+
return existing.value;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const pubsub = yield* PubSub.unbounded<PresenceEvent>();
|
|
466
|
+
yield* Ref.update(presencePubSubs, (map) =>
|
|
467
|
+
HashMap.set(map, documentId, pubsub)
|
|
468
|
+
);
|
|
469
|
+
return pubsub;
|
|
470
|
+
}),
|
|
626
471
|
};
|
|
627
|
-
})
|
|
472
|
+
})()
|
|
628
473
|
);
|
|
629
474
|
|
|
630
475
|
// =============================================================================
|