@voidhash/mimic-effect 1.0.0-beta.5 → 1.0.0-beta.7
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 +91 -91
- 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/DocumentManager.cjs +14 -14
- package/dist/DocumentManager.d.cts.map +1 -1
- package/dist/DocumentManager.d.mts.map +1 -1
- package/dist/DocumentManager.mjs +14 -14
- package/dist/DocumentManager.mjs.map +1 -1
- package/dist/HotStorage.cjs +17 -13
- package/dist/HotStorage.d.cts.map +1 -1
- package/dist/HotStorage.d.mts.map +1 -1
- package/dist/HotStorage.mjs +17 -13
- package/dist/HotStorage.mjs.map +1 -1
- package/dist/MimicClusterServerEngine.cjs +17 -17
- package/dist/MimicClusterServerEngine.d.cts.map +1 -1
- package/dist/MimicClusterServerEngine.d.mts.map +1 -1
- package/dist/MimicClusterServerEngine.mjs +17 -17
- package/dist/MimicClusterServerEngine.mjs.map +1 -1
- package/dist/MimicServer.cjs +19 -19
- package/dist/MimicServer.d.cts.map +1 -1
- package/dist/MimicServer.d.mts.map +1 -1
- package/dist/MimicServer.mjs +19 -19
- package/dist/MimicServer.mjs.map +1 -1
- package/dist/MimicServerEngine.cjs +2 -2
- package/dist/MimicServerEngine.mjs +2 -2
- 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/testing/HotStorageTestSuite.cjs +7 -19
- package/dist/testing/HotStorageTestSuite.mjs +7 -19
- package/dist/testing/HotStorageTestSuite.mjs.map +1 -1
- package/package.json +3 -3
- package/src/ColdStorage.ts +21 -12
- package/src/DocumentManager.ts +49 -47
- package/src/HotStorage.ts +75 -58
- package/src/MimicClusterServerEngine.ts +61 -49
- package/src/MimicServer.ts +83 -75
- package/src/MimicServerEngine.ts +2 -2
- package/src/PresenceManager.ts +44 -34
- package/src/testing/HotStorageTestSuite.ts +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voidhash/mimic-effect",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,14 +26,14 @@
|
|
|
26
26
|
"typescript": "5.8.3",
|
|
27
27
|
"vite-tsconfig-paths": "^5.1.4",
|
|
28
28
|
"vitest": "^3.2.4",
|
|
29
|
-
"@voidhash/tsconfig": "1.0.0-beta.
|
|
29
|
+
"@voidhash/tsconfig": "1.0.0-beta.7"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"@effect/platform": "^0.93.8",
|
|
33
33
|
"@effect/cluster": "^0.55.0",
|
|
34
34
|
"@effect/rpc": "^0.72.2",
|
|
35
35
|
"effect": "^3.19.12",
|
|
36
|
-
"@voidhash/mimic": "1.0.0-beta.
|
|
36
|
+
"@voidhash/mimic": "1.0.0-beta.7"
|
|
37
37
|
},
|
|
38
38
|
"peerDependenciesMeta": {
|
|
39
39
|
"@effect/cluster": {
|
package/src/ColdStorage.ts
CHANGED
|
@@ -104,24 +104,33 @@ export namespace InMemory {
|
|
|
104
104
|
export const make = (): Layer.Layer<ColdStorageTag> =>
|
|
105
105
|
Layer.effect(
|
|
106
106
|
ColdStorageTag,
|
|
107
|
-
Effect.
|
|
107
|
+
Effect.fn("cold-storage.in-memory.create")(function* () {
|
|
108
108
|
const store = yield* Ref.make(HashMap.empty<string, StoredDocument>());
|
|
109
109
|
|
|
110
110
|
return {
|
|
111
|
-
load: (documentId)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}),
|
|
111
|
+
load: Effect.fn("cold-storage.load")(function* (documentId: string) {
|
|
112
|
+
const current = yield* Ref.get(store);
|
|
113
|
+
const result = HashMap.get(current, documentId);
|
|
114
|
+
return result._tag === "Some" ? result.value : undefined;
|
|
115
|
+
}),
|
|
117
116
|
|
|
118
|
-
save: (
|
|
119
|
-
|
|
117
|
+
save: Effect.fn("cold-storage.save")(
|
|
118
|
+
function* (documentId: string, document: StoredDocument) {
|
|
119
|
+
yield* Ref.update(store, (map) =>
|
|
120
|
+
HashMap.set(map, documentId, document)
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
),
|
|
120
124
|
|
|
121
|
-
delete: (
|
|
122
|
-
|
|
125
|
+
delete: Effect.fn("cold-storage.delete")(
|
|
126
|
+
function* (documentId: string) {
|
|
127
|
+
yield* Ref.update(store, (map) =>
|
|
128
|
+
HashMap.remove(map, documentId)
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
),
|
|
123
132
|
};
|
|
124
|
-
})
|
|
133
|
+
})()
|
|
125
134
|
);
|
|
126
135
|
}
|
|
127
136
|
|
package/src/DocumentManager.ts
CHANGED
|
@@ -176,10 +176,8 @@ export const layer = Layer.scoped(
|
|
|
176
176
|
/**
|
|
177
177
|
* Restore a document from storage
|
|
178
178
|
*/
|
|
179
|
-
const restoreDocument = (
|
|
180
|
-
documentId: string
|
|
181
|
-
): Effect.Effect<DocumentInstance<typeof config.schema>, ColdStorageError | HotStorageError> =>
|
|
182
|
-
Effect.gen(function* () {
|
|
179
|
+
const restoreDocument = Effect.fn("document.restore")(
|
|
180
|
+
function* (documentId: string) {
|
|
183
181
|
// 1. Load snapshot from ColdStorage (errors propagate - do not silently fallback)
|
|
184
182
|
const storedDoc = yield* coldStorage.load(documentId);
|
|
185
183
|
|
|
@@ -294,15 +292,14 @@ export const layer = Layer.scoped(
|
|
|
294
292
|
yield* Metric.incrementBy(Metrics.documentsActive, 1);
|
|
295
293
|
|
|
296
294
|
return instance;
|
|
297
|
-
}
|
|
295
|
+
}
|
|
296
|
+
);
|
|
298
297
|
|
|
299
298
|
/**
|
|
300
299
|
* Get or create a document instance
|
|
301
300
|
*/
|
|
302
|
-
const getOrCreateDocument = (
|
|
303
|
-
documentId: string
|
|
304
|
-
): Effect.Effect<DocumentInstance<typeof config.schema>, ColdStorageError | HotStorageError> =>
|
|
305
|
-
Effect.gen(function* () {
|
|
301
|
+
const getOrCreateDocument = Effect.fn("document.get-or-create")(
|
|
302
|
+
function* (documentId: string) {
|
|
306
303
|
const current = yield* Ref.get(store);
|
|
307
304
|
const existing = HashMap.get(current, documentId);
|
|
308
305
|
|
|
@@ -321,7 +318,8 @@ export const layer = Layer.scoped(
|
|
|
321
318
|
);
|
|
322
319
|
|
|
323
320
|
return instance;
|
|
324
|
-
}
|
|
321
|
+
}
|
|
322
|
+
);
|
|
325
323
|
|
|
326
324
|
/**
|
|
327
325
|
* Save a snapshot to ColdStorage derived from WAL entries.
|
|
@@ -329,12 +327,12 @@ export const layer = Layer.scoped(
|
|
|
329
327
|
* Idempotent: skips save if already snapshotted at target version.
|
|
330
328
|
* Truncate failures are non-fatal and will be retried on next snapshot.
|
|
331
329
|
*/
|
|
332
|
-
const saveSnapshot = (
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
330
|
+
const saveSnapshot = Effect.fn("document.snapshot.save")(
|
|
331
|
+
function* (
|
|
332
|
+
documentId: string,
|
|
333
|
+
instance: DocumentInstance<typeof config.schema>,
|
|
334
|
+
targetVersion: number
|
|
335
|
+
) {
|
|
338
336
|
const lastSnapshotVersion = yield* Ref.get(instance.lastSnapshotVersion);
|
|
339
337
|
|
|
340
338
|
// Idempotency check: skip if already snapshotted at this version
|
|
@@ -410,16 +408,17 @@ export const layer = Layer.scoped(
|
|
|
410
408
|
error: e,
|
|
411
409
|
})
|
|
412
410
|
);
|
|
413
|
-
}
|
|
411
|
+
}
|
|
412
|
+
);
|
|
414
413
|
|
|
415
414
|
/**
|
|
416
415
|
* Check if snapshot should be triggered
|
|
417
416
|
*/
|
|
418
|
-
const checkSnapshotTriggers = (
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
417
|
+
const checkSnapshotTriggers = Effect.fn("document.snapshot.check-triggers")(
|
|
418
|
+
function* (
|
|
419
|
+
documentId: string,
|
|
420
|
+
instance: DocumentInstance<typeof config.schema>
|
|
421
|
+
) {
|
|
423
422
|
const txCount = yield* Ref.get(instance.transactionsSinceSnapshot);
|
|
424
423
|
const lastTime = yield* Ref.get(instance.lastSnapshotTime);
|
|
425
424
|
const now = Date.now();
|
|
@@ -439,13 +438,14 @@ export const layer = Layer.scoped(
|
|
|
439
438
|
yield* saveSnapshot(documentId, instance, currentVersion);
|
|
440
439
|
return;
|
|
441
440
|
}
|
|
442
|
-
}
|
|
441
|
+
}
|
|
442
|
+
);
|
|
443
443
|
|
|
444
444
|
/**
|
|
445
445
|
* Start background GC fiber
|
|
446
446
|
*/
|
|
447
|
-
const startGCFiber = Effect.
|
|
448
|
-
const gcLoop = Effect.
|
|
447
|
+
const startGCFiber = Effect.fn("document.gc.start")(function* () {
|
|
448
|
+
const gcLoop = Effect.fn("document.gc.loop")(function* () {
|
|
449
449
|
const current = yield* Ref.get(store);
|
|
450
450
|
const now = Date.now();
|
|
451
451
|
const maxIdleMs = Duration.toMillis(config.maxIdleTime);
|
|
@@ -477,18 +477,18 @@ export const layer = Layer.scoped(
|
|
|
477
477
|
});
|
|
478
478
|
|
|
479
479
|
// Run GC every minute
|
|
480
|
-
yield* gcLoop.pipe(
|
|
480
|
+
yield* gcLoop().pipe(
|
|
481
481
|
Effect.repeat(Schedule.spaced("1 minute")),
|
|
482
482
|
Effect.fork
|
|
483
483
|
);
|
|
484
484
|
});
|
|
485
485
|
|
|
486
486
|
// Start GC fiber
|
|
487
|
-
yield* startGCFiber;
|
|
487
|
+
yield* startGCFiber();
|
|
488
488
|
|
|
489
489
|
// Cleanup on shutdown
|
|
490
490
|
yield* Effect.addFinalizer(() =>
|
|
491
|
-
Effect.
|
|
491
|
+
Effect.fn("document-manager.shutdown")(function* () {
|
|
492
492
|
const current = yield* Ref.get(store);
|
|
493
493
|
for (const [documentId, instance] of current) {
|
|
494
494
|
// Best effort save - don't fail shutdown if storage is unavailable
|
|
@@ -501,12 +501,12 @@ export const layer = Layer.scoped(
|
|
|
501
501
|
);
|
|
502
502
|
}
|
|
503
503
|
yield* Effect.logInfo("DocumentManager shutdown complete");
|
|
504
|
-
})
|
|
504
|
+
})()
|
|
505
505
|
);
|
|
506
506
|
|
|
507
507
|
return {
|
|
508
|
-
submit: (
|
|
509
|
-
|
|
508
|
+
submit: Effect.fn("document.transaction.submit")(
|
|
509
|
+
function* (documentId: string, transaction: Transaction.Transaction) {
|
|
510
510
|
const instance = yield* getOrCreateDocument(documentId);
|
|
511
511
|
const submitStartTime = Date.now();
|
|
512
512
|
|
|
@@ -577,28 +577,30 @@ export const layer = Layer.scoped(
|
|
|
577
577
|
success: true as const,
|
|
578
578
|
version: validation.nextVersion,
|
|
579
579
|
};
|
|
580
|
-
}
|
|
580
|
+
}
|
|
581
|
+
),
|
|
581
582
|
|
|
582
|
-
getSnapshot: (
|
|
583
|
-
|
|
583
|
+
getSnapshot: Effect.fn("document.snapshot.get")(
|
|
584
|
+
function* (documentId: string) {
|
|
584
585
|
const instance = yield* getOrCreateDocument(documentId);
|
|
585
586
|
return instance.document.getSnapshot();
|
|
586
|
-
}
|
|
587
|
+
}
|
|
588
|
+
),
|
|
587
589
|
|
|
588
|
-
subscribe: (
|
|
589
|
-
|
|
590
|
+
subscribe: Effect.fn("document.subscribe")(
|
|
591
|
+
function* (documentId: string) {
|
|
590
592
|
const instance = yield* getOrCreateDocument(documentId);
|
|
591
593
|
return Stream.fromPubSub(instance.pubsub);
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
594
|
+
}
|
|
595
|
+
),
|
|
596
|
+
|
|
597
|
+
touch: Effect.fn("document.touch")(function* (documentId: string) {
|
|
598
|
+
const current = yield* Ref.get(store);
|
|
599
|
+
const existing = HashMap.get(current, documentId);
|
|
600
|
+
if (existing._tag === "Some") {
|
|
601
|
+
yield* Ref.set(existing.value.lastActivityTime, Date.now());
|
|
602
|
+
}
|
|
603
|
+
}),
|
|
602
604
|
};
|
|
603
605
|
})
|
|
604
606
|
);
|
package/src/HotStorage.ts
CHANGED
|
@@ -136,59 +136,71 @@ export namespace InMemory {
|
|
|
136
136
|
export const make = (): Layer.Layer<HotStorageTag> =>
|
|
137
137
|
Layer.effect(
|
|
138
138
|
HotStorageTag,
|
|
139
|
-
Effect.
|
|
139
|
+
Effect.fn("hot-storage.in-memory.create")(function* () {
|
|
140
140
|
const store = yield* Ref.make(HashMap.empty<string, WalEntry[]>());
|
|
141
141
|
|
|
142
142
|
return {
|
|
143
|
-
append: (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
143
|
+
append: Effect.fn("hot-storage.append")(
|
|
144
|
+
function* (documentId: string, entry: WalEntry) {
|
|
145
|
+
yield* Ref.update(store, (map) => {
|
|
146
|
+
const existing = HashMap.get(map, documentId);
|
|
147
|
+
const entries =
|
|
148
|
+
existing._tag === "Some" ? existing.value : [];
|
|
149
|
+
return HashMap.set(map, documentId, [...entries, entry]);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
),
|
|
150
153
|
|
|
151
|
-
appendWithCheck: (
|
|
152
|
-
|
|
154
|
+
appendWithCheck: Effect.fn("hot-storage.append-with-check")(
|
|
155
|
+
function* (
|
|
156
|
+
documentId: string,
|
|
157
|
+
entry: WalEntry,
|
|
158
|
+
expectedVersion: number
|
|
159
|
+
) {
|
|
153
160
|
type CheckResult =
|
|
154
161
|
| { type: "ok" }
|
|
155
162
|
| { type: "gap"; lastVersion: number | undefined };
|
|
156
163
|
|
|
157
164
|
// Use Ref.modify for atomic check + update
|
|
158
|
-
const result: CheckResult = yield* Ref.modify(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
165
|
+
const result: CheckResult = yield* Ref.modify(
|
|
166
|
+
store,
|
|
167
|
+
(map): [CheckResult, HashMap.HashMap<string, WalEntry[]>] => {
|
|
168
|
+
const existing = HashMap.get(map, documentId);
|
|
169
|
+
const entries =
|
|
170
|
+
existing._tag === "Some" ? existing.value : [];
|
|
171
|
+
|
|
172
|
+
// Find the highest version in existing entries
|
|
173
|
+
const lastVersion =
|
|
174
|
+
entries.length > 0
|
|
175
|
+
? Math.max(...entries.map((e) => e.version))
|
|
176
|
+
: 0;
|
|
177
|
+
|
|
178
|
+
// Gap check
|
|
179
|
+
if (expectedVersion === 1) {
|
|
180
|
+
// First entry: should have no entries with version >= 1
|
|
181
|
+
if (lastVersion >= 1) {
|
|
182
|
+
return [{ type: "gap", lastVersion }, map];
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
// Not first: last entry should have version = expectedVersion - 1
|
|
186
|
+
if (lastVersion !== expectedVersion - 1) {
|
|
187
|
+
return [
|
|
188
|
+
{
|
|
189
|
+
type: "gap",
|
|
190
|
+
lastVersion: lastVersion > 0 ? lastVersion : undefined,
|
|
191
|
+
},
|
|
192
|
+
map,
|
|
193
|
+
];
|
|
194
|
+
}
|
|
183
195
|
}
|
|
184
|
-
}
|
|
185
196
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
197
|
+
// No gap: append and return success
|
|
198
|
+
return [
|
|
199
|
+
{ type: "ok" },
|
|
200
|
+
HashMap.set(map, documentId, [...entries, entry]),
|
|
201
|
+
];
|
|
202
|
+
}
|
|
203
|
+
);
|
|
192
204
|
|
|
193
205
|
if (result.type === "gap") {
|
|
194
206
|
return yield* Effect.fail(
|
|
@@ -199,10 +211,11 @@ export namespace InMemory {
|
|
|
199
211
|
})
|
|
200
212
|
);
|
|
201
213
|
}
|
|
202
|
-
}
|
|
214
|
+
}
|
|
215
|
+
),
|
|
203
216
|
|
|
204
|
-
getEntries: (
|
|
205
|
-
|
|
217
|
+
getEntries: Effect.fn("hot-storage.get-entries")(
|
|
218
|
+
function* (documentId: string, sinceVersion: number) {
|
|
206
219
|
const current = yield* Ref.get(store);
|
|
207
220
|
const existing = HashMap.get(current, documentId);
|
|
208
221
|
const entries =
|
|
@@ -210,21 +223,25 @@ export namespace InMemory {
|
|
|
210
223
|
return entries
|
|
211
224
|
.filter((e) => e.version > sinceVersion)
|
|
212
225
|
.sort((a, b) => a.version - b.version);
|
|
213
|
-
}
|
|
226
|
+
}
|
|
227
|
+
),
|
|
214
228
|
|
|
215
|
-
truncate: (
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
229
|
+
truncate: Effect.fn("hot-storage.truncate")(
|
|
230
|
+
function* (documentId: string, upToVersion: number) {
|
|
231
|
+
yield* Ref.update(store, (map) => {
|
|
232
|
+
const existing = HashMap.get(map, documentId);
|
|
233
|
+
if (existing._tag === "None") {
|
|
234
|
+
return map;
|
|
235
|
+
}
|
|
236
|
+
const filtered = existing.value.filter(
|
|
237
|
+
(e) => e.version > upToVersion
|
|
238
|
+
);
|
|
239
|
+
return HashMap.set(map, documentId, filtered);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
),
|
|
226
243
|
};
|
|
227
|
-
})
|
|
244
|
+
})()
|
|
228
245
|
);
|
|
229
246
|
}
|
|
230
247
|
|
|
@@ -257,7 +257,7 @@ const createEntityHandler = <TSchema extends Primitive.AnyPrimitive>(
|
|
|
257
257
|
coldStorage: ColdStorage,
|
|
258
258
|
hotStorage: HotStorage
|
|
259
259
|
) =>
|
|
260
|
-
Effect.
|
|
260
|
+
Effect.fn("cluster.entity.handler.create")(function* () {
|
|
261
261
|
// Get entity address to determine documentId
|
|
262
262
|
const address = yield* Entity.CurrentAddress;
|
|
263
263
|
const documentId = address.entityId;
|
|
@@ -406,7 +406,8 @@ const createEntityHandler = <TSchema extends Primitive.AnyPrimitive>(
|
|
|
406
406
|
* Idempotent: skips save if already snapshotted at target version.
|
|
407
407
|
* Truncate failures are non-fatal and will be retried on next snapshot.
|
|
408
408
|
*/
|
|
409
|
-
const saveSnapshot =
|
|
409
|
+
const saveSnapshot = Effect.fn("cluster.document.snapshot.save")(
|
|
410
|
+
function* (targetVersion: number) {
|
|
410
411
|
const state = yield* Ref.get(stateRef);
|
|
411
412
|
|
|
412
413
|
// Idempotency check: skip if already snapshotted at this version
|
|
@@ -504,12 +505,15 @@ const createEntityHandler = <TSchema extends Primitive.AnyPrimitive>(
|
|
|
504
505
|
error: e,
|
|
505
506
|
})
|
|
506
507
|
);
|
|
507
|
-
|
|
508
|
+
}
|
|
509
|
+
);
|
|
508
510
|
|
|
509
511
|
/**
|
|
510
512
|
* Check if snapshot should be triggered
|
|
511
513
|
*/
|
|
512
|
-
const checkSnapshotTriggers = Effect.
|
|
514
|
+
const checkSnapshotTriggers = Effect.fn(
|
|
515
|
+
"cluster.document.snapshot.check-triggers"
|
|
516
|
+
)(function* () {
|
|
513
517
|
const state = yield* Ref.get(stateRef);
|
|
514
518
|
const now = Date.now();
|
|
515
519
|
const currentVersion = state.document.getVersion();
|
|
@@ -530,7 +534,7 @@ const createEntityHandler = <TSchema extends Primitive.AnyPrimitive>(
|
|
|
530
534
|
|
|
531
535
|
// Cleanup on entity finalization
|
|
532
536
|
yield* Effect.addFinalizer(() =>
|
|
533
|
-
Effect.
|
|
537
|
+
Effect.fn("cluster.entity.finalize")(function* () {
|
|
534
538
|
// Save final snapshot before entity is garbage collected
|
|
535
539
|
const state = yield* Ref.get(stateRef);
|
|
536
540
|
const currentVersion = state.document.getVersion();
|
|
@@ -538,12 +542,14 @@ const createEntityHandler = <TSchema extends Primitive.AnyPrimitive>(
|
|
|
538
542
|
yield* Metric.incrementBy(Metrics.documentsActive, -1);
|
|
539
543
|
yield* Metric.increment(Metrics.documentsEvicted);
|
|
540
544
|
yield* Effect.logDebug("Entity finalized", { documentId });
|
|
541
|
-
})
|
|
545
|
+
})()
|
|
542
546
|
);
|
|
543
547
|
|
|
544
548
|
// Return RPC handlers
|
|
545
549
|
return {
|
|
546
|
-
Submit: Effect.
|
|
550
|
+
Submit: Effect.fn("cluster.document.transaction.submit")(function* ({
|
|
551
|
+
payload,
|
|
552
|
+
}) {
|
|
547
553
|
const submitStartTime = Date.now();
|
|
548
554
|
const state = yield* Ref.get(stateRef);
|
|
549
555
|
|
|
@@ -611,7 +617,7 @@ const createEntityHandler = <TSchema extends Primitive.AnyPrimitive>(
|
|
|
611
617
|
}));
|
|
612
618
|
|
|
613
619
|
// Check snapshot triggers
|
|
614
|
-
yield* checkSnapshotTriggers;
|
|
620
|
+
yield* checkSnapshotTriggers();
|
|
615
621
|
|
|
616
622
|
return {
|
|
617
623
|
success: true as const,
|
|
@@ -619,18 +625,18 @@ const createEntityHandler = <TSchema extends Primitive.AnyPrimitive>(
|
|
|
619
625
|
};
|
|
620
626
|
}),
|
|
621
627
|
|
|
622
|
-
GetSnapshot: Effect.
|
|
628
|
+
GetSnapshot: Effect.fn("cluster.document.snapshot.get")(function* () {
|
|
623
629
|
const state = yield* Ref.get(stateRef);
|
|
624
630
|
return state.document.getSnapshot();
|
|
625
631
|
}),
|
|
626
632
|
|
|
627
|
-
Touch: Effect.
|
|
633
|
+
Touch: Effect.fn("cluster.document.touch")(function* () {
|
|
628
634
|
// Entity touch is handled automatically by the cluster framework
|
|
629
635
|
// Just update last activity time conceptually
|
|
630
636
|
return void 0;
|
|
631
637
|
}),
|
|
632
638
|
|
|
633
|
-
SetPresence: Effect.
|
|
639
|
+
SetPresence: Effect.fn("cluster.presence.set")(function* ({ payload }) {
|
|
634
640
|
const { connectionId, entry } = payload;
|
|
635
641
|
|
|
636
642
|
yield* Ref.update(stateRef, (s) => ({
|
|
@@ -651,7 +657,9 @@ const createEntityHandler = <TSchema extends Primitive.AnyPrimitive>(
|
|
|
651
657
|
yield* PubSub.publish(state.presencePubSub, event);
|
|
652
658
|
}),
|
|
653
659
|
|
|
654
|
-
RemovePresence: Effect.
|
|
660
|
+
RemovePresence: Effect.fn("cluster.presence.remove")(function* ({
|
|
661
|
+
payload,
|
|
662
|
+
}) {
|
|
655
663
|
const { connectionId } = payload;
|
|
656
664
|
const state = yield* Ref.get(stateRef);
|
|
657
665
|
|
|
@@ -673,16 +681,18 @@ const createEntityHandler = <TSchema extends Primitive.AnyPrimitive>(
|
|
|
673
681
|
yield* PubSub.publish(state.presencePubSub, event);
|
|
674
682
|
}),
|
|
675
683
|
|
|
676
|
-
GetPresenceSnapshot: Effect.
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
684
|
+
GetPresenceSnapshot: Effect.fn("cluster.presence.snapshot.get")(
|
|
685
|
+
function* () {
|
|
686
|
+
const state = yield* Ref.get(stateRef);
|
|
687
|
+
const presences: Record<string, PresenceEntry> = {};
|
|
688
|
+
for (const [id, entry] of state.presences) {
|
|
689
|
+
presences[id] = entry;
|
|
690
|
+
}
|
|
691
|
+
return { presences };
|
|
681
692
|
}
|
|
682
|
-
|
|
683
|
-
}),
|
|
693
|
+
),
|
|
684
694
|
};
|
|
685
|
-
});
|
|
695
|
+
})();
|
|
686
696
|
|
|
687
697
|
// =============================================================================
|
|
688
698
|
// Subscription Store (for managing subscriptions at the gateway level)
|
|
@@ -707,7 +717,7 @@ class SubscriptionStoreTag extends Context.Tag(
|
|
|
707
717
|
|
|
708
718
|
const subscriptionStoreLayer = Layer.effect(
|
|
709
719
|
SubscriptionStoreTag,
|
|
710
|
-
Effect.
|
|
720
|
+
Effect.fn("cluster.subscriptions.layer.create")(function* () {
|
|
711
721
|
const documentPubSubs = yield* Ref.make(
|
|
712
722
|
HashMap.empty<string, PubSub.PubSub<Protocol.ServerMessage>>()
|
|
713
723
|
);
|
|
@@ -716,37 +726,39 @@ const subscriptionStoreLayer = Layer.effect(
|
|
|
716
726
|
);
|
|
717
727
|
|
|
718
728
|
return {
|
|
719
|
-
getOrCreatePubSub: (
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
729
|
+
getOrCreatePubSub: Effect.fn(
|
|
730
|
+
"cluster.subscriptions.pubsub.get-or-create"
|
|
731
|
+
)(function* (documentId: string) {
|
|
732
|
+
const current = yield* Ref.get(documentPubSubs);
|
|
733
|
+
const existing = HashMap.get(current, documentId);
|
|
734
|
+
if (existing._tag === "Some") {
|
|
735
|
+
return existing.value;
|
|
736
|
+
}
|
|
726
737
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
getOrCreatePresencePubSub: (
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
738
|
+
const pubsub = yield* PubSub.unbounded<Protocol.ServerMessage>();
|
|
739
|
+
yield* Ref.update(documentPubSubs, (map) =>
|
|
740
|
+
HashMap.set(map, documentId, pubsub)
|
|
741
|
+
);
|
|
742
|
+
return pubsub;
|
|
743
|
+
}),
|
|
744
|
+
|
|
745
|
+
getOrCreatePresencePubSub: Effect.fn(
|
|
746
|
+
"cluster.subscriptions.presence-pubsub.get-or-create"
|
|
747
|
+
)(function* (documentId: string) {
|
|
748
|
+
const current = yield* Ref.get(presencePubSubs);
|
|
749
|
+
const existing = HashMap.get(current, documentId);
|
|
750
|
+
if (existing._tag === "Some") {
|
|
751
|
+
return existing.value;
|
|
752
|
+
}
|
|
741
753
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
754
|
+
const pubsub = yield* PubSub.unbounded<PresenceEvent>();
|
|
755
|
+
yield* Ref.update(presencePubSubs, (map) =>
|
|
756
|
+
HashMap.set(map, documentId, pubsub)
|
|
757
|
+
);
|
|
758
|
+
return pubsub;
|
|
759
|
+
}),
|
|
748
760
|
};
|
|
749
|
-
})
|
|
761
|
+
})()
|
|
750
762
|
);
|
|
751
763
|
|
|
752
764
|
// =============================================================================
|