@voidhash/mimic-effect 1.0.0-beta.11 → 1.0.0-beta.13

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @voidhash/mimic-effect@1.0.0-beta.11 build /home/runner/work/mimic/mimic/packages/mimic-effect
2
+ > @voidhash/mimic-effect@1.0.0-beta.13 build /home/runner/work/mimic/mimic/packages/mimic-effect
3
3
  > tsdown
4
4
 
5
5
  ℹ tsdown v0.18.2 powered by rolldown v1.0.0-beta.55
@@ -13,10 +13,10 @@
13
13
  ℹ [CJS] dist/testing/HotStorageTestSuite.cjs 40.13 kB │ gzip: 5.31 kB
14
14
  ℹ [CJS] dist/testing/StorageIntegrationTestSuite.cjs 20.68 kB │ gzip: 3.67 kB
15
15
  ℹ [CJS] dist/testing/ColdStorageTestSuite.cjs 18.14 kB │ gzip: 3.22 kB
16
- ℹ [CJS] dist/MimicClusterServerEngine.cjs 15.02 kB │ gzip: 3.53 kB
17
- ℹ [CJS] dist/DocumentInstance.cjs 10.55 kB │ gzip: 2.75 kB
16
+ ℹ [CJS] dist/MimicClusterServerEngine.cjs 15.39 kB │ gzip: 3.56 kB
17
+ ℹ [CJS] dist/DocumentInstance.cjs 10.59 kB │ gzip: 2.75 kB
18
18
  ℹ [CJS] dist/MimicServer.cjs 10.37 kB │ gzip: 2.84 kB
19
- ℹ [CJS] dist/MimicServerEngine.cjs  8.30 kB │ gzip: 2.21 kB
19
+ ℹ [CJS] dist/MimicServerEngine.cjs  8.45 kB │ gzip: 2.21 kB
20
20
  ℹ [CJS] dist/testing/FailingStorage.cjs  6.91 kB │ gzip: 1.53 kB
21
21
  ℹ [CJS] dist/Metrics.cjs  4.47 kB │ gzip: 1.08 kB
22
22
  ℹ [CJS] dist/HotStorage.cjs  4.06 kB │ gzip: 1.31 kB
@@ -33,11 +33,11 @@
33
33
  ℹ [CJS] dist/_virtual/_@oxc-project_runtime@0.103.0/helpers/toPropertyKey.cjs  0.37 kB │ gzip: 0.23 kB
34
34
  ℹ [CJS] dist/_virtual/rolldown_runtime.cjs  0.36 kB │ gzip: 0.25 kB
35
35
  ℹ [CJS] dist/testing/types.cjs  0.34 kB │ gzip: 0.23 kB
36
- ℹ [CJS] 25 files, total: 162.99 kB
36
+ ℹ [CJS] 25 files, total: 163.55 kB
37
37
  ℹ [CJS] dist/Protocol.d.cts.map 1.57 kB │ gzip: 0.70 kB
38
38
  ℹ [CJS] dist/Types.d.cts.map 1.44 kB │ gzip: 0.66 kB
39
- ℹ [CJS] dist/DocumentInstance.d.cts.map 1.26 kB │ gzip: 0.57 kB
40
- ℹ [CJS] dist/MimicServerEngine.d.cts.map 0.97 kB │ gzip: 0.49 kB
39
+ ℹ [CJS] dist/DocumentInstance.d.cts.map 1.32 kB │ gzip: 0.59 kB
40
+ ℹ [CJS] dist/MimicServerEngine.d.cts.map 0.93 kB │ gzip: 0.46 kB
41
41
  ℹ [CJS] dist/ColdStorage.d.cts.map 0.70 kB │ gzip: 0.37 kB
42
42
  ℹ [CJS] dist/MimicAuthService.d.cts.map 0.61 kB │ gzip: 0.35 kB
43
43
  ℹ [CJS] dist/Errors.d.cts.map 0.60 kB │ gzip: 0.32 kB
@@ -56,9 +56,9 @@
56
56
  ℹ [CJS] dist/Protocol.d.cts 5.84 kB │ gzip: 1.34 kB
57
57
  ℹ [CJS] dist/Types.d.cts 5.72 kB │ gzip: 1.61 kB
58
58
  ℹ [CJS] dist/Errors.d.cts 4.86 kB │ gzip: 1.02 kB
59
- ℹ [CJS] dist/MimicServerEngine.d.cts 3.60 kB │ gzip: 1.09 kB
59
+ ℹ [CJS] dist/MimicServerEngine.d.cts 4.03 kB │ gzip: 1.20 kB
60
+ ℹ [CJS] dist/DocumentInstance.d.cts 3.34 kB │ gzip: 1.10 kB
60
61
  ℹ [CJS] dist/HotStorage.d.cts 3.11 kB │ gzip: 1.23 kB
61
- ℹ [CJS] dist/DocumentInstance.d.cts 3.07 kB │ gzip: 1.00 kB
62
62
  ℹ [CJS] dist/testing/types.d.cts 2.76 kB │ gzip: 1.15 kB
63
63
  ℹ [CJS] dist/MimicAuthService.d.cts 2.70 kB │ gzip: 1.04 kB
64
64
  ℹ [CJS] dist/testing/HotStorageTestSuite.d.cts 1.93 kB │ gzip: 0.57 kB
@@ -70,28 +70,28 @@
70
70
  ℹ [CJS] dist/testing/StorageIntegrationTestSuite.d.cts 1.43 kB │ gzip: 0.57 kB
71
71
  ℹ [CJS] dist/MimicClusterServerEngine.d.cts 0.82 kB │ gzip: 0.33 kB
72
72
  ℹ [CJS] dist/MimicServer.d.cts 0.74 kB │ gzip: 0.31 kB
73
- ℹ [CJS] 36 files, total: 58.13 kB
74
- ✔ Build complete in 7202ms
73
+ ℹ [CJS] 36 files, total: 58.86 kB
74
+ ✔ Build complete in 6780ms
75
75
  ℹ [ESM] dist/index.mjs  1.29 kB │ gzip: 0.34 kB
76
76
  ℹ [ESM] dist/testing/index.mjs  0.60 kB │ gzip: 0.19 kB
77
77
  ℹ [ESM] dist/testing/HotStorageTestSuite.mjs.map 67.12 kB │ gzip: 9.05 kB
78
78
  ℹ [ESM] dist/testing/HotStorageTestSuite.mjs 36.09 kB │ gzip: 5.26 kB
79
79
  ℹ [ESM] dist/testing/StorageIntegrationTestSuite.mjs.map 35.60 kB │ gzip: 6.44 kB
80
80
  ℹ [ESM] dist/testing/ColdStorageTestSuite.mjs.map 31.92 kB │ gzip: 5.21 kB
81
- ℹ [ESM] dist/MimicClusterServerEngine.mjs.map 28.95 kB │ gzip: 6.88 kB
82
- ℹ [ESM] dist/DocumentInstance.mjs.map 23.98 kB │ gzip: 6.20 kB
81
+ ℹ [ESM] dist/MimicClusterServerEngine.mjs.map 29.67 kB │ gzip: 6.99 kB
82
+ ℹ [ESM] dist/DocumentInstance.mjs.map 24.33 kB │ gzip: 6.31 kB
83
83
  ℹ [ESM] dist/MimicServer.mjs.map 21.35 kB │ gzip: 5.64 kB
84
- ℹ [ESM] dist/MimicServerEngine.mjs.map 19.25 kB │ gzip: 4.87 kB
84
+ ℹ [ESM] dist/MimicServerEngine.mjs.map 20.00 kB │ gzip: 5.01 kB
85
85
  ℹ [ESM] dist/testing/StorageIntegrationTestSuite.mjs 18.49 kB │ gzip: 3.63 kB
86
86
  ℹ [ESM] dist/testing/ColdStorageTestSuite.mjs 16.50 kB │ gzip: 3.21 kB
87
87
  ℹ [ESM] dist/testing/FailingStorage.mjs.map 15.60 kB │ gzip: 3.07 kB
88
- ℹ [ESM] dist/MimicClusterServerEngine.mjs 13.93 kB │ gzip: 3.55 kB
88
+ ℹ [ESM] dist/MimicClusterServerEngine.mjs 14.26 kB │ gzip: 3.58 kB
89
89
  ℹ [ESM] dist/HotStorage.mjs.map 11.89 kB │ gzip: 3.36 kB
90
- ℹ [ESM] dist/DocumentInstance.mjs 10.15 kB │ gzip: 2.76 kB
90
+ ℹ [ESM] dist/DocumentInstance.mjs 10.19 kB │ gzip: 2.77 kB
91
91
  ℹ [ESM] dist/MimicServer.mjs  9.90 kB │ gzip: 2.88 kB
92
92
  ℹ [ESM] dist/PresenceManager.mjs.map  9.38 kB │ gzip: 2.38 kB
93
93
  ℹ [ESM] dist/Protocol.mjs.map  9.18 kB │ gzip: 2.15 kB
94
- ℹ [ESM] dist/MimicServerEngine.mjs  7.82 kB │ gzip: 2.23 kB
94
+ ℹ [ESM] dist/MimicServerEngine.mjs  7.95 kB │ gzip: 2.23 kB
95
95
  ℹ [ESM] dist/testing/assertions.mjs.map  7.31 kB │ gzip: 2.01 kB
96
96
  ℹ [ESM] dist/testing/FailingStorage.mjs  6.49 kB │ gzip: 1.52 kB
97
97
  ℹ [ESM] dist/Metrics.mjs.map  6.35 kB │ gzip: 1.48 kB
@@ -109,8 +109,8 @@
109
109
  ℹ [ESM] dist/Errors.mjs  1.73 kB │ gzip: 0.61 kB
110
110
  ℹ [ESM] dist/Protocol.d.mts.map  1.57 kB │ gzip: 0.70 kB
111
111
  ℹ [ESM] dist/Types.d.mts.map  1.44 kB │ gzip: 0.66 kB
112
- ℹ [ESM] dist/DocumentInstance.d.mts.map  1.26 kB │ gzip: 0.57 kB
113
- ℹ [ESM] dist/MimicServerEngine.d.mts.map  0.97 kB │ gzip: 0.49 kB
112
+ ℹ [ESM] dist/DocumentInstance.d.mts.map  1.32 kB │ gzip: 0.59 kB
113
+ ℹ [ESM] dist/MimicServerEngine.d.mts.map  0.93 kB │ gzip: 0.46 kB
114
114
  ℹ [ESM] dist/_virtual/_@oxc-project_runtime@0.103.0/helpers/objectSpread2.mjs  0.90 kB │ gzip: 0.42 kB
115
115
  ℹ [ESM] dist/ColdStorage.d.mts.map  0.70 kB │ gzip: 0.37 kB
116
116
  ℹ [ESM] dist/MimicAuthService.d.mts.map  0.61 kB │ gzip: 0.35 kB
@@ -136,9 +136,9 @@
136
136
  ℹ [ESM] dist/Protocol.d.mts  5.84 kB │ gzip: 1.34 kB
137
137
  ℹ [ESM] dist/Types.d.mts  5.72 kB │ gzip: 1.61 kB
138
138
  ℹ [ESM] dist/Errors.d.mts  4.86 kB │ gzip: 1.02 kB
139
- ℹ [ESM] dist/MimicServerEngine.d.mts  3.60 kB │ gzip: 1.09 kB
139
+ ℹ [ESM] dist/MimicServerEngine.d.mts  4.03 kB │ gzip: 1.20 kB
140
+ ℹ [ESM] dist/DocumentInstance.d.mts  3.34 kB │ gzip: 1.10 kB
140
141
  ℹ [ESM] dist/HotStorage.d.mts  3.11 kB │ gzip: 1.23 kB
141
- ℹ [ESM] dist/DocumentInstance.d.mts  3.07 kB │ gzip: 1.00 kB
142
142
  ℹ [ESM] dist/testing/types.d.mts  2.76 kB │ gzip: 1.15 kB
143
143
  ℹ [ESM] dist/MimicAuthService.d.mts  2.70 kB │ gzip: 1.04 kB
144
144
  ℹ [ESM] dist/testing/HotStorageTestSuite.d.mts  1.93 kB │ gzip: 0.57 kB
@@ -150,5 +150,5 @@
150
150
  ℹ [ESM] dist/testing/StorageIntegrationTestSuite.d.mts  1.43 kB │ gzip: 0.57 kB
151
151
  ℹ [ESM] dist/MimicClusterServerEngine.d.mts  0.82 kB │ gzip: 0.33 kB
152
152
  ℹ [ESM] dist/MimicServer.d.mts  0.74 kB │ gzip: 0.31 kB
153
- ℹ [ESM] 78 files, total: 513.20 kB
154
- ✔ Build complete in 7224ms
153
+ ℹ [ESM] 78 files, total: 516.25 kB
154
+ ✔ Build complete in 6801ms
@@ -169,6 +169,7 @@ const make = (documentId, config, coldStorage, hotStorage) => effect.Effect.gen(
169
169
  touch,
170
170
  getVersion: () => document.getVersion(),
171
171
  getSnapshot: () => document.getSnapshot(),
172
+ toSnapshot: () => document.toSnapshot(),
172
173
  needsSnapshot,
173
174
  getLastActivityTime
174
175
  };
@@ -60,11 +60,17 @@ interface DocumentInstance<TSchema extends Primitive.AnyPrimitive> {
60
60
  readonly touch: () => Effect.Effect<void>;
61
61
  /** Get current document version */
62
62
  readonly getVersion: () => number;
63
- /** Get document snapshot */
63
+ /** Get document snapshot (flat state format) */
64
64
  readonly getSnapshot: () => {
65
65
  state: unknown;
66
66
  version: number;
67
67
  };
68
+ /**
69
+ * Get tree-like snapshot for rendering.
70
+ * The snapshot is a type-safe, readonly structure where trees
71
+ * are converted from flat state to nested/hierarchical structure.
72
+ */
73
+ readonly toSnapshot: () => Primitive.InferSnapshot<TSchema>;
68
74
  /** Check if document has unsnapshot transactions that need persisting */
69
75
  readonly needsSnapshot: () => Effect.Effect<boolean>;
70
76
  /** Get the last activity timestamp for idle detection */
@@ -1 +1 @@
1
- {"version":3,"file":"DocumentInstance.d.cts","names":[],"sources":["../src/DocumentInstance.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;AAkDA;AASiB,KA/BL,YAAA,GA+BqB;EAAiB,SAAU,OAAA,EAAA,IAAA;EAET,SAAA,OAAA,EAAA,MAAA;CAA9B,GAAA;EAEY,SAAA,OAAA,EAAA,KAAA;EAAd,SAAO,MAAA,EAAA,MAAA;CAEoB;;;;AAE2C,UAhCxE,sBAgCwE,CAAA,gBAhCjC,SAAA,CAAU,YAgCuB,CAAA,CAAA;EAAmB,SAAA,MAAA,EA/BzF,OA+ByF;EAA/C,SAAO,OAAA,CAAA,EA7B9D,SAAA,CAAU,aA6BoD,CA7BtC,OA6BsC,CAAA,GAAA,CAAA,CAAA,GAAA,EAAA;IAEjB,UAAA,EAAA,MAAA;EAAmB,CAAA,EAAA,GA9B9B,MAAA,CAAO,MA8BuB,CA9BhB,SAAA,CAAU,aA8BM,CA9BQ,OA8BR,CAAA,CAAA,CAAA;EAAvC,SAAO,qBAAA,EAAA,MAAA;EAEsB,SAAA,QAAA,EAAA;IAAmB,SAAA,QAAA,EA7BxD,QAAA,CAAS,QA6B+C;IAAhC,SAAA,oBAAA,EAAA,MAAA;EAEvB,CAAA;;;;AAucxB;AA9aqC,UAhDpB,gBAAA,CAgD8B;EAEd,SAAA,mBAAA,EAAA,MAAA;EAAvB,SAAA,gBAAA,EAAA,MAAA;EACK,SAAA,yBAAA,EAAA,MAAA;;;;;AAEgD,UA5C9C,gBA4C8C,CAAA,gBA5Cb,SAAA,CAAU,YA4CG,CAAA,CAAA;EAA5D;EAAa,SAAA,QAAA,EA1CK,cAAA,CAAe,cA0CpB,CA1CmC,OA0CnC,CAAA;;mBAxCG,MAAA,CAAO,OAAO;;gCAED,MAAA,CAAO,OAAO;;iCAEb,WAAA,CAAY,gBAAgB,MAAA,CAAO,OAAO,cAAc,mBAAmB;;+BAE7E,MAAA,CAAO,aAAa,mBAAmB;;wCAE9B,MAAA,CAAO,aAAa,mBAAmB;;wBAEvD,MAAA,CAAO;;;;;;;;;gCAMC,MAAA,CAAO;;sCAED,MAAA,CAAO;;cA+bhC;yBA9awB,SAAA,CAAU,0CAErC,uBAAuB,uBAClB,yBACD,eACX,MAAA,CAAO,OAAO,iBAAiB,UAAU,mBAAmB"}
1
+ {"version":3,"file":"DocumentInstance.d.cts","names":[],"sources":["../src/DocumentInstance.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;AAkDA;AASiB,KA/BL,YAAA,GA+BqB;EAAiB,SAAU,OAAA,EAAA,IAAA;EAET,SAAA,OAAA,EAAA,MAAA;CAA9B,GAAA;EAEY,SAAA,OAAA,EAAA,KAAA;EAAd,SAAO,MAAA,EAAA,MAAA;CAEoB;;;;AAE2C,UAhCxE,sBAgCwE,CAAA,gBAhCjC,SAAA,CAAU,YAgCuB,CAAA,CAAA;EAAmB,SAAA,MAAA,EA/BzF,OA+ByF;EAA/C,SAAO,OAAA,CAAA,EA7B9D,SAAA,CAAU,aA6BoD,CA7BtC,OA6BsC,CAAA,GAAA,CAAA,CAAA,GAAA,EAAA;IAEjB,UAAA,EAAA,MAAA;EAAmB,CAAA,EAAA,GA9B9B,MAAA,CAAO,MA8BuB,CA9BhB,SAAA,CAAU,aA8BM,CA9BQ,OA8BR,CAAA,CAAA,CAAA;EAAvC,SAAO,qBAAA,EAAA,MAAA;EAEsB,SAAA,QAAA,EAAA;IAAmB,SAAA,QAAA,EA7BxD,QAAA,CAAS,QA6B+C;IAAhC,SAAA,oBAAA,EAAA,MAAA;EAEvB,CAAA;;;;;AAc2B,UArClC,gBAAA,CAqCkC;EAgctC,SAAA,mBAEZ,EAAA,MAAA;EAjboC,SAAU,gBAAA,EAAA,MAAA;EAEd,SAAA,yBAAA,EAAA,MAAA;;;;;AAGhB,UAlDA,gBAkDA,CAAA,gBAlDiC,SAAA,CAAU,YAkD3C,CAAA,CAAA;EAA2B;EAAmB,SAAA,QAAA,EAhD1C,cAAA,CAAe,cAgD2B,CAhDZ,OAgDY,CAAA;EAA5D;EAAa,SAAA,MAAA,EA9CG,MAAA,CAAO,MA8CV,CA9CiB,eA8CjB,CAAA;;gCA5CgB,MAAA,CAAO,OAAO;;iCAEb,WAAA,CAAY,gBAAgB,MAAA,CAAO,OAAO,cAAc,mBAAmB;;+BAE7E,MAAA,CAAO,aAAa,mBAAmB;;wCAE9B,MAAA,CAAO,aAAa,mBAAmB;;wBAEvD,MAAA,CAAO;;;;;;;;;;;;;6BAUF,SAAA,CAAU,cAAc;;gCAErB,MAAA,CAAO;;sCAED,MAAA,CAAO;;cAgchC;yBA/awB,SAAA,CAAU,0CAErC,uBAAuB,uBAClB,yBACD,eACX,MAAA,CAAO,OAAO,iBAAiB,UAAU,mBAAmB"}
@@ -60,11 +60,17 @@ interface DocumentInstance<TSchema extends Primitive.AnyPrimitive> {
60
60
  readonly touch: () => Effect.Effect<void>;
61
61
  /** Get current document version */
62
62
  readonly getVersion: () => number;
63
- /** Get document snapshot */
63
+ /** Get document snapshot (flat state format) */
64
64
  readonly getSnapshot: () => {
65
65
  state: unknown;
66
66
  version: number;
67
67
  };
68
+ /**
69
+ * Get tree-like snapshot for rendering.
70
+ * The snapshot is a type-safe, readonly structure where trees
71
+ * are converted from flat state to nested/hierarchical structure.
72
+ */
73
+ readonly toSnapshot: () => Primitive.InferSnapshot<TSchema>;
68
74
  /** Check if document has unsnapshot transactions that need persisting */
69
75
  readonly needsSnapshot: () => Effect.Effect<boolean>;
70
76
  /** Get the last activity timestamp for idle detection */
@@ -1 +1 @@
1
- {"version":3,"file":"DocumentInstance.d.mts","names":[],"sources":["../src/DocumentInstance.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;AAkDA;AASiB,KA/BL,YAAA,GA+BqB;EAAiB,SAAU,OAAA,EAAA,IAAA;EAET,SAAA,OAAA,EAAA,MAAA;CAA9B,GAAA;EAEY,SAAA,OAAA,EAAA,KAAA;EAAd,SAAO,MAAA,EAAA,MAAA;CAEoB;;;;AAE2C,UAhCxE,sBAgCwE,CAAA,gBAhCjC,SAAA,CAAU,YAgCuB,CAAA,CAAA;EAAmB,SAAA,MAAA,EA/BzF,OA+ByF;EAA/C,SAAO,OAAA,CAAA,EA7B9D,SAAA,CAAU,aA6BoD,CA7BtC,OA6BsC,CAAA,GAAA,CAAA,CAAA,GAAA,EAAA;IAEjB,UAAA,EAAA,MAAA;EAAmB,CAAA,EAAA,GA9B9B,MAAA,CAAO,MA8BuB,CA9BhB,SAAA,CAAU,aA8BM,CA9BQ,OA8BR,CAAA,CAAA,CAAA;EAAvC,SAAO,qBAAA,EAAA,MAAA;EAEsB,SAAA,QAAA,EAAA;IAAmB,SAAA,QAAA,EA7BxD,QAAA,CAAS,QA6B+C;IAAhC,SAAA,oBAAA,EAAA,MAAA;EAEvB,CAAA;;;;AAucxB;AA9aqC,UAhDpB,gBAAA,CAgD8B;EAEd,SAAA,mBAAA,EAAA,MAAA;EAAvB,SAAA,gBAAA,EAAA,MAAA;EACK,SAAA,yBAAA,EAAA,MAAA;;;;;AAEgD,UA5C9C,gBA4C8C,CAAA,gBA5Cb,SAAA,CAAU,YA4CG,CAAA,CAAA;EAA5D;EAAa,SAAA,QAAA,EA1CK,cAAA,CAAe,cA0CpB,CA1CmC,OA0CnC,CAAA;;mBAxCG,MAAA,CAAO,OAAO;;gCAED,MAAA,CAAO,OAAO;;iCAEb,WAAA,CAAY,gBAAgB,MAAA,CAAO,OAAO,cAAc,mBAAmB;;+BAE7E,MAAA,CAAO,aAAa,mBAAmB;;wCAE9B,MAAA,CAAO,aAAa,mBAAmB;;wBAEvD,MAAA,CAAO;;;;;;;;;gCAMC,MAAA,CAAO;;sCAED,MAAA,CAAO;;cA+bhC;yBA9awB,SAAA,CAAU,0CAErC,uBAAuB,uBAClB,yBACD,eACX,MAAA,CAAO,OAAO,iBAAiB,UAAU,mBAAmB"}
1
+ {"version":3,"file":"DocumentInstance.d.mts","names":[],"sources":["../src/DocumentInstance.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;AAkDA;AASiB,KA/BL,YAAA,GA+BqB;EAAiB,SAAU,OAAA,EAAA,IAAA;EAET,SAAA,OAAA,EAAA,MAAA;CAA9B,GAAA;EAEY,SAAA,OAAA,EAAA,KAAA;EAAd,SAAO,MAAA,EAAA,MAAA;CAEoB;;;;AAE2C,UAhCxE,sBAgCwE,CAAA,gBAhCjC,SAAA,CAAU,YAgCuB,CAAA,CAAA;EAAmB,SAAA,MAAA,EA/BzF,OA+ByF;EAA/C,SAAO,OAAA,CAAA,EA7B9D,SAAA,CAAU,aA6BoD,CA7BtC,OA6BsC,CAAA,GAAA,CAAA,CAAA,GAAA,EAAA;IAEjB,UAAA,EAAA,MAAA;EAAmB,CAAA,EAAA,GA9B9B,MAAA,CAAO,MA8BuB,CA9BhB,SAAA,CAAU,aA8BM,CA9BQ,OA8BR,CAAA,CAAA,CAAA;EAAvC,SAAO,qBAAA,EAAA,MAAA;EAEsB,SAAA,QAAA,EAAA;IAAmB,SAAA,QAAA,EA7BxD,QAAA,CAAS,QA6B+C;IAAhC,SAAA,oBAAA,EAAA,MAAA;EAEvB,CAAA;;;;;AAc2B,UArClC,gBAAA,CAqCkC;EAgctC,SAAA,mBAEZ,EAAA,MAAA;EAjboC,SAAU,gBAAA,EAAA,MAAA;EAEd,SAAA,yBAAA,EAAA,MAAA;;;;;AAGhB,UAlDA,gBAkDA,CAAA,gBAlDiC,SAAA,CAAU,YAkD3C,CAAA,CAAA;EAA2B;EAAmB,SAAA,QAAA,EAhD1C,cAAA,CAAe,cAgD2B,CAhDZ,OAgDY,CAAA;EAA5D;EAAa,SAAA,MAAA,EA9CG,MAAA,CAAO,MA8CV,CA9CiB,eA8CjB,CAAA;;gCA5CgB,MAAA,CAAO,OAAO;;iCAEb,WAAA,CAAY,gBAAgB,MAAA,CAAO,OAAO,cAAc,mBAAmB;;+BAE7E,MAAA,CAAO,aAAa,mBAAmB;;wCAE9B,MAAA,CAAO,aAAa,mBAAmB;;wBAEvD,MAAA,CAAO;;;;;;;;;;;;;6BAUF,SAAA,CAAU,cAAc;;gCAErB,MAAA,CAAO;;sCAED,MAAA,CAAO;;cAgchC;yBA/awB,SAAA,CAAU,0CAErC,uBAAuB,uBAClB,yBACD,eACX,MAAA,CAAO,OAAO,iBAAiB,UAAU,mBAAmB"}
@@ -169,6 +169,7 @@ const make = (documentId, config, coldStorage, hotStorage) => Effect.gen(functio
169
169
  touch,
170
170
  getVersion: () => document.getVersion(),
171
171
  getSnapshot: () => document.getSnapshot(),
172
+ toSnapshot: () => document.toSnapshot(),
172
173
  needsSnapshot,
173
174
  getLastActivityTime
174
175
  };
@@ -1 +1 @@
1
- {"version":3,"file":"DocumentInstance.mjs","names":["initialState: Primitive.InferState<TSchema> | undefined","initial: Primitive.InferSetInput<TSchema> | undefined","Metrics.documentsRestored","Metrics.documentsCreated","Metrics.documentsActive","walEntries","storedDoc","Metrics.storageSnapshots","Metrics.storageSnapshotLatency","Metrics.transactionsRejected","latency","Metrics.transactionsLatency","walEntry: WalEntry","Metrics.walAppendFailures","Metrics.transactionsProcessed","Metrics.storageWalAppends","Metrics.storageVersionGaps","snapshotState: Primitive.InferState<TSchema> | undefined"],"sources":["../src/DocumentInstance.ts"],"sourcesContent":["/**\n * @voidhash/mimic-effect - DocumentInstance\n *\n * Manages the lifecycle of a single document including:\n * - Restoration from storage (cold storage + WAL replay)\n * - Transaction submission with WAL persistence\n * - Snapshot saving and trigger checking\n *\n * Used by both MimicServerEngine (single-node) and MimicClusterServerEngine (clustered).\n */\nimport { Duration, Effect, Metric, PubSub, Ref } from \"effect\";\nimport { Document, type Primitive, type Transaction } from \"@voidhash/mimic\";\nimport { ServerDocument } from \"@voidhash/mimic/server\";\nimport type { StoredDocument, WalEntry } from \"./Types\";\nimport type { ServerBroadcast } from \"./Protocol\";\nimport type { ColdStorage } from \"./ColdStorage\";\nimport type { HotStorage } from \"./HotStorage\";\nimport type { ColdStorageError } from \"./Errors\";\nimport type { HotStorageError } from \"./Errors\";\nimport * as Metrics from \"./Metrics\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/**\n * Result of submitting a transaction\n */\nexport type SubmitResult =\n | { readonly success: true; readonly version: number }\n | { readonly success: false; readonly reason: string };\n\n/**\n * Configuration for a DocumentInstance\n */\nexport interface DocumentInstanceConfig<TSchema extends Primitive.AnyPrimitive> {\n readonly schema: TSchema;\n readonly initial?:\n | Primitive.InferSetInput<TSchema>\n | ((ctx: { documentId: string }) => Effect.Effect<Primitive.InferSetInput<TSchema>>);\n readonly maxTransactionHistory: number;\n readonly snapshot: {\n readonly interval: Duration.Duration;\n readonly transactionThreshold: number;\n };\n}\n\n/**\n * Snapshot tracking state\n */\nexport interface SnapshotTracking {\n readonly lastSnapshotVersion: number;\n readonly lastSnapshotTime: number;\n readonly transactionsSinceSnapshot: number;\n}\n\n/**\n * A DocumentInstance manages a single document's lifecycle\n */\nexport interface DocumentInstance<TSchema extends Primitive.AnyPrimitive> {\n /** The underlying ServerDocument */\n readonly document: ServerDocument.ServerDocument<TSchema>;\n /** PubSub for broadcasting messages to subscribers */\n readonly pubsub: PubSub.PubSub<ServerBroadcast>;\n /** Current snapshot tracking state */\n readonly getSnapshotTracking: Effect.Effect<SnapshotTracking>;\n /** Submit a transaction */\n readonly submit: (transaction: Transaction.Transaction) => Effect.Effect<SubmitResult, ColdStorageError | HotStorageError>;\n /** Save a snapshot to cold storage */\n readonly saveSnapshot: () => Effect.Effect<void, ColdStorageError | HotStorageError>;\n /** Check if snapshot should be triggered and save if needed */\n readonly checkSnapshotTriggers: () => Effect.Effect<void, ColdStorageError | HotStorageError>;\n /** Update last activity time (for external tracking) */\n readonly touch: () => Effect.Effect<void>;\n /** Get current document version */\n readonly getVersion: () => number;\n /** Get document snapshot */\n readonly getSnapshot: () => { state: unknown; version: number };\n /** Check if document has unsnapshot transactions that need persisting */\n readonly needsSnapshot: () => Effect.Effect<boolean>;\n /** Get the last activity timestamp for idle detection */\n readonly getLastActivityTime: () => Effect.Effect<number>;\n}\n\n// =============================================================================\n// Factory\n// =============================================================================\n\n/**\n * Create a DocumentInstance for a single document.\n *\n * This handles:\n * - Loading from cold storage or computing initial state\n * - Persisting initial state immediately (crash safety)\n * - Replaying WAL entries\n * - Transaction submission with WAL persistence\n * - Snapshot saving\n */\nexport const make = <TSchema extends Primitive.AnyPrimitive>(\n documentId: string,\n config: DocumentInstanceConfig<TSchema>,\n coldStorage: ColdStorage,\n hotStorage: HotStorage\n): Effect.Effect<DocumentInstance<TSchema>, ColdStorageError | HotStorageError> =>\n Effect.gen(function* () {\n // Current schema version (hard-coded to 1 for now)\n const SCHEMA_VERSION = 1;\n\n // 1. Load snapshot from ColdStorage\n const storedDoc = yield* coldStorage.load(documentId);\n\n // Track initial values - only one will be set:\n // - initialState: raw state from storage (already in internal format)\n // - initial: computed from config (needs conversion to state format)\n let initialState: Primitive.InferState<TSchema> | undefined;\n let initial: Primitive.InferSetInput<TSchema> | undefined;\n let initialVersion = 0;\n\n if (storedDoc) {\n // Loading from storage - state is already in internal format\n initialState = storedDoc.state as Primitive.InferState<TSchema>;\n initialVersion = storedDoc.version;\n } else {\n // New document - compute initial value (set input format)\n initial = yield* computeInitialState(config, documentId);\n }\n\n // 2. Create PubSub for broadcasting\n const pubsub = yield* PubSub.unbounded<ServerBroadcast>();\n\n // 3. Create refs for tracking\n const lastSnapshotVersionRef = yield* Ref.make(initialVersion);\n const lastSnapshotTimeRef = yield* Ref.make(Date.now());\n const transactionsSinceSnapshotRef = yield* Ref.make(0);\n const lastActivityTimeRef = yield* Ref.make(Date.now());\n\n // 4. Create ServerDocument with callbacks\n const document = ServerDocument.make({\n schema: config.schema,\n initial,\n initialState,\n initialVersion,\n maxTransactionHistory: config.maxTransactionHistory,\n onBroadcast: (message: ServerDocument.TransactionMessage) => {\n Effect.runSync(\n PubSub.publish(pubsub, {\n type: \"transaction\",\n transaction: message.transaction,\n version: message.version,\n })\n );\n },\n onRejection: (transactionId: string, reason: string) => {\n Effect.runSync(\n PubSub.publish(pubsub, {\n type: \"error\",\n transactionId,\n reason,\n })\n );\n },\n });\n\n // 5. If this is a new document, immediately save to cold storage\n // This ensures the initial state is durable before any transactions are accepted.\n if (!storedDoc) {\n const initialStoredDoc = createStoredDocument(document.get(), 0, SCHEMA_VERSION);\n yield* coldStorage.save(documentId, initialStoredDoc);\n yield* Effect.logDebug(\"Initial state persisted to cold storage\", { documentId });\n }\n\n // 6. Load WAL entries\n const walEntries = yield* hotStorage.getEntries(documentId, initialVersion);\n\n // 7. Verify WAL continuity (warning only, non-blocking)\n yield* verifyWalContinuity(documentId, walEntries, initialVersion);\n\n // 8. Replay WAL entries\n yield* replayWalEntries(documentId, document, walEntries);\n\n // Track metrics\n if (storedDoc) {\n yield* Metric.increment(Metrics.documentsRestored);\n } else {\n yield* Metric.increment(Metrics.documentsCreated);\n }\n yield* Metric.incrementBy(Metrics.documentsActive, 1);\n\n // ==========================================================================\n // Instance Methods\n // ==========================================================================\n\n const getSnapshotTracking = Effect.gen(function* () {\n return {\n lastSnapshotVersion: yield* Ref.get(lastSnapshotVersionRef),\n lastSnapshotTime: yield* Ref.get(lastSnapshotTimeRef),\n transactionsSinceSnapshot: yield* Ref.get(transactionsSinceSnapshotRef),\n };\n });\n\n const saveSnapshot = Effect.fn(\"document.snapshot.save\")(function* () {\n const targetVersion = document.getVersion();\n const lastSnapshotVersion = yield* Ref.get(lastSnapshotVersionRef);\n\n // Idempotency check: skip if already snapshotted at this version\n if (targetVersion <= lastSnapshotVersion) {\n return;\n }\n\n const snapshotStartTime = Date.now();\n\n // Load base snapshot from cold storage\n const baseSnapshot = yield* coldStorage.load(documentId);\n const baseVersion = baseSnapshot?.version ?? 0;\n const baseState = baseSnapshot?.state as Primitive.InferState<TSchema> | undefined;\n\n // Load WAL entries from base to target\n const walEntries = yield* hotStorage.getEntries(documentId, baseVersion);\n\n // Compute snapshot state by replaying WAL on base\n const snapshotResult = computeSnapshotState(\n config.schema,\n baseState,\n walEntries,\n targetVersion\n );\n\n if (!snapshotResult) {\n return;\n }\n\n // Re-check before saving (in case another snapshot completed while we were working)\n const currentLastSnapshot = yield* Ref.get(lastSnapshotVersionRef);\n if (snapshotResult.version <= currentLastSnapshot) {\n return;\n }\n\n const storedDoc = createStoredDocument(\n snapshotResult.state,\n snapshotResult.version,\n SCHEMA_VERSION\n );\n\n // Save to ColdStorage\n yield* coldStorage.save(documentId, storedDoc);\n\n // Track snapshot metrics\n const snapshotDuration = Date.now() - snapshotStartTime;\n yield* Metric.increment(Metrics.storageSnapshots);\n yield* Metric.update(Metrics.storageSnapshotLatency, snapshotDuration);\n\n // Update tracking BEFORE truncate (for idempotency on retry)\n yield* Ref.set(lastSnapshotVersionRef, snapshotResult.version);\n yield* Ref.set(lastSnapshotTimeRef, Date.now());\n yield* Ref.set(transactionsSinceSnapshotRef, 0);\n\n // Truncate WAL - non-fatal, will be retried on next snapshot\n yield* Effect.catchAll(hotStorage.truncate(documentId, snapshotResult.version), (e) =>\n Effect.logWarning(\"WAL truncate failed - will retry on next snapshot\", {\n documentId,\n version: snapshotResult.version,\n error: e,\n })\n );\n });\n\n const checkSnapshotTriggers = Effect.fn(\"document.snapshot.check-triggers\")(function* () {\n const txCount = yield* Ref.get(transactionsSinceSnapshotRef);\n const lastTime = yield* Ref.get(lastSnapshotTimeRef);\n\n if (shouldTriggerSnapshot(txCount, lastTime, config.snapshot)) {\n yield* saveSnapshot();\n }\n });\n\n const submit = Effect.fn(\"document.transaction.submit\")(function* (\n transaction: Transaction.Transaction\n ) {\n const submitStartTime = Date.now();\n\n // Update activity time\n yield* Ref.set(lastActivityTimeRef, Date.now());\n\n // Phase 1: Validate (no side effects)\n const validation = document.validate(transaction);\n\n if (!validation.valid) {\n yield* Metric.increment(Metrics.transactionsRejected);\n const latency = Date.now() - submitStartTime;\n yield* Metric.update(Metrics.transactionsLatency, latency);\n\n return {\n success: false as const,\n reason: validation.reason,\n };\n }\n\n // Phase 2: Append to WAL with gap check (BEFORE state mutation)\n const walEntry: WalEntry = {\n transaction,\n version: validation.nextVersion,\n timestamp: Date.now(),\n };\n\n // Get the current snapshot version to pass as baseVersion for gap checking\n // This ensures correct validation after truncation or restart\n const snapshotVersion = yield* Ref.get(lastSnapshotVersionRef);\n\n const appendResult = yield* Effect.either(\n hotStorage.appendWithCheck(documentId, walEntry, validation.nextVersion, snapshotVersion)\n );\n\n if (appendResult._tag === \"Left\") {\n yield* Effect.logError(\"WAL append failed\", {\n documentId,\n version: validation.nextVersion,\n error: appendResult.left,\n });\n yield* Metric.increment(Metrics.walAppendFailures);\n\n const latency = Date.now() - submitStartTime;\n yield* Metric.update(Metrics.transactionsLatency, latency);\n\n return {\n success: false as const,\n reason: \"Storage unavailable. Please retry.\",\n };\n }\n\n // Phase 3: Apply (state mutation + broadcast)\n document.apply(transaction);\n\n // Track metrics\n const latency = Date.now() - submitStartTime;\n yield* Metric.update(Metrics.transactionsLatency, latency);\n yield* Metric.increment(Metrics.transactionsProcessed);\n yield* Metric.increment(Metrics.storageWalAppends);\n\n // Increment transaction count\n yield* Ref.update(transactionsSinceSnapshotRef, (n) => n + 1);\n\n // Check snapshot triggers\n yield* checkSnapshotTriggers();\n\n return {\n success: true as const,\n version: validation.nextVersion,\n };\n });\n\n const touch = Effect.fn(\"document.touch\")(function* () {\n yield* Ref.set(lastActivityTimeRef, Date.now());\n });\n\n const needsSnapshot = () =>\n Effect.map(Ref.get(transactionsSinceSnapshotRef), (n) => n > 0);\n\n const getLastActivityTime = () => Ref.get(lastActivityTimeRef);\n\n return {\n document,\n pubsub,\n getSnapshotTracking,\n submit,\n saveSnapshot,\n checkSnapshotTriggers,\n touch,\n getVersion: () => document.getVersion(),\n getSnapshot: () => document.getSnapshot(),\n needsSnapshot,\n getLastActivityTime,\n };\n });\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\n/**\n * Compute initial state for a new document.\n */\nconst computeInitialState = <TSchema extends Primitive.AnyPrimitive>(\n config: DocumentInstanceConfig<TSchema>,\n documentId: string\n): Effect.Effect<Primitive.InferSetInput<TSchema> | undefined> => {\n if (config.initial === undefined) {\n return Effect.succeed(undefined);\n }\n\n if (typeof config.initial === \"function\") {\n return (config.initial as (ctx: { documentId: string }) => Effect.Effect<Primitive.InferSetInput<TSchema>>)({\n documentId,\n });\n }\n\n return Effect.succeed(config.initial as Primitive.InferSetInput<TSchema>);\n};\n\n/**\n * Verify WAL continuity and log warnings for any gaps.\n */\nconst verifyWalContinuity = Effect.fn(\"document.wal.verify\")(function* (\n documentId: string,\n walEntries: readonly WalEntry[],\n baseVersion: number\n) {\n if (walEntries.length === 0) {\n return;\n }\n\n const firstWalVersion = walEntries[0]!.version;\n const expectedFirst = baseVersion + 1;\n\n if (firstWalVersion !== expectedFirst) {\n yield* Effect.logWarning(\"WAL version gap detected\", {\n documentId,\n snapshotVersion: baseVersion,\n firstWalVersion,\n expectedFirst,\n });\n yield* Metric.increment(Metrics.storageVersionGaps);\n }\n\n for (let i = 1; i < walEntries.length; i++) {\n const prev = walEntries[i - 1]!.version;\n const curr = walEntries[i]!.version;\n if (curr !== prev + 1) {\n yield* Effect.logWarning(\"WAL internal gap detected\", {\n documentId,\n previousVersion: prev,\n currentVersion: curr,\n });\n }\n }\n});\n\n/**\n * Replay WAL entries onto a ServerDocument.\n */\nconst replayWalEntries = Effect.fn(\"document.wal.replay\")(function* (\n documentId: string,\n document: ServerDocument.ServerDocument<Primitive.AnyPrimitive>,\n walEntries: readonly WalEntry[]\n) {\n for (const entry of walEntries) {\n const result = document.submit(entry.transaction);\n if (!result.success) {\n yield* Effect.logWarning(\"Skipping corrupted WAL entry\", {\n documentId,\n version: entry.version,\n reason: result.reason,\n });\n }\n }\n});\n\n/**\n * Compute snapshot state by replaying WAL entries on a base state.\n */\nconst computeSnapshotState = <TSchema extends Primitive.AnyPrimitive>(\n schema: TSchema,\n baseState: Primitive.InferState<TSchema> | undefined,\n walEntries: readonly WalEntry[],\n targetVersion: number\n): { state: Primitive.InferState<TSchema>; version: number } | undefined => {\n const relevantEntries = walEntries.filter((e) => e.version <= targetVersion);\n\n if (relevantEntries.length === 0 && baseState === undefined) {\n return undefined;\n }\n\n let snapshotState: Primitive.InferState<TSchema> | undefined = baseState;\n for (const entry of relevantEntries) {\n const tempDoc = Document.make(schema, { initialState: snapshotState });\n tempDoc.apply(entry.transaction.ops);\n snapshotState = tempDoc.get();\n }\n\n if (snapshotState === undefined) {\n return undefined;\n }\n\n const snapshotVersion =\n relevantEntries.length > 0 ? relevantEntries[relevantEntries.length - 1]!.version : 0;\n\n return { state: snapshotState, version: snapshotVersion };\n};\n\n/**\n * Check if a snapshot should be triggered.\n */\nconst shouldTriggerSnapshot = (\n transactionsSinceSnapshot: number,\n lastSnapshotTime: number,\n config: { interval: Duration.Duration; transactionThreshold: number }\n): boolean => {\n const now = Date.now();\n const intervalMs = Duration.toMillis(config.interval);\n\n if (transactionsSinceSnapshot >= config.transactionThreshold) {\n return true;\n }\n\n if (now - lastSnapshotTime >= intervalMs) {\n return true;\n }\n\n return false;\n};\n\n/**\n * Create a StoredDocument for persistence.\n */\nconst createStoredDocument = (\n state: unknown,\n version: number,\n schemaVersion: number\n): StoredDocument => ({\n state,\n version,\n schemaVersion,\n savedAt: Date.now(),\n});\n\n// =============================================================================\n// Re-export namespace\n// =============================================================================\n\nexport const DocumentInstance = {\n make,\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"DocumentInstance.mjs","names":["initialState: Primitive.InferState<TSchema> | undefined","initial: Primitive.InferSetInput<TSchema> | undefined","Metrics.documentsRestored","Metrics.documentsCreated","Metrics.documentsActive","walEntries","storedDoc","Metrics.storageSnapshots","Metrics.storageSnapshotLatency","Metrics.transactionsRejected","latency","Metrics.transactionsLatency","walEntry: WalEntry","Metrics.walAppendFailures","Metrics.transactionsProcessed","Metrics.storageWalAppends","Metrics.storageVersionGaps","snapshotState: Primitive.InferState<TSchema> | undefined"],"sources":["../src/DocumentInstance.ts"],"sourcesContent":["/**\n * @voidhash/mimic-effect - DocumentInstance\n *\n * Manages the lifecycle of a single document including:\n * - Restoration from storage (cold storage + WAL replay)\n * - Transaction submission with WAL persistence\n * - Snapshot saving and trigger checking\n *\n * Used by both MimicServerEngine (single-node) and MimicClusterServerEngine (clustered).\n */\nimport { Duration, Effect, Metric, PubSub, Ref } from \"effect\";\nimport { Document, type Primitive, type Transaction } from \"@voidhash/mimic\";\nimport { ServerDocument } from \"@voidhash/mimic/server\";\nimport type { StoredDocument, WalEntry } from \"./Types\";\nimport type { ServerBroadcast } from \"./Protocol\";\nimport type { ColdStorage } from \"./ColdStorage\";\nimport type { HotStorage } from \"./HotStorage\";\nimport type { ColdStorageError } from \"./Errors\";\nimport type { HotStorageError } from \"./Errors\";\nimport * as Metrics from \"./Metrics\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/**\n * Result of submitting a transaction\n */\nexport type SubmitResult =\n | { readonly success: true; readonly version: number }\n | { readonly success: false; readonly reason: string };\n\n/**\n * Configuration for a DocumentInstance\n */\nexport interface DocumentInstanceConfig<TSchema extends Primitive.AnyPrimitive> {\n readonly schema: TSchema;\n readonly initial?:\n | Primitive.InferSetInput<TSchema>\n | ((ctx: { documentId: string }) => Effect.Effect<Primitive.InferSetInput<TSchema>>);\n readonly maxTransactionHistory: number;\n readonly snapshot: {\n readonly interval: Duration.Duration;\n readonly transactionThreshold: number;\n };\n}\n\n/**\n * Snapshot tracking state\n */\nexport interface SnapshotTracking {\n readonly lastSnapshotVersion: number;\n readonly lastSnapshotTime: number;\n readonly transactionsSinceSnapshot: number;\n}\n\n/**\n * A DocumentInstance manages a single document's lifecycle\n */\nexport interface DocumentInstance<TSchema extends Primitive.AnyPrimitive> {\n /** The underlying ServerDocument */\n readonly document: ServerDocument.ServerDocument<TSchema>;\n /** PubSub for broadcasting messages to subscribers */\n readonly pubsub: PubSub.PubSub<ServerBroadcast>;\n /** Current snapshot tracking state */\n readonly getSnapshotTracking: Effect.Effect<SnapshotTracking>;\n /** Submit a transaction */\n readonly submit: (transaction: Transaction.Transaction) => Effect.Effect<SubmitResult, ColdStorageError | HotStorageError>;\n /** Save a snapshot to cold storage */\n readonly saveSnapshot: () => Effect.Effect<void, ColdStorageError | HotStorageError>;\n /** Check if snapshot should be triggered and save if needed */\n readonly checkSnapshotTriggers: () => Effect.Effect<void, ColdStorageError | HotStorageError>;\n /** Update last activity time (for external tracking) */\n readonly touch: () => Effect.Effect<void>;\n /** Get current document version */\n readonly getVersion: () => number;\n /** Get document snapshot (flat state format) */\n readonly getSnapshot: () => { state: unknown; version: number };\n /**\n * Get tree-like snapshot for rendering.\n * The snapshot is a type-safe, readonly structure where trees\n * are converted from flat state to nested/hierarchical structure.\n */\n readonly toSnapshot: () => Primitive.InferSnapshot<TSchema>;\n /** Check if document has unsnapshot transactions that need persisting */\n readonly needsSnapshot: () => Effect.Effect<boolean>;\n /** Get the last activity timestamp for idle detection */\n readonly getLastActivityTime: () => Effect.Effect<number>;\n}\n\n// =============================================================================\n// Factory\n// =============================================================================\n\n/**\n * Create a DocumentInstance for a single document.\n *\n * This handles:\n * - Loading from cold storage or computing initial state\n * - Persisting initial state immediately (crash safety)\n * - Replaying WAL entries\n * - Transaction submission with WAL persistence\n * - Snapshot saving\n */\nexport const make = <TSchema extends Primitive.AnyPrimitive>(\n documentId: string,\n config: DocumentInstanceConfig<TSchema>,\n coldStorage: ColdStorage,\n hotStorage: HotStorage\n): Effect.Effect<DocumentInstance<TSchema>, ColdStorageError | HotStorageError> =>\n Effect.gen(function* () {\n // Current schema version (hard-coded to 1 for now)\n const SCHEMA_VERSION = 1;\n\n // 1. Load snapshot from ColdStorage\n const storedDoc = yield* coldStorage.load(documentId);\n\n // Track initial values - only one will be set:\n // - initialState: raw state from storage (already in internal format)\n // - initial: computed from config (needs conversion to state format)\n let initialState: Primitive.InferState<TSchema> | undefined;\n let initial: Primitive.InferSetInput<TSchema> | undefined;\n let initialVersion = 0;\n\n if (storedDoc) {\n // Loading from storage - state is already in internal format\n initialState = storedDoc.state as Primitive.InferState<TSchema>;\n initialVersion = storedDoc.version;\n } else {\n // New document - compute initial value (set input format)\n initial = yield* computeInitialState(config, documentId);\n }\n\n // 2. Create PubSub for broadcasting\n const pubsub = yield* PubSub.unbounded<ServerBroadcast>();\n\n // 3. Create refs for tracking\n const lastSnapshotVersionRef = yield* Ref.make(initialVersion);\n const lastSnapshotTimeRef = yield* Ref.make(Date.now());\n const transactionsSinceSnapshotRef = yield* Ref.make(0);\n const lastActivityTimeRef = yield* Ref.make(Date.now());\n\n // 4. Create ServerDocument with callbacks\n const document = ServerDocument.make({\n schema: config.schema,\n initial,\n initialState,\n initialVersion,\n maxTransactionHistory: config.maxTransactionHistory,\n onBroadcast: (message: ServerDocument.TransactionMessage) => {\n Effect.runSync(\n PubSub.publish(pubsub, {\n type: \"transaction\",\n transaction: message.transaction,\n version: message.version,\n })\n );\n },\n onRejection: (transactionId: string, reason: string) => {\n Effect.runSync(\n PubSub.publish(pubsub, {\n type: \"error\",\n transactionId,\n reason,\n })\n );\n },\n });\n\n // 5. If this is a new document, immediately save to cold storage\n // This ensures the initial state is durable before any transactions are accepted.\n if (!storedDoc) {\n const initialStoredDoc = createStoredDocument(document.get(), 0, SCHEMA_VERSION);\n yield* coldStorage.save(documentId, initialStoredDoc);\n yield* Effect.logDebug(\"Initial state persisted to cold storage\", { documentId });\n }\n\n // 6. Load WAL entries\n const walEntries = yield* hotStorage.getEntries(documentId, initialVersion);\n\n // 7. Verify WAL continuity (warning only, non-blocking)\n yield* verifyWalContinuity(documentId, walEntries, initialVersion);\n\n // 8. Replay WAL entries\n yield* replayWalEntries(documentId, document, walEntries);\n\n // Track metrics\n if (storedDoc) {\n yield* Metric.increment(Metrics.documentsRestored);\n } else {\n yield* Metric.increment(Metrics.documentsCreated);\n }\n yield* Metric.incrementBy(Metrics.documentsActive, 1);\n\n // ==========================================================================\n // Instance Methods\n // ==========================================================================\n\n const getSnapshotTracking = Effect.gen(function* () {\n return {\n lastSnapshotVersion: yield* Ref.get(lastSnapshotVersionRef),\n lastSnapshotTime: yield* Ref.get(lastSnapshotTimeRef),\n transactionsSinceSnapshot: yield* Ref.get(transactionsSinceSnapshotRef),\n };\n });\n\n const saveSnapshot = Effect.fn(\"document.snapshot.save\")(function* () {\n const targetVersion = document.getVersion();\n const lastSnapshotVersion = yield* Ref.get(lastSnapshotVersionRef);\n\n // Idempotency check: skip if already snapshotted at this version\n if (targetVersion <= lastSnapshotVersion) {\n return;\n }\n\n const snapshotStartTime = Date.now();\n\n // Load base snapshot from cold storage\n const baseSnapshot = yield* coldStorage.load(documentId);\n const baseVersion = baseSnapshot?.version ?? 0;\n const baseState = baseSnapshot?.state as Primitive.InferState<TSchema> | undefined;\n\n // Load WAL entries from base to target\n const walEntries = yield* hotStorage.getEntries(documentId, baseVersion);\n\n // Compute snapshot state by replaying WAL on base\n const snapshotResult = computeSnapshotState(\n config.schema,\n baseState,\n walEntries,\n targetVersion\n );\n\n if (!snapshotResult) {\n return;\n }\n\n // Re-check before saving (in case another snapshot completed while we were working)\n const currentLastSnapshot = yield* Ref.get(lastSnapshotVersionRef);\n if (snapshotResult.version <= currentLastSnapshot) {\n return;\n }\n\n const storedDoc = createStoredDocument(\n snapshotResult.state,\n snapshotResult.version,\n SCHEMA_VERSION\n );\n\n // Save to ColdStorage\n yield* coldStorage.save(documentId, storedDoc);\n\n // Track snapshot metrics\n const snapshotDuration = Date.now() - snapshotStartTime;\n yield* Metric.increment(Metrics.storageSnapshots);\n yield* Metric.update(Metrics.storageSnapshotLatency, snapshotDuration);\n\n // Update tracking BEFORE truncate (for idempotency on retry)\n yield* Ref.set(lastSnapshotVersionRef, snapshotResult.version);\n yield* Ref.set(lastSnapshotTimeRef, Date.now());\n yield* Ref.set(transactionsSinceSnapshotRef, 0);\n\n // Truncate WAL - non-fatal, will be retried on next snapshot\n yield* Effect.catchAll(hotStorage.truncate(documentId, snapshotResult.version), (e) =>\n Effect.logWarning(\"WAL truncate failed - will retry on next snapshot\", {\n documentId,\n version: snapshotResult.version,\n error: e,\n })\n );\n });\n\n const checkSnapshotTriggers = Effect.fn(\"document.snapshot.check-triggers\")(function* () {\n const txCount = yield* Ref.get(transactionsSinceSnapshotRef);\n const lastTime = yield* Ref.get(lastSnapshotTimeRef);\n\n if (shouldTriggerSnapshot(txCount, lastTime, config.snapshot)) {\n yield* saveSnapshot();\n }\n });\n\n const submit = Effect.fn(\"document.transaction.submit\")(function* (\n transaction: Transaction.Transaction\n ) {\n const submitStartTime = Date.now();\n\n // Update activity time\n yield* Ref.set(lastActivityTimeRef, Date.now());\n\n // Phase 1: Validate (no side effects)\n const validation = document.validate(transaction);\n\n if (!validation.valid) {\n yield* Metric.increment(Metrics.transactionsRejected);\n const latency = Date.now() - submitStartTime;\n yield* Metric.update(Metrics.transactionsLatency, latency);\n\n return {\n success: false as const,\n reason: validation.reason,\n };\n }\n\n // Phase 2: Append to WAL with gap check (BEFORE state mutation)\n const walEntry: WalEntry = {\n transaction,\n version: validation.nextVersion,\n timestamp: Date.now(),\n };\n\n // Get the current snapshot version to pass as baseVersion for gap checking\n // This ensures correct validation after truncation or restart\n const snapshotVersion = yield* Ref.get(lastSnapshotVersionRef);\n\n const appendResult = yield* Effect.either(\n hotStorage.appendWithCheck(documentId, walEntry, validation.nextVersion, snapshotVersion)\n );\n\n if (appendResult._tag === \"Left\") {\n yield* Effect.logError(\"WAL append failed\", {\n documentId,\n version: validation.nextVersion,\n error: appendResult.left,\n });\n yield* Metric.increment(Metrics.walAppendFailures);\n\n const latency = Date.now() - submitStartTime;\n yield* Metric.update(Metrics.transactionsLatency, latency);\n\n return {\n success: false as const,\n reason: \"Storage unavailable. Please retry.\",\n };\n }\n\n // Phase 3: Apply (state mutation + broadcast)\n document.apply(transaction);\n\n // Track metrics\n const latency = Date.now() - submitStartTime;\n yield* Metric.update(Metrics.transactionsLatency, latency);\n yield* Metric.increment(Metrics.transactionsProcessed);\n yield* Metric.increment(Metrics.storageWalAppends);\n\n // Increment transaction count\n yield* Ref.update(transactionsSinceSnapshotRef, (n) => n + 1);\n\n // Check snapshot triggers\n yield* checkSnapshotTriggers();\n\n return {\n success: true as const,\n version: validation.nextVersion,\n };\n });\n\n const touch = Effect.fn(\"document.touch\")(function* () {\n yield* Ref.set(lastActivityTimeRef, Date.now());\n });\n\n const needsSnapshot = () =>\n Effect.map(Ref.get(transactionsSinceSnapshotRef), (n) => n > 0);\n\n const getLastActivityTime = () => Ref.get(lastActivityTimeRef);\n\n return {\n document,\n pubsub,\n getSnapshotTracking,\n submit,\n saveSnapshot,\n checkSnapshotTriggers,\n touch,\n getVersion: () => document.getVersion(),\n getSnapshot: () => document.getSnapshot(),\n toSnapshot: () => document.toSnapshot(),\n needsSnapshot,\n getLastActivityTime,\n };\n });\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\n/**\n * Compute initial state for a new document.\n */\nconst computeInitialState = <TSchema extends Primitive.AnyPrimitive>(\n config: DocumentInstanceConfig<TSchema>,\n documentId: string\n): Effect.Effect<Primitive.InferSetInput<TSchema> | undefined> => {\n if (config.initial === undefined) {\n return Effect.succeed(undefined);\n }\n\n if (typeof config.initial === \"function\") {\n return (config.initial as (ctx: { documentId: string }) => Effect.Effect<Primitive.InferSetInput<TSchema>>)({\n documentId,\n });\n }\n\n return Effect.succeed(config.initial as Primitive.InferSetInput<TSchema>);\n};\n\n/**\n * Verify WAL continuity and log warnings for any gaps.\n */\nconst verifyWalContinuity = Effect.fn(\"document.wal.verify\")(function* (\n documentId: string,\n walEntries: readonly WalEntry[],\n baseVersion: number\n) {\n if (walEntries.length === 0) {\n return;\n }\n\n const firstWalVersion = walEntries[0]!.version;\n const expectedFirst = baseVersion + 1;\n\n if (firstWalVersion !== expectedFirst) {\n yield* Effect.logWarning(\"WAL version gap detected\", {\n documentId,\n snapshotVersion: baseVersion,\n firstWalVersion,\n expectedFirst,\n });\n yield* Metric.increment(Metrics.storageVersionGaps);\n }\n\n for (let i = 1; i < walEntries.length; i++) {\n const prev = walEntries[i - 1]!.version;\n const curr = walEntries[i]!.version;\n if (curr !== prev + 1) {\n yield* Effect.logWarning(\"WAL internal gap detected\", {\n documentId,\n previousVersion: prev,\n currentVersion: curr,\n });\n }\n }\n});\n\n/**\n * Replay WAL entries onto a ServerDocument.\n */\nconst replayWalEntries = Effect.fn(\"document.wal.replay\")(function* (\n documentId: string,\n document: ServerDocument.ServerDocument<Primitive.AnyPrimitive>,\n walEntries: readonly WalEntry[]\n) {\n for (const entry of walEntries) {\n const result = document.submit(entry.transaction);\n if (!result.success) {\n yield* Effect.logWarning(\"Skipping corrupted WAL entry\", {\n documentId,\n version: entry.version,\n reason: result.reason,\n });\n }\n }\n});\n\n/**\n * Compute snapshot state by replaying WAL entries on a base state.\n */\nconst computeSnapshotState = <TSchema extends Primitive.AnyPrimitive>(\n schema: TSchema,\n baseState: Primitive.InferState<TSchema> | undefined,\n walEntries: readonly WalEntry[],\n targetVersion: number\n): { state: Primitive.InferState<TSchema>; version: number } | undefined => {\n const relevantEntries = walEntries.filter((e) => e.version <= targetVersion);\n\n if (relevantEntries.length === 0 && baseState === undefined) {\n return undefined;\n }\n\n let snapshotState: Primitive.InferState<TSchema> | undefined = baseState;\n for (const entry of relevantEntries) {\n const tempDoc = Document.make(schema, { initialState: snapshotState });\n tempDoc.apply(entry.transaction.ops);\n snapshotState = tempDoc.get();\n }\n\n if (snapshotState === undefined) {\n return undefined;\n }\n\n const snapshotVersion =\n relevantEntries.length > 0 ? relevantEntries[relevantEntries.length - 1]!.version : 0;\n\n return { state: snapshotState, version: snapshotVersion };\n};\n\n/**\n * Check if a snapshot should be triggered.\n */\nconst shouldTriggerSnapshot = (\n transactionsSinceSnapshot: number,\n lastSnapshotTime: number,\n config: { interval: Duration.Duration; transactionThreshold: number }\n): boolean => {\n const now = Date.now();\n const intervalMs = Duration.toMillis(config.interval);\n\n if (transactionsSinceSnapshot >= config.transactionThreshold) {\n return true;\n }\n\n if (now - lastSnapshotTime >= intervalMs) {\n return true;\n }\n\n return false;\n};\n\n/**\n * Create a StoredDocument for persistence.\n */\nconst createStoredDocument = (\n state: unknown,\n version: number,\n schemaVersion: number\n): StoredDocument => ({\n state,\n version,\n schemaVersion,\n savedAt: Date.now(),\n});\n\n// =============================================================================\n// Re-export namespace\n// =============================================================================\n\nexport const DocumentInstance = {\n make,\n};\n"],"mappings":"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"}
@@ -81,6 +81,7 @@ const MimicDocumentEntity = _effect_cluster.Entity.make("MimicDocument", [
81
81
  success: SubmitResultSchema
82
82
  }),
83
83
  _effect_rpc.Rpc.make("GetSnapshot", { success: SnapshotResponseSchema }),
84
+ _effect_rpc.Rpc.make("GetTreeSnapshot", { success: effect.Schema.Unknown }),
84
85
  _effect_rpc.Rpc.make("Touch", { success: effect.Schema.Void }),
85
86
  _effect_rpc.Rpc.make("SetPresence", {
86
87
  payload: {
@@ -175,6 +176,9 @@ const createEntityHandler = (config, coldStorage, hotStorage) => effect.Effect.f
175
176
  GetSnapshot: effect.Effect.fn("cluster.document.snapshot.get")(function* () {
176
177
  return instance.getSnapshot();
177
178
  }),
179
+ GetTreeSnapshot: effect.Effect.fn("cluster.document.tree-snapshot.get")(function* () {
180
+ return instance.toSnapshot();
181
+ }),
178
182
  Touch: effect.Effect.fn("cluster.document.touch")(function* () {
179
183
  yield* instance.touch();
180
184
  }),
@@ -302,6 +306,9 @@ const make = (config) => {
302
306
  getSnapshot: (documentId) => effect.Effect.gen(function* () {
303
307
  return yield* makeClient(documentId).GetSnapshot(void 0).pipe(effect.Effect.orDie);
304
308
  }),
309
+ getTreeSnapshot: (documentId) => effect.Effect.gen(function* () {
310
+ return yield* makeClient(documentId).GetTreeSnapshot(void 0).pipe(effect.Effect.orDie);
311
+ }),
305
312
  subscribe: (documentId) => effect.Effect.gen(function* () {
306
313
  const pubsub = yield* subscriptionStore.getOrCreatePubSub(documentId);
307
314
  return effect.Stream.fromPubSub(pubsub);
@@ -1 +1 @@
1
- {"version":3,"file":"MimicClusterServerEngine.d.cts","names":[],"sources":["../src/MimicClusterServerEngine.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;cA0pBa;yBA3JwB,SAAA,CAAU,sBACrC,+BAA+B,aACtC,KAAA,CAAM,MACP,6BAEA,iBAAiB,gBAAgB,sBAAsB,QAAA,CAAS"}
1
+ {"version":3,"file":"MimicClusterServerEngine.d.cts","names":[],"sources":["../src/MimicClusterServerEngine.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;cAyqBa;yBAjKwB,SAAA,CAAU,sBACrC,+BAA+B,aACtC,KAAA,CAAM,MACP,6BAEA,iBAAiB,gBAAgB,sBAAsB,QAAA,CAAS"}
@@ -1 +1 @@
1
- {"version":3,"file":"MimicClusterServerEngine.d.mts","names":[],"sources":["../src/MimicClusterServerEngine.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;cA0pBa;yBA3JwB,SAAA,CAAU,sBACrC,+BAA+B,aACtC,KAAA,CAAM,MACP,6BAEA,iBAAiB,gBAAgB,sBAAsB,QAAA,CAAS"}
1
+ {"version":3,"file":"MimicClusterServerEngine.d.mts","names":[],"sources":["../src/MimicClusterServerEngine.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;cAyqBa;yBAjKwB,SAAA,CAAU,sBACrC,+BAA+B,aACtC,KAAA,CAAM,MACP,6BAEA,iBAAiB,gBAAgB,sBAAsB,QAAA,CAAS"}
@@ -82,6 +82,7 @@ const MimicDocumentEntity = Entity.make("MimicDocument", [
82
82
  success: SubmitResultSchema
83
83
  }),
84
84
  Rpc.make("GetSnapshot", { success: SnapshotResponseSchema }),
85
+ Rpc.make("GetTreeSnapshot", { success: Schema.Unknown }),
85
86
  Rpc.make("Touch", { success: Schema.Void }),
86
87
  Rpc.make("SetPresence", {
87
88
  payload: {
@@ -176,6 +177,9 @@ const createEntityHandler = (config, coldStorage, hotStorage) => Effect.fn("clus
176
177
  GetSnapshot: Effect.fn("cluster.document.snapshot.get")(function* () {
177
178
  return instance.getSnapshot();
178
179
  }),
180
+ GetTreeSnapshot: Effect.fn("cluster.document.tree-snapshot.get")(function* () {
181
+ return instance.toSnapshot();
182
+ }),
179
183
  Touch: Effect.fn("cluster.document.touch")(function* () {
180
184
  yield* instance.touch();
181
185
  }),
@@ -303,6 +307,9 @@ const make = (config) => {
303
307
  getSnapshot: (documentId) => Effect.gen(function* () {
304
308
  return yield* makeClient(documentId).GetSnapshot(void 0).pipe(Effect.orDie);
305
309
  }),
310
+ getTreeSnapshot: (documentId) => Effect.gen(function* () {
311
+ return yield* makeClient(documentId).GetTreeSnapshot(void 0).pipe(Effect.orDie);
312
+ }),
306
313
  subscribe: (documentId) => Effect.gen(function* () {
307
314
  const pubsub = yield* subscriptionStore.getOrCreatePubSub(documentId);
308
315
  return Stream.fromPubSub(pubsub);
@@ -1 +1 @@
1
- {"version":3,"file":"MimicClusterServerEngine.mjs","names":["Metrics.documentsActive","Metrics.documentsEvicted","Metrics.storageIdleSnapshots","Metrics.presenceUpdates","Metrics.presenceActive","event: PresenceEvent","presences: Record<string, PresenceEntry>"],"sources":["../src/MimicClusterServerEngine.ts"],"sourcesContent":["/**\n * @voidhash/mimic-effect - MimicClusterServerEngine\n *\n * Clustered document management service using Effect Cluster for horizontal scaling.\n * Each document becomes a cluster Entity with automatic sharding, failover, and location-transparent routing.\n *\n * This is an alternative to MimicServerEngine for distributed deployments.\n */\nimport {\n Context,\n Duration,\n Effect,\n HashMap,\n Layer,\n Metric,\n PubSub,\n Ref,\n Schedule,\n Schema,\n Stream,\n} from \"effect\";\nimport { Entity, Sharding } from \"@effect/cluster\";\nimport { Rpc } from \"@effect/rpc\";\nimport { type Primitive, type Transaction } from \"@voidhash/mimic\";\nimport type {\n MimicClusterServerEngineConfig,\n PresenceEntry,\n PresenceEvent,\n ResolvedClusterConfig,\n} from \"./Types\";\nimport type * as Protocol from \"./Protocol\";\nimport { ColdStorageTag, type ColdStorage } from \"./ColdStorage\";\nimport { HotStorageTag, type HotStorage } from \"./HotStorage\";\nimport { MimicAuthServiceTag } from \"./MimicAuthService\";\nimport { MimicServerEngineTag, type MimicServerEngine } from \"./MimicServerEngine\";\nimport {\n DocumentInstance,\n type DocumentInstance as DocumentInstanceInterface,\n} from \"./DocumentInstance\";\nimport * as Metrics from \"./Metrics\";\n\n// =============================================================================\n// Default Configuration\n// =============================================================================\n\nconst DEFAULT_MAX_IDLE_TIME = Duration.minutes(5);\nconst DEFAULT_MAX_TRANSACTION_HISTORY = 1000;\nconst DEFAULT_SNAPSHOT_INTERVAL = Duration.minutes(5);\nconst DEFAULT_SNAPSHOT_THRESHOLD = 100;\nconst DEFAULT_SNAPSHOT_IDLE_TIMEOUT = Duration.seconds(30);\nconst DEFAULT_SHARD_GROUP = \"mimic-documents\";\n\n// =============================================================================\n// RPC Schemas\n// =============================================================================\n\n/**\n * Schema for encoded transaction (wire format)\n */\nconst EncodedTransactionSchema = Schema.Struct({\n id: Schema.String,\n ops: Schema.Array(Schema.Unknown),\n});\n\n/**\n * Schema for submit result\n */\nconst SubmitResultSchema = Schema.Union(\n Schema.Struct({\n success: Schema.Literal(true),\n version: Schema.Number,\n }),\n Schema.Struct({\n success: Schema.Literal(false),\n reason: Schema.String,\n })\n);\n\n/**\n * Schema for snapshot response\n */\nconst SnapshotResponseSchema = Schema.Struct({\n state: Schema.Unknown,\n version: Schema.Number,\n});\n\n/**\n * Schema for presence entry\n */\nconst PresenceEntrySchema = Schema.Struct({\n data: Schema.Unknown,\n userId: Schema.optional(Schema.String),\n});\n\n/**\n * Schema for presence snapshot response\n */\nconst PresenceSnapshotResponseSchema = Schema.Struct({\n presences: Schema.Record({ key: Schema.String, value: PresenceEntrySchema }),\n});\n\n/**\n * Schema for presence event\n */\nconst PresenceEventSchema = Schema.Union(\n Schema.Struct({\n type: Schema.Literal(\"presence_update\"),\n id: Schema.String,\n data: Schema.Unknown,\n userId: Schema.optional(Schema.String),\n }),\n Schema.Struct({\n type: Schema.Literal(\"presence_remove\"),\n id: Schema.String,\n })\n);\n\n/**\n * Schema for server message (for broadcasts)\n */\nconst ServerMessageSchema = Schema.Unknown;\n\n// =============================================================================\n// Mimic Document Entity Definition\n// =============================================================================\n\n/**\n * Define the Mimic Document Entity with its RPC protocol.\n * This entity handles document operations for a single documentId.\n */\nconst MimicDocumentEntity = Entity.make(\"MimicDocument\", [\n // Submit a transaction\n Rpc.make(\"Submit\", {\n payload: { transaction: EncodedTransactionSchema },\n success: SubmitResultSchema,\n }),\n\n // Get document snapshot\n Rpc.make(\"GetSnapshot\", {\n success: SnapshotResponseSchema,\n }),\n\n // Touch document to prevent idle GC\n Rpc.make(\"Touch\", {\n success: Schema.Void,\n }),\n\n // Set presence for a connection\n Rpc.make(\"SetPresence\", {\n payload: {\n connectionId: Schema.String,\n entry: PresenceEntrySchema,\n },\n success: Schema.Void,\n }),\n\n // Remove presence for a connection\n Rpc.make(\"RemovePresence\", {\n payload: { connectionId: Schema.String },\n success: Schema.Void,\n }),\n\n // Get presence snapshot\n Rpc.make(\"GetPresenceSnapshot\", {\n success: PresenceSnapshotResponseSchema,\n }),\n]);\n\n// =============================================================================\n// Entity State Types\n// =============================================================================\n\n/**\n * Entity state that wraps DocumentInstance and adds presence management\n */\ninterface EntityState<TSchema extends Primitive.AnyPrimitive> {\n readonly instance: DocumentInstanceInterface<TSchema>;\n readonly presences: HashMap.HashMap<string, PresenceEntry>;\n readonly presencePubSub: PubSub.PubSub<PresenceEvent>;\n}\n\n// =============================================================================\n// Config Context Tag\n// =============================================================================\n\n/**\n * Context tag for cluster engine configuration\n */\nclass MimicClusterConfigTag extends Context.Tag(\n \"@voidhash/mimic-effect/MimicClusterConfig\"\n)<MimicClusterConfigTag, ResolvedClusterConfig<Primitive.AnyPrimitive>>() {}\n\n// =============================================================================\n// Resolve Configuration\n// =============================================================================\n\nconst resolveClusterConfig = <TSchema extends Primitive.AnyPrimitive>(\n config: MimicClusterServerEngineConfig<TSchema>\n): ResolvedClusterConfig<TSchema> => ({\n schema: config.schema,\n initial: config.initial,\n presence: config.presence,\n maxIdleTime: config.maxIdleTime\n ? Duration.decode(config.maxIdleTime)\n : DEFAULT_MAX_IDLE_TIME,\n maxTransactionHistory:\n config.maxTransactionHistory ?? DEFAULT_MAX_TRANSACTION_HISTORY,\n snapshot: {\n interval: config.snapshot?.interval\n ? Duration.decode(config.snapshot.interval)\n : DEFAULT_SNAPSHOT_INTERVAL,\n transactionThreshold:\n config.snapshot?.transactionThreshold ?? DEFAULT_SNAPSHOT_THRESHOLD,\n idleTimeout: config.snapshot?.idleTimeout\n ? Duration.decode(config.snapshot.idleTimeout)\n : DEFAULT_SNAPSHOT_IDLE_TIMEOUT,\n },\n shardGroup: config.shardGroup ?? DEFAULT_SHARD_GROUP,\n});\n\n// =============================================================================\n// Helper to decode/encode transactions\n// =============================================================================\n\n/**\n * Decode an encoded transaction to a Transaction object\n */\nconst decodeTransaction = (\n encoded: { id: string; ops: readonly unknown[] }\n): Transaction.Transaction => {\n // Import Transaction dynamically to avoid circular deps\n const { Transaction } = require(\"@voidhash/mimic\");\n return Transaction.decode(encoded as Transaction.EncodedTransaction);\n};\n\n/**\n * Encode a Transaction to wire format\n */\nconst encodeTransaction = (\n tx: Transaction.Transaction\n): { id: string; ops: readonly unknown[] } => {\n const { Transaction } = require(\"@voidhash/mimic\");\n return Transaction.encode(tx);\n};\n\n// =============================================================================\n// Entity Handler Factory\n// =============================================================================\n\n/**\n * Create the entity handler for MimicDocument\n */\nconst createEntityHandler = <TSchema extends Primitive.AnyPrimitive>(\n config: ResolvedClusterConfig<TSchema>,\n coldStorage: ColdStorage,\n hotStorage: HotStorage\n) =>\n Effect.fn(\"cluster.entity.handler.create\")(function* () {\n // Get entity address to determine documentId\n const address = yield* Entity.CurrentAddress;\n const documentId = address.entityId;\n\n // Create DocumentInstance (fatal if unavailable - entity cannot start)\n const instance = yield* DocumentInstance.make(\n documentId,\n {\n schema: config.schema,\n initial: config.initial,\n maxTransactionHistory: config.maxTransactionHistory,\n snapshot: config.snapshot,\n },\n coldStorage,\n hotStorage\n ).pipe(Effect.orDie);\n\n // Create presence PubSub and state ref\n const presencePubSub = yield* PubSub.unbounded<PresenceEvent>();\n const stateRef = yield* Ref.make<EntityState<TSchema>>({\n instance,\n presences: HashMap.empty(),\n presencePubSub,\n });\n\n // Cleanup on entity finalization\n yield* Effect.addFinalizer(() =>\n Effect.fn(\"cluster.entity.finalize\")(function* () {\n // Best effort save - don't fail shutdown if storage is unavailable\n yield* Effect.catchAll(instance.saveSnapshot(), (e) =>\n Effect.logError(\"Failed to save snapshot during entity finalization\", {\n documentId,\n error: e,\n })\n );\n yield* Metric.incrementBy(Metrics.documentsActive, -1);\n yield* Metric.increment(Metrics.documentsEvicted);\n yield* Effect.logDebug(\"Entity finalized\", { documentId });\n })()\n );\n\n // Start periodic snapshot fiber for this entity\n const idleTimeoutMs = Duration.toMillis(config.snapshot.idleTimeout);\n if (idleTimeoutMs > 0) {\n const snapshotLoop = Effect.fn(\"cluster.entity.snapshot.loop\")(function* () {\n const needs = yield* instance.needsSnapshot();\n if (needs) {\n yield* Effect.catchAll(instance.saveSnapshot(), (e) =>\n Effect.logWarning(\"Periodic snapshot failed in cluster entity\", {\n documentId,\n error: e,\n })\n );\n yield* Metric.increment(Metrics.storageIdleSnapshots);\n }\n });\n\n // Run every idleTimeout\n yield* snapshotLoop().pipe(\n Effect.repeat(Schedule.spaced(config.snapshot.idleTimeout)),\n Effect.fork\n );\n }\n\n // Return RPC handlers\n return {\n Submit: Effect.fn(\"cluster.document.transaction.submit\")(function* ({\n payload,\n }) {\n // Decode transaction\n const transaction = decodeTransaction(payload.transaction);\n\n // Use DocumentInstance's submit method, catching storage errors\n return yield* instance.submit(transaction).pipe(\n Effect.catchAll((error) =>\n Effect.succeed({\n success: false as const,\n reason: `Storage error: ${String(error)}`,\n })\n )\n );\n }),\n\n GetSnapshot: Effect.fn(\"cluster.document.snapshot.get\")(function* () {\n return instance.getSnapshot();\n }),\n\n Touch: Effect.fn(\"cluster.document.touch\")(function* () {\n yield* instance.touch();\n }),\n\n SetPresence: Effect.fn(\"cluster.presence.set\")(function* ({ payload }) {\n const { connectionId, entry } = payload;\n\n yield* Ref.update(stateRef, (s) => ({\n ...s,\n presences: HashMap.set(s.presences, connectionId, entry),\n }));\n\n yield* Metric.increment(Metrics.presenceUpdates);\n yield* Metric.incrementBy(Metrics.presenceActive, 1);\n\n const state = yield* Ref.get(stateRef);\n const event: PresenceEvent = {\n type: \"presence_update\",\n id: connectionId,\n data: entry.data,\n userId: entry.userId,\n };\n yield* PubSub.publish(state.presencePubSub, event);\n }),\n\n RemovePresence: Effect.fn(\"cluster.presence.remove\")(function* ({\n payload,\n }) {\n const { connectionId } = payload;\n const state = yield* Ref.get(stateRef);\n\n if (!HashMap.has(state.presences, connectionId)) {\n return;\n }\n\n yield* Ref.update(stateRef, (s) => ({\n ...s,\n presences: HashMap.remove(s.presences, connectionId),\n }));\n\n yield* Metric.incrementBy(Metrics.presenceActive, -1);\n\n const event: PresenceEvent = {\n type: \"presence_remove\",\n id: connectionId,\n };\n yield* PubSub.publish(state.presencePubSub, event);\n }),\n\n GetPresenceSnapshot: Effect.fn(\"cluster.presence.snapshot.get\")(\n function* () {\n const state = yield* Ref.get(stateRef);\n const presences: Record<string, PresenceEntry> = {};\n for (const [id, entry] of state.presences) {\n presences[id] = entry;\n }\n return { presences };\n }\n ),\n };\n })();\n\n// =============================================================================\n// Subscription Store (for managing subscriptions at the gateway level)\n// =============================================================================\n\n/**\n * Store for managing document subscriptions\n * This is needed because cluster entities don't support streaming directly\n */\ninterface SubscriptionStore {\n readonly getOrCreatePubSub: (\n documentId: string\n ) => Effect.Effect<PubSub.PubSub<Protocol.ServerMessage>>;\n readonly getOrCreatePresencePubSub: (\n documentId: string\n ) => Effect.Effect<PubSub.PubSub<PresenceEvent>>;\n}\n\nclass SubscriptionStoreTag extends Context.Tag(\n \"@voidhash/mimic-effect/SubscriptionStore\"\n)<SubscriptionStoreTag, SubscriptionStore>() {}\n\nconst subscriptionStoreLayer = Layer.effect(\n SubscriptionStoreTag,\n Effect.fn(\"cluster.subscriptions.layer.create\")(function* () {\n const documentPubSubs = yield* Ref.make(\n HashMap.empty<string, PubSub.PubSub<Protocol.ServerMessage>>()\n );\n const presencePubSubs = yield* Ref.make(\n HashMap.empty<string, PubSub.PubSub<PresenceEvent>>()\n );\n\n return {\n getOrCreatePubSub: Effect.fn(\n \"cluster.subscriptions.pubsub.get-or-create\"\n )(function* (documentId: string) {\n const current = yield* Ref.get(documentPubSubs);\n const existing = HashMap.get(current, documentId);\n if (existing._tag === \"Some\") {\n return existing.value;\n }\n\n const pubsub = yield* PubSub.unbounded<Protocol.ServerMessage>();\n yield* Ref.update(documentPubSubs, (map) =>\n HashMap.set(map, documentId, pubsub)\n );\n return pubsub;\n }),\n\n getOrCreatePresencePubSub: Effect.fn(\n \"cluster.subscriptions.presence-pubsub.get-or-create\"\n )(function* (documentId: string) {\n const current = yield* Ref.get(presencePubSubs);\n const existing = HashMap.get(current, documentId);\n if (existing._tag === \"Some\") {\n return existing.value;\n }\n\n const pubsub = yield* PubSub.unbounded<PresenceEvent>();\n yield* Ref.update(presencePubSubs, (map) =>\n HashMap.set(map, documentId, pubsub)\n );\n return pubsub;\n }),\n };\n })()\n);\n\n// =============================================================================\n// Factory\n// =============================================================================\n\n/**\n * Create a MimicClusterServerEngine layer.\n *\n * This creates a clustered document management service using Effect Cluster.\n * Each document becomes a cluster Entity with automatic sharding and failover.\n *\n * @example\n * ```typescript\n * // 1. Create the engine\n * const Engine = MimicClusterServerEngine.make({\n * schema: DocSchema,\n * initial: { title: \"Untitled\" },\n * presence: CursorPresence,\n * maxIdleTime: \"5 minutes\",\n * snapshot: { interval: \"5 minutes\", transactionThreshold: 100 },\n * shardGroup: \"documents\",\n * })\n *\n * // 2. Create the WebSocket route\n * const MimicRoute = MimicServer.layerHttpLayerRouter({\n * path: \"/mimic\",\n * })\n *\n * // 3. Wire together with cluster infrastructure\n * const MimicLive = MimicRoute.pipe(\n * Layer.provide(Engine),\n * Layer.provide(ColdStorage.S3.make(...)),\n * Layer.provide(HotStorage.Redis.make(...)),\n * Layer.provide(MimicAuthService.make(...)),\n * Layer.provide(ClusterInfrastructure),\n * )\n * ```\n */\nexport const make = <TSchema extends Primitive.AnyPrimitive>(\n config: MimicClusterServerEngineConfig<TSchema>\n): Layer.Layer<\n MimicServerEngineTag,\n never,\n ColdStorageTag | HotStorageTag | MimicAuthServiceTag | Sharding.Sharding\n> => {\n const resolvedConfig = resolveClusterConfig(config);\n\n // Create config layer\n const configLayer = Layer.succeed(\n MimicClusterConfigTag,\n resolvedConfig as ResolvedClusterConfig<Primitive.AnyPrimitive>\n );\n\n // Create entity layer that registers with sharding\n const entityLayer = MimicDocumentEntity.toLayer(\n Effect.gen(function* () {\n const clusterConfig = yield* MimicClusterConfigTag;\n const coldStorage = yield* ColdStorageTag;\n const hotStorage = yield* HotStorageTag;\n\n return yield* createEntityHandler(\n clusterConfig as ResolvedClusterConfig<TSchema>,\n coldStorage,\n hotStorage\n );\n }),\n {\n maxIdleTime: resolvedConfig.maxIdleTime,\n concurrency: 1, // Sequential message processing per document\n mailboxCapacity: 4096,\n }\n );\n\n // Create the engine service\n const engineLayer = Layer.scoped(\n MimicServerEngineTag,\n Effect.gen(function* () {\n // Get entity client maker\n const makeClient = yield* MimicDocumentEntity.client;\n\n // Get subscription store\n const subscriptionStore = yield* SubscriptionStoreTag;\n\n const engine: MimicServerEngine = {\n submit: (documentId, transaction) =>\n Effect.gen(function* () {\n const client = makeClient(documentId);\n const encodedTx = encodeTransaction(transaction);\n const result = yield* client.Submit({\n transaction: encodedTx as { id: string; ops: unknown[] },\n }).pipe(\n Effect.catchAll((error) =>\n Effect.succeed({\n success: false as const,\n reason: `Cluster error: ${String(error)}`,\n })\n )\n );\n\n // Broadcast to local subscribers if success\n if (result.success) {\n const pubsub =\n yield* subscriptionStore.getOrCreatePubSub(documentId);\n yield* PubSub.publish(pubsub, {\n type: \"transaction\",\n transaction,\n version: result.version,\n } as Protocol.ServerMessage);\n }\n\n return result;\n }),\n\n getSnapshot: (documentId) =>\n Effect.gen(function* () {\n const client = makeClient(documentId);\n return yield* client.GetSnapshot(undefined as void).pipe(Effect.orDie);\n }),\n\n subscribe: (documentId) =>\n Effect.gen(function* () {\n const pubsub =\n yield* subscriptionStore.getOrCreatePubSub(documentId);\n return Stream.fromPubSub(pubsub);\n }),\n\n touch: (documentId) =>\n Effect.gen(function* () {\n const client = makeClient(documentId);\n yield* client.Touch(undefined as void).pipe(Effect.orDie);\n }),\n\n getPresenceSnapshot: (documentId) =>\n Effect.gen(function* () {\n const client = makeClient(documentId);\n return yield* client.GetPresenceSnapshot(undefined as void).pipe(Effect.orDie);\n }),\n\n setPresence: (documentId, connectionId, entry) =>\n Effect.gen(function* () {\n const client = makeClient(documentId);\n yield* client.SetPresence({ connectionId, entry }).pipe(Effect.orDie);\n\n // Broadcast to local presence subscribers\n const pubsub =\n yield* subscriptionStore.getOrCreatePresencePubSub(documentId);\n yield* PubSub.publish(pubsub, {\n type: \"presence_update\",\n id: connectionId,\n data: entry.data,\n userId: entry.userId,\n });\n }),\n\n removePresence: (documentId, connectionId) =>\n Effect.gen(function* () {\n const client = makeClient(documentId);\n yield* client.RemovePresence({ connectionId }).pipe(Effect.orDie);\n\n // Broadcast to local presence subscribers\n const pubsub =\n yield* subscriptionStore.getOrCreatePresencePubSub(documentId);\n yield* PubSub.publish(pubsub, {\n type: \"presence_remove\",\n id: connectionId,\n });\n }),\n\n subscribePresence: (documentId) =>\n Effect.gen(function* () {\n const pubsub =\n yield* subscriptionStore.getOrCreatePresencePubSub(documentId);\n return Stream.fromPubSub(pubsub);\n }),\n\n config: resolvedConfig as ResolvedClusterConfig<Primitive.AnyPrimitive>,\n };\n\n return engine;\n })\n );\n\n // Compose all layers\n return Layer.mergeAll(entityLayer, engineLayer).pipe(\n Layer.provideMerge(subscriptionStoreLayer),\n Layer.provideMerge(configLayer)\n );\n};\n\n// =============================================================================\n// Re-export namespace\n// =============================================================================\n\nexport const MimicClusterServerEngine = {\n make,\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"MimicClusterServerEngine.mjs","names":["Metrics.documentsActive","Metrics.documentsEvicted","Metrics.storageIdleSnapshots","Metrics.presenceUpdates","Metrics.presenceActive","event: PresenceEvent","presences: Record<string, PresenceEntry>"],"sources":["../src/MimicClusterServerEngine.ts"],"sourcesContent":["/**\n * @voidhash/mimic-effect - MimicClusterServerEngine\n *\n * Clustered document management service using Effect Cluster for horizontal scaling.\n * Each document becomes a cluster Entity with automatic sharding, failover, and location-transparent routing.\n *\n * This is an alternative to MimicServerEngine for distributed deployments.\n */\nimport {\n Context,\n Duration,\n Effect,\n HashMap,\n Layer,\n Metric,\n PubSub,\n Ref,\n Schedule,\n Schema,\n Stream,\n} from \"effect\";\nimport { Entity, Sharding } from \"@effect/cluster\";\nimport { Rpc } from \"@effect/rpc\";\nimport { type Primitive, type Transaction } from \"@voidhash/mimic\";\nimport type {\n MimicClusterServerEngineConfig,\n PresenceEntry,\n PresenceEvent,\n ResolvedClusterConfig,\n} from \"./Types\";\nimport type * as Protocol from \"./Protocol\";\nimport { ColdStorageTag, type ColdStorage } from \"./ColdStorage\";\nimport { HotStorageTag, type HotStorage } from \"./HotStorage\";\nimport { MimicAuthServiceTag } from \"./MimicAuthService\";\nimport { MimicServerEngineTag, type MimicServerEngine } from \"./MimicServerEngine\";\nimport {\n DocumentInstance,\n type DocumentInstance as DocumentInstanceInterface,\n} from \"./DocumentInstance\";\nimport * as Metrics from \"./Metrics\";\n\n// =============================================================================\n// Default Configuration\n// =============================================================================\n\nconst DEFAULT_MAX_IDLE_TIME = Duration.minutes(5);\nconst DEFAULT_MAX_TRANSACTION_HISTORY = 1000;\nconst DEFAULT_SNAPSHOT_INTERVAL = Duration.minutes(5);\nconst DEFAULT_SNAPSHOT_THRESHOLD = 100;\nconst DEFAULT_SNAPSHOT_IDLE_TIMEOUT = Duration.seconds(30);\nconst DEFAULT_SHARD_GROUP = \"mimic-documents\";\n\n// =============================================================================\n// RPC Schemas\n// =============================================================================\n\n/**\n * Schema for encoded transaction (wire format)\n */\nconst EncodedTransactionSchema = Schema.Struct({\n id: Schema.String,\n ops: Schema.Array(Schema.Unknown),\n});\n\n/**\n * Schema for submit result\n */\nconst SubmitResultSchema = Schema.Union(\n Schema.Struct({\n success: Schema.Literal(true),\n version: Schema.Number,\n }),\n Schema.Struct({\n success: Schema.Literal(false),\n reason: Schema.String,\n })\n);\n\n/**\n * Schema for snapshot response\n */\nconst SnapshotResponseSchema = Schema.Struct({\n state: Schema.Unknown,\n version: Schema.Number,\n});\n\n/**\n * Schema for presence entry\n */\nconst PresenceEntrySchema = Schema.Struct({\n data: Schema.Unknown,\n userId: Schema.optional(Schema.String),\n});\n\n/**\n * Schema for presence snapshot response\n */\nconst PresenceSnapshotResponseSchema = Schema.Struct({\n presences: Schema.Record({ key: Schema.String, value: PresenceEntrySchema }),\n});\n\n/**\n * Schema for presence event\n */\nconst PresenceEventSchema = Schema.Union(\n Schema.Struct({\n type: Schema.Literal(\"presence_update\"),\n id: Schema.String,\n data: Schema.Unknown,\n userId: Schema.optional(Schema.String),\n }),\n Schema.Struct({\n type: Schema.Literal(\"presence_remove\"),\n id: Schema.String,\n })\n);\n\n/**\n * Schema for server message (for broadcasts)\n */\nconst ServerMessageSchema = Schema.Unknown;\n\n// =============================================================================\n// Mimic Document Entity Definition\n// =============================================================================\n\n/**\n * Define the Mimic Document Entity with its RPC protocol.\n * This entity handles document operations for a single documentId.\n */\nconst MimicDocumentEntity = Entity.make(\"MimicDocument\", [\n // Submit a transaction\n Rpc.make(\"Submit\", {\n payload: { transaction: EncodedTransactionSchema },\n success: SubmitResultSchema,\n }),\n\n // Get document snapshot (flat state)\n Rpc.make(\"GetSnapshot\", {\n success: SnapshotResponseSchema,\n }),\n\n // Get tree-like snapshot for rendering\n Rpc.make(\"GetTreeSnapshot\", {\n success: Schema.Unknown,\n }),\n\n // Touch document to prevent idle GC\n Rpc.make(\"Touch\", {\n success: Schema.Void,\n }),\n\n // Set presence for a connection\n Rpc.make(\"SetPresence\", {\n payload: {\n connectionId: Schema.String,\n entry: PresenceEntrySchema,\n },\n success: Schema.Void,\n }),\n\n // Remove presence for a connection\n Rpc.make(\"RemovePresence\", {\n payload: { connectionId: Schema.String },\n success: Schema.Void,\n }),\n\n // Get presence snapshot\n Rpc.make(\"GetPresenceSnapshot\", {\n success: PresenceSnapshotResponseSchema,\n }),\n]);\n\n// =============================================================================\n// Entity State Types\n// =============================================================================\n\n/**\n * Entity state that wraps DocumentInstance and adds presence management\n */\ninterface EntityState<TSchema extends Primitive.AnyPrimitive> {\n readonly instance: DocumentInstanceInterface<TSchema>;\n readonly presences: HashMap.HashMap<string, PresenceEntry>;\n readonly presencePubSub: PubSub.PubSub<PresenceEvent>;\n}\n\n// =============================================================================\n// Config Context Tag\n// =============================================================================\n\n/**\n * Context tag for cluster engine configuration\n */\nclass MimicClusterConfigTag extends Context.Tag(\n \"@voidhash/mimic-effect/MimicClusterConfig\"\n)<MimicClusterConfigTag, ResolvedClusterConfig<Primitive.AnyPrimitive>>() {}\n\n// =============================================================================\n// Resolve Configuration\n// =============================================================================\n\nconst resolveClusterConfig = <TSchema extends Primitive.AnyPrimitive>(\n config: MimicClusterServerEngineConfig<TSchema>\n): ResolvedClusterConfig<TSchema> => ({\n schema: config.schema,\n initial: config.initial,\n presence: config.presence,\n maxIdleTime: config.maxIdleTime\n ? Duration.decode(config.maxIdleTime)\n : DEFAULT_MAX_IDLE_TIME,\n maxTransactionHistory:\n config.maxTransactionHistory ?? DEFAULT_MAX_TRANSACTION_HISTORY,\n snapshot: {\n interval: config.snapshot?.interval\n ? Duration.decode(config.snapshot.interval)\n : DEFAULT_SNAPSHOT_INTERVAL,\n transactionThreshold:\n config.snapshot?.transactionThreshold ?? DEFAULT_SNAPSHOT_THRESHOLD,\n idleTimeout: config.snapshot?.idleTimeout\n ? Duration.decode(config.snapshot.idleTimeout)\n : DEFAULT_SNAPSHOT_IDLE_TIMEOUT,\n },\n shardGroup: config.shardGroup ?? DEFAULT_SHARD_GROUP,\n});\n\n// =============================================================================\n// Helper to decode/encode transactions\n// =============================================================================\n\n/**\n * Decode an encoded transaction to a Transaction object\n */\nconst decodeTransaction = (\n encoded: { id: string; ops: readonly unknown[] }\n): Transaction.Transaction => {\n // Import Transaction dynamically to avoid circular deps\n const { Transaction } = require(\"@voidhash/mimic\");\n return Transaction.decode(encoded as Transaction.EncodedTransaction);\n};\n\n/**\n * Encode a Transaction to wire format\n */\nconst encodeTransaction = (\n tx: Transaction.Transaction\n): { id: string; ops: readonly unknown[] } => {\n const { Transaction } = require(\"@voidhash/mimic\");\n return Transaction.encode(tx);\n};\n\n// =============================================================================\n// Entity Handler Factory\n// =============================================================================\n\n/**\n * Create the entity handler for MimicDocument\n */\nconst createEntityHandler = <TSchema extends Primitive.AnyPrimitive>(\n config: ResolvedClusterConfig<TSchema>,\n coldStorage: ColdStorage,\n hotStorage: HotStorage\n) =>\n Effect.fn(\"cluster.entity.handler.create\")(function* () {\n // Get entity address to determine documentId\n const address = yield* Entity.CurrentAddress;\n const documentId = address.entityId;\n\n // Create DocumentInstance (fatal if unavailable - entity cannot start)\n const instance = yield* DocumentInstance.make(\n documentId,\n {\n schema: config.schema,\n initial: config.initial,\n maxTransactionHistory: config.maxTransactionHistory,\n snapshot: config.snapshot,\n },\n coldStorage,\n hotStorage\n ).pipe(Effect.orDie);\n\n // Create presence PubSub and state ref\n const presencePubSub = yield* PubSub.unbounded<PresenceEvent>();\n const stateRef = yield* Ref.make<EntityState<TSchema>>({\n instance,\n presences: HashMap.empty(),\n presencePubSub,\n });\n\n // Cleanup on entity finalization\n yield* Effect.addFinalizer(() =>\n Effect.fn(\"cluster.entity.finalize\")(function* () {\n // Best effort save - don't fail shutdown if storage is unavailable\n yield* Effect.catchAll(instance.saveSnapshot(), (e) =>\n Effect.logError(\"Failed to save snapshot during entity finalization\", {\n documentId,\n error: e,\n })\n );\n yield* Metric.incrementBy(Metrics.documentsActive, -1);\n yield* Metric.increment(Metrics.documentsEvicted);\n yield* Effect.logDebug(\"Entity finalized\", { documentId });\n })()\n );\n\n // Start periodic snapshot fiber for this entity\n const idleTimeoutMs = Duration.toMillis(config.snapshot.idleTimeout);\n if (idleTimeoutMs > 0) {\n const snapshotLoop = Effect.fn(\"cluster.entity.snapshot.loop\")(function* () {\n const needs = yield* instance.needsSnapshot();\n if (needs) {\n yield* Effect.catchAll(instance.saveSnapshot(), (e) =>\n Effect.logWarning(\"Periodic snapshot failed in cluster entity\", {\n documentId,\n error: e,\n })\n );\n yield* Metric.increment(Metrics.storageIdleSnapshots);\n }\n });\n\n // Run every idleTimeout\n yield* snapshotLoop().pipe(\n Effect.repeat(Schedule.spaced(config.snapshot.idleTimeout)),\n Effect.fork\n );\n }\n\n // Return RPC handlers\n return {\n Submit: Effect.fn(\"cluster.document.transaction.submit\")(function* ({\n payload,\n }) {\n // Decode transaction\n const transaction = decodeTransaction(payload.transaction);\n\n // Use DocumentInstance's submit method, catching storage errors\n return yield* instance.submit(transaction).pipe(\n Effect.catchAll((error) =>\n Effect.succeed({\n success: false as const,\n reason: `Storage error: ${String(error)}`,\n })\n )\n );\n }),\n\n GetSnapshot: Effect.fn(\"cluster.document.snapshot.get\")(function* () {\n return instance.getSnapshot();\n }),\n\n GetTreeSnapshot: Effect.fn(\"cluster.document.tree-snapshot.get\")(function* () {\n return instance.toSnapshot();\n }),\n\n Touch: Effect.fn(\"cluster.document.touch\")(function* () {\n yield* instance.touch();\n }),\n\n SetPresence: Effect.fn(\"cluster.presence.set\")(function* ({ payload }) {\n const { connectionId, entry } = payload;\n\n yield* Ref.update(stateRef, (s) => ({\n ...s,\n presences: HashMap.set(s.presences, connectionId, entry),\n }));\n\n yield* Metric.increment(Metrics.presenceUpdates);\n yield* Metric.incrementBy(Metrics.presenceActive, 1);\n\n const state = yield* Ref.get(stateRef);\n const event: PresenceEvent = {\n type: \"presence_update\",\n id: connectionId,\n data: entry.data,\n userId: entry.userId,\n };\n yield* PubSub.publish(state.presencePubSub, event);\n }),\n\n RemovePresence: Effect.fn(\"cluster.presence.remove\")(function* ({\n payload,\n }) {\n const { connectionId } = payload;\n const state = yield* Ref.get(stateRef);\n\n if (!HashMap.has(state.presences, connectionId)) {\n return;\n }\n\n yield* Ref.update(stateRef, (s) => ({\n ...s,\n presences: HashMap.remove(s.presences, connectionId),\n }));\n\n yield* Metric.incrementBy(Metrics.presenceActive, -1);\n\n const event: PresenceEvent = {\n type: \"presence_remove\",\n id: connectionId,\n };\n yield* PubSub.publish(state.presencePubSub, event);\n }),\n\n GetPresenceSnapshot: Effect.fn(\"cluster.presence.snapshot.get\")(\n function* () {\n const state = yield* Ref.get(stateRef);\n const presences: Record<string, PresenceEntry> = {};\n for (const [id, entry] of state.presences) {\n presences[id] = entry;\n }\n return { presences };\n }\n ),\n };\n })();\n\n// =============================================================================\n// Subscription Store (for managing subscriptions at the gateway level)\n// =============================================================================\n\n/**\n * Store for managing document subscriptions\n * This is needed because cluster entities don't support streaming directly\n */\ninterface SubscriptionStore {\n readonly getOrCreatePubSub: (\n documentId: string\n ) => Effect.Effect<PubSub.PubSub<Protocol.ServerMessage>>;\n readonly getOrCreatePresencePubSub: (\n documentId: string\n ) => Effect.Effect<PubSub.PubSub<PresenceEvent>>;\n}\n\nclass SubscriptionStoreTag extends Context.Tag(\n \"@voidhash/mimic-effect/SubscriptionStore\"\n)<SubscriptionStoreTag, SubscriptionStore>() {}\n\nconst subscriptionStoreLayer = Layer.effect(\n SubscriptionStoreTag,\n Effect.fn(\"cluster.subscriptions.layer.create\")(function* () {\n const documentPubSubs = yield* Ref.make(\n HashMap.empty<string, PubSub.PubSub<Protocol.ServerMessage>>()\n );\n const presencePubSubs = yield* Ref.make(\n HashMap.empty<string, PubSub.PubSub<PresenceEvent>>()\n );\n\n return {\n getOrCreatePubSub: Effect.fn(\n \"cluster.subscriptions.pubsub.get-or-create\"\n )(function* (documentId: string) {\n const current = yield* Ref.get(documentPubSubs);\n const existing = HashMap.get(current, documentId);\n if (existing._tag === \"Some\") {\n return existing.value;\n }\n\n const pubsub = yield* PubSub.unbounded<Protocol.ServerMessage>();\n yield* Ref.update(documentPubSubs, (map) =>\n HashMap.set(map, documentId, pubsub)\n );\n return pubsub;\n }),\n\n getOrCreatePresencePubSub: Effect.fn(\n \"cluster.subscriptions.presence-pubsub.get-or-create\"\n )(function* (documentId: string) {\n const current = yield* Ref.get(presencePubSubs);\n const existing = HashMap.get(current, documentId);\n if (existing._tag === \"Some\") {\n return existing.value;\n }\n\n const pubsub = yield* PubSub.unbounded<PresenceEvent>();\n yield* Ref.update(presencePubSubs, (map) =>\n HashMap.set(map, documentId, pubsub)\n );\n return pubsub;\n }),\n };\n })()\n);\n\n// =============================================================================\n// Factory\n// =============================================================================\n\n/**\n * Create a MimicClusterServerEngine layer.\n *\n * This creates a clustered document management service using Effect Cluster.\n * Each document becomes a cluster Entity with automatic sharding and failover.\n *\n * @example\n * ```typescript\n * // 1. Create the engine\n * const Engine = MimicClusterServerEngine.make({\n * schema: DocSchema,\n * initial: { title: \"Untitled\" },\n * presence: CursorPresence,\n * maxIdleTime: \"5 minutes\",\n * snapshot: { interval: \"5 minutes\", transactionThreshold: 100 },\n * shardGroup: \"documents\",\n * })\n *\n * // 2. Create the WebSocket route\n * const MimicRoute = MimicServer.layerHttpLayerRouter({\n * path: \"/mimic\",\n * })\n *\n * // 3. Wire together with cluster infrastructure\n * const MimicLive = MimicRoute.pipe(\n * Layer.provide(Engine),\n * Layer.provide(ColdStorage.S3.make(...)),\n * Layer.provide(HotStorage.Redis.make(...)),\n * Layer.provide(MimicAuthService.make(...)),\n * Layer.provide(ClusterInfrastructure),\n * )\n * ```\n */\nexport const make = <TSchema extends Primitive.AnyPrimitive>(\n config: MimicClusterServerEngineConfig<TSchema>\n): Layer.Layer<\n MimicServerEngineTag,\n never,\n ColdStorageTag | HotStorageTag | MimicAuthServiceTag | Sharding.Sharding\n> => {\n const resolvedConfig = resolveClusterConfig(config);\n\n // Create config layer\n const configLayer = Layer.succeed(\n MimicClusterConfigTag,\n resolvedConfig as ResolvedClusterConfig<Primitive.AnyPrimitive>\n );\n\n // Create entity layer that registers with sharding\n const entityLayer = MimicDocumentEntity.toLayer(\n Effect.gen(function* () {\n const clusterConfig = yield* MimicClusterConfigTag;\n const coldStorage = yield* ColdStorageTag;\n const hotStorage = yield* HotStorageTag;\n\n return yield* createEntityHandler(\n clusterConfig as ResolvedClusterConfig<TSchema>,\n coldStorage,\n hotStorage\n );\n }),\n {\n maxIdleTime: resolvedConfig.maxIdleTime,\n concurrency: 1, // Sequential message processing per document\n mailboxCapacity: 4096,\n }\n );\n\n // Create the engine service\n const engineLayer = Layer.scoped(\n MimicServerEngineTag,\n Effect.gen(function* () {\n // Get entity client maker\n const makeClient = yield* MimicDocumentEntity.client;\n\n // Get subscription store\n const subscriptionStore = yield* SubscriptionStoreTag;\n\n const engine: MimicServerEngine = {\n submit: (documentId, transaction) =>\n Effect.gen(function* () {\n const client = makeClient(documentId);\n const encodedTx = encodeTransaction(transaction);\n const result = yield* client.Submit({\n transaction: encodedTx as { id: string; ops: unknown[] },\n }).pipe(\n Effect.catchAll((error) =>\n Effect.succeed({\n success: false as const,\n reason: `Cluster error: ${String(error)}`,\n })\n )\n );\n\n // Broadcast to local subscribers if success\n if (result.success) {\n const pubsub =\n yield* subscriptionStore.getOrCreatePubSub(documentId);\n yield* PubSub.publish(pubsub, {\n type: \"transaction\",\n transaction,\n version: result.version,\n } as Protocol.ServerMessage);\n }\n\n return result;\n }),\n\n getSnapshot: (documentId) =>\n Effect.gen(function* () {\n const client = makeClient(documentId);\n return yield* client.GetSnapshot(undefined as void).pipe(Effect.orDie);\n }),\n\n getTreeSnapshot: (documentId) =>\n Effect.gen(function* () {\n const client = makeClient(documentId);\n return yield* client.GetTreeSnapshot(undefined as void).pipe(Effect.orDie);\n }),\n\n subscribe: (documentId) =>\n Effect.gen(function* () {\n const pubsub =\n yield* subscriptionStore.getOrCreatePubSub(documentId);\n return Stream.fromPubSub(pubsub);\n }),\n\n touch: (documentId) =>\n Effect.gen(function* () {\n const client = makeClient(documentId);\n yield* client.Touch(undefined as void).pipe(Effect.orDie);\n }),\n\n getPresenceSnapshot: (documentId) =>\n Effect.gen(function* () {\n const client = makeClient(documentId);\n return yield* client.GetPresenceSnapshot(undefined as void).pipe(Effect.orDie);\n }),\n\n setPresence: (documentId, connectionId, entry) =>\n Effect.gen(function* () {\n const client = makeClient(documentId);\n yield* client.SetPresence({ connectionId, entry }).pipe(Effect.orDie);\n\n // Broadcast to local presence subscribers\n const pubsub =\n yield* subscriptionStore.getOrCreatePresencePubSub(documentId);\n yield* PubSub.publish(pubsub, {\n type: \"presence_update\",\n id: connectionId,\n data: entry.data,\n userId: entry.userId,\n });\n }),\n\n removePresence: (documentId, connectionId) =>\n Effect.gen(function* () {\n const client = makeClient(documentId);\n yield* client.RemovePresence({ connectionId }).pipe(Effect.orDie);\n\n // Broadcast to local presence subscribers\n const pubsub =\n yield* subscriptionStore.getOrCreatePresencePubSub(documentId);\n yield* PubSub.publish(pubsub, {\n type: \"presence_remove\",\n id: connectionId,\n });\n }),\n\n subscribePresence: (documentId) =>\n Effect.gen(function* () {\n const pubsub =\n yield* subscriptionStore.getOrCreatePresencePubSub(documentId);\n return Stream.fromPubSub(pubsub);\n }),\n\n config: resolvedConfig as ResolvedClusterConfig<Primitive.AnyPrimitive>,\n };\n\n return engine;\n })\n );\n\n // Compose all layers\n return Layer.mergeAll(entityLayer, engineLayer).pipe(\n Layer.provideMerge(subscriptionStoreLayer),\n Layer.provideMerge(configLayer)\n );\n};\n\n// =============================================================================\n// Re-export namespace\n// =============================================================================\n\nexport const MimicClusterServerEngine = {\n make,\n};\n"],"mappings":"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"}
@@ -151,6 +151,9 @@ const make = (config) => {
151
151
  getSnapshot: (documentId) => effect.Effect.gen(function* () {
152
152
  return (yield* getOrCreateDocument(documentId)).getSnapshot();
153
153
  }),
154
+ getTreeSnapshot: (documentId) => effect.Effect.gen(function* () {
155
+ return (yield* getOrCreateDocument(documentId)).toSnapshot();
156
+ }),
154
157
  subscribe: (documentId) => effect.Effect.gen(function* () {
155
158
  const instance = yield* getOrCreateDocument(documentId);
156
159
  return effect.Stream.fromPubSub(instance.pubsub);
@@ -29,12 +29,20 @@ interface MimicServerEngine {
29
29
  readonly submit: (documentId: string, transaction: Transaction.Transaction) => Effect.Effect<SubmitResult, MimicServerEngineError>;
30
30
  /**
31
31
  * Get document snapshot (current state and version).
32
+ * Returns flat state format used for storage and synchronization.
32
33
  * May fail with MimicServerEngineError if storage is unavailable.
33
34
  */
34
35
  readonly getSnapshot: (documentId: string) => Effect.Effect<{
35
36
  state: unknown;
36
37
  version: number;
37
38
  }, MimicServerEngineError>;
39
+ /**
40
+ * Get tree-like snapshot for rendering.
41
+ * Returns a readonly structure where trees are converted from
42
+ * flat state to nested/hierarchical structure suitable for UI rendering.
43
+ * May fail with MimicServerEngineError if storage is unavailable.
44
+ */
45
+ readonly getTreeSnapshot: (documentId: string) => Effect.Effect<unknown, MimicServerEngineError>;
38
46
  /**
39
47
  * Subscribe to document broadcasts (transactions).
40
48
  * Returns a stream of server messages.
@@ -1 +1 @@
1
- {"version":3,"file":"MimicServerEngine.d.cts","names":[],"sources":["../src/MimicServerEngine.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;AA0FqB,KAvCT,sBAAA,GAAyB,gBAuCT,GAvC4B,eAuC5B;;;;;;;AAoBjB,UA/CM,iBAAA,CA+CN;EACJ;;;;;EAgBA,SAAO,MAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,WAAA,EAxDG,WAAA,CAAY,WAwDf,EAAA,GAvDP,MAAA,CAAO,MAuDA,CAvDO,YAuDP,EAvDqB,sBAuDrB,CAAA;EAMoB;;;AACjC;gDAtDM,MAAA,CAAO;;;KAA4C;EA+D7C;AA+Tb;;;;;EArOE,SAAA,SAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,GA/IK,MAAA,CAAO,MA+IZ,CA/ImB,MAAA,CAAO,MA+I1B,CA/IiC,aA+IjC,EAAA,KAAA,EAAA,KAAA,CAAA,EA/IwE,sBA+IxE,EA/IgG,KAAA,CAAM,KA+ItG,CAAA;EAEA;;;EAHC,SAAM,KAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,GAzIiC,MAAA,CAAO,MAyIxC,CAAA,IAAA,EAAA,KAAA,CAAA;EAAK;;;wDAlIP,MAAA,CAAO,OAAO;;;;0EAQV,kBACJ,MAAA,CAAO;;;;yEAQP,MAAA,CAAO;;;;;sDAQP,MAAA,CAAO,OAAO,MAAA,CAAO,OAAO,qCAAqC,KAAA,CAAM;;;;;mBAM3D,eAAe,SAAA,CAAU;;cAC3C;;;;cASY,oBAAA,SAA6B,yBAAA;cA+T7B;;yBAxOwB,SAAA,CAAU,sBACrC,wBAAwB,aAC/B,KAAA,CAAM,MACP,6BAEA,iBAAiB,gBAAgB"}
1
+ {"version":3,"file":"MimicServerEngine.d.cts","names":[],"sources":["../src/MimicServerEngine.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;AA2FO,KAxCK,sBAAA,GAAyB,gBAwCvB,GAxC0C,eAwC1C;;;;;;;AAsBO,UAlDJ,iBAAA,CAkDI;EAAd;;;;;EAyBc,SAAO,MAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,WAAA,EAnEX,WAAA,CAAY,WAmED,EAAA,GAlErB,MAAA,CAAO,MAkEc,CAlEP,YAkEO,EAlEO,sBAkEP,CAAA;EAA4C;;;;;EAOvE,SAAA,WAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,GAhEM,MAAA,CAAO,MAgEb,CAAA;;;KAhEyD;;AAyE1D;AAqUA;;;;EA7OU,SAAA,eAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,GAvJH,MAAA,CAAO,MAuJJ,CAAA,OAAA,EAvJoB,sBAuJpB,CAAA;EAER;;;;;;8CA/IK,MAAA,CAAO,OAAO,MAAA,CAAO,OAAO,8BAAuC,wBAAwB,KAAA,CAAM;;;;0CAK9D,MAAA,CAAO;;;;wDAO1C,MAAA,CAAO,OAAO;;;;0EAQV,kBACJ,MAAA,CAAO;;;;yEAQP,MAAA,CAAO;;;;;sDAQP,MAAA,CAAO,OAAO,MAAA,CAAO,OAAO,qCAAqC,KAAA,CAAM;;;;;mBAM3D,eAAe,SAAA,CAAU;;cAC3C;;;;cASY,oBAAA,SAA6B,yBAAA;cAqU7B;;yBA9OwB,SAAA,CAAU,sBACrC,wBAAwB,aAC/B,KAAA,CAAM,MACP,6BAEA,iBAAiB,gBAAgB"}
@@ -29,12 +29,20 @@ interface MimicServerEngine {
29
29
  readonly submit: (documentId: string, transaction: Transaction.Transaction) => Effect.Effect<SubmitResult, MimicServerEngineError>;
30
30
  /**
31
31
  * Get document snapshot (current state and version).
32
+ * Returns flat state format used for storage and synchronization.
32
33
  * May fail with MimicServerEngineError if storage is unavailable.
33
34
  */
34
35
  readonly getSnapshot: (documentId: string) => Effect.Effect<{
35
36
  state: unknown;
36
37
  version: number;
37
38
  }, MimicServerEngineError>;
39
+ /**
40
+ * Get tree-like snapshot for rendering.
41
+ * Returns a readonly structure where trees are converted from
42
+ * flat state to nested/hierarchical structure suitable for UI rendering.
43
+ * May fail with MimicServerEngineError if storage is unavailable.
44
+ */
45
+ readonly getTreeSnapshot: (documentId: string) => Effect.Effect<unknown, MimicServerEngineError>;
38
46
  /**
39
47
  * Subscribe to document broadcasts (transactions).
40
48
  * Returns a stream of server messages.
@@ -1 +1 @@
1
- {"version":3,"file":"MimicServerEngine.d.mts","names":[],"sources":["../src/MimicServerEngine.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;AA0FqB,KAvCT,sBAAA,GAAyB,gBAuCT,GAvC4B,eAuC5B;;;;;;;AAoBjB,UA/CM,iBAAA,CA+CN;EACJ;;;;;EAgBA,SAAO,MAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,WAAA,EAxDG,WAAA,CAAY,WAwDf,EAAA,GAvDP,MAAA,CAAO,MAuDA,CAvDO,YAuDP,EAvDqB,sBAuDrB,CAAA;EAMoB;;;AACjC;gDAtDM,MAAA,CAAO;;;KAA4C;EA+D7C;AA+Tb;;;;;EArOE,SAAA,SAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,GA/IK,MAAA,CAAO,MA+IZ,CA/ImB,MAAA,CAAO,MA+I1B,CA/IiC,aA+IjC,EAAA,KAAA,EAAA,KAAA,CAAA,EA/IwE,sBA+IxE,EA/IgG,KAAA,CAAM,KA+ItG,CAAA;EAEA;;;EAHC,SAAM,KAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,GAzIiC,MAAA,CAAO,MAyIxC,CAAA,IAAA,EAAA,KAAA,CAAA;EAAK;;;wDAlIP,MAAA,CAAO,OAAO;;;;0EAQV,kBACJ,MAAA,CAAO;;;;yEAQP,MAAA,CAAO;;;;;sDAQP,MAAA,CAAO,OAAO,MAAA,CAAO,OAAO,qCAAqC,KAAA,CAAM;;;;;mBAM3D,eAAe,SAAA,CAAU;;cAC3C;;;;cASY,oBAAA,SAA6B,yBAAA;cA+T7B;;yBAxOwB,SAAA,CAAU,sBACrC,wBAAwB,aAC/B,KAAA,CAAM,MACP,6BAEA,iBAAiB,gBAAgB"}
1
+ {"version":3,"file":"MimicServerEngine.d.mts","names":[],"sources":["../src/MimicServerEngine.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;AA2FO,KAxCK,sBAAA,GAAyB,gBAwCvB,GAxC0C,eAwC1C;;;;;;;AAsBO,UAlDJ,iBAAA,CAkDI;EAAd;;;;;EAyBc,SAAO,MAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,WAAA,EAnEX,WAAA,CAAY,WAmED,EAAA,GAlErB,MAAA,CAAO,MAkEc,CAlEP,YAkEO,EAlEO,sBAkEP,CAAA;EAA4C;;;;;EAOvE,SAAA,WAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,GAhEM,MAAA,CAAO,MAgEb,CAAA;;;KAhEyD;;AAyE1D;AAqUA;;;;EA7OU,SAAA,eAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,GAvJH,MAAA,CAAO,MAuJJ,CAAA,OAAA,EAvJoB,sBAuJpB,CAAA;EAER;;;;;;8CA/IK,MAAA,CAAO,OAAO,MAAA,CAAO,OAAO,8BAAuC,wBAAwB,KAAA,CAAM;;;;0CAK9D,MAAA,CAAO;;;;wDAO1C,MAAA,CAAO,OAAO;;;;0EAQV,kBACJ,MAAA,CAAO;;;;yEAQP,MAAA,CAAO;;;;;sDAQP,MAAA,CAAO,OAAO,MAAA,CAAO,OAAO,qCAAqC,KAAA,CAAM;;;;;mBAM3D,eAAe,SAAA,CAAU;;cAC3C;;;;cASY,oBAAA,SAA6B,yBAAA;cAqU7B;;yBA9OwB,SAAA,CAAU,sBACrC,wBAAwB,aAC/B,KAAA,CAAM,MACP,6BAEA,iBAAiB,gBAAgB"}
@@ -151,6 +151,9 @@ const make = (config) => {
151
151
  getSnapshot: (documentId) => Effect.gen(function* () {
152
152
  return (yield* getOrCreateDocument(documentId)).getSnapshot();
153
153
  }),
154
+ getTreeSnapshot: (documentId) => Effect.gen(function* () {
155
+ return (yield* getOrCreateDocument(documentId)).toSnapshot();
156
+ }),
154
157
  subscribe: (documentId) => Effect.gen(function* () {
155
158
  const instance = yield* getOrCreateDocument(documentId);
156
159
  return Stream.fromPubSub(instance.pubsub);
@@ -1 +1 @@
1
- {"version":3,"file":"MimicServerEngine.mjs","names":["Metrics.documentsEvicted","Metrics.documentsActive","Metrics.storageIdleSnapshots","presenceManagerLayer"],"sources":["../src/MimicServerEngine.ts"],"sourcesContent":["/**\n * @voidhash/mimic-effect - MimicServerEngine\n *\n * Core document management service for Mimic real-time collaboration.\n * Handles document lifecycle, storage, presence, and transaction processing.\n *\n * This is the engine layer - for WebSocket routes, use MimicServer.layerHttpLayerRouter().\n */\nimport {\n Context,\n Duration,\n Effect,\n HashMap,\n Layer,\n Metric,\n Ref,\n Schedule,\n Scope,\n Stream,\n} from \"effect\";\nimport type { Primitive, Transaction } from \"@voidhash/mimic\";\nimport type {\n MimicServerEngineConfig,\n PresenceEntry,\n PresenceEvent,\n PresenceSnapshot,\n ResolvedConfig,\n} from \"./Types\";\nimport type * as Protocol from \"./Protocol\";\nimport { ColdStorageTag } from \"./ColdStorage\";\nimport { HotStorageTag } from \"./HotStorage\";\nimport { MimicAuthServiceTag } from \"./MimicAuthService\";\nimport {\n DocumentInstance,\n type SubmitResult,\n type DocumentInstance as DocumentInstanceType,\n} from \"./DocumentInstance\";\nimport {\n PresenceManagerTag,\n layer as presenceManagerLayer,\n} from \"./PresenceManager\";\nimport * as Metrics from \"./Metrics\";\nimport type { ColdStorageError, HotStorageError } from \"./Errors\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/**\n * Error type for MimicServerEngine operations\n */\nexport type MimicServerEngineError = ColdStorageError | HotStorageError;\n\n// =============================================================================\n// MimicServerEngine Interface\n// =============================================================================\n\n/**\n * MimicServerEngine service interface.\n *\n * Provides document management operations for Mimic collaboration.\n * Use MimicServer.layerHttpLayerRouter() to create WebSocket routes.\n */\nexport interface MimicServerEngine {\n /**\n * Submit a transaction to a document.\n * Authorization is checked against the auth service.\n * May fail with MimicServerEngineError if storage is unavailable.\n */\n readonly submit: (\n documentId: string,\n transaction: Transaction.Transaction\n ) => Effect.Effect<SubmitResult, MimicServerEngineError>;\n\n /**\n * Get document snapshot (current state and version).\n * May fail with MimicServerEngineError if storage is unavailable.\n */\n readonly getSnapshot: (\n documentId: string\n ) => Effect.Effect<{ state: unknown; version: number }, MimicServerEngineError>;\n\n /**\n * Subscribe to document broadcasts (transactions).\n * Returns a stream of server messages.\n * Requires a Scope for cleanup when the subscription ends.\n * May fail with MimicServerEngineError if storage is unavailable.\n */\n readonly subscribe: (\n documentId: string\n ) => Effect.Effect<Stream.Stream<Protocol.ServerMessage, never, never>, MimicServerEngineError, Scope.Scope>;\n\n /**\n * Touch document to prevent idle garbage collection.\n */\n readonly touch: (documentId: string) => Effect.Effect<void, never>;\n\n /**\n * Get presence snapshot for a document.\n */\n readonly getPresenceSnapshot: (\n documentId: string\n ) => Effect.Effect<PresenceSnapshot, never>;\n\n /**\n * Set presence for a connection.\n */\n readonly setPresence: (\n documentId: string,\n connectionId: string,\n entry: PresenceEntry\n ) => Effect.Effect<void, never>;\n\n /**\n * Remove presence for a connection.\n */\n readonly removePresence: (\n documentId: string,\n connectionId: string\n ) => Effect.Effect<void, never>;\n\n /**\n * Subscribe to presence events for a document.\n * Requires a Scope for cleanup when the subscription ends.\n */\n readonly subscribePresence: (\n documentId: string\n ) => Effect.Effect<Stream.Stream<PresenceEvent, never, never>, never, Scope.Scope>;\n\n /**\n * Resolved engine configuration.\n * Used by route layer to access schema, presence config, etc.\n */\n readonly config: ResolvedConfig<Primitive.AnyPrimitive>;\n}\n\n// =============================================================================\n// Context Tag\n// =============================================================================\n\n/**\n * Context tag for MimicServerEngine\n */\nexport class MimicServerEngineTag extends Context.Tag(\n \"@voidhash/mimic-effect/MimicServerEngine\"\n)<MimicServerEngineTag, MimicServerEngine>() {}\n\n// =============================================================================\n// Default Configuration\n// =============================================================================\n\nconst DEFAULT_MAX_IDLE_TIME = Duration.minutes(5);\nconst DEFAULT_MAX_TRANSACTION_HISTORY = 1000;\nconst DEFAULT_SNAPSHOT_INTERVAL = Duration.minutes(5);\nconst DEFAULT_SNAPSHOT_THRESHOLD = 100;\nconst DEFAULT_SNAPSHOT_IDLE_TIMEOUT = Duration.seconds(30);\n\n/**\n * Resolve configuration with defaults\n */\nconst resolveConfig = <TSchema extends Primitive.AnyPrimitive>(\n config: MimicServerEngineConfig<TSchema>\n): ResolvedConfig<TSchema> => ({\n schema: config.schema,\n initial: config.initial,\n presence: config.presence,\n maxIdleTime: config.maxIdleTime\n ? Duration.decode(config.maxIdleTime)\n : DEFAULT_MAX_IDLE_TIME,\n maxTransactionHistory:\n config.maxTransactionHistory ?? DEFAULT_MAX_TRANSACTION_HISTORY,\n snapshot: {\n interval: config.snapshot?.interval\n ? Duration.decode(config.snapshot.interval)\n : DEFAULT_SNAPSHOT_INTERVAL,\n transactionThreshold:\n config.snapshot?.transactionThreshold ?? DEFAULT_SNAPSHOT_THRESHOLD,\n idleTimeout: config.snapshot?.idleTimeout\n ? Duration.decode(config.snapshot.idleTimeout)\n : DEFAULT_SNAPSHOT_IDLE_TIMEOUT,\n },\n});\n\n// =============================================================================\n// Internal Types\n// =============================================================================\n\n/**\n * Store entry for a document instance with last activity time\n */\ninterface StoreEntry<TSchema extends Primitive.AnyPrimitive> {\n readonly instance: DocumentInstanceType<TSchema>;\n readonly lastActivityTime: Ref.Ref<number>;\n}\n\n// =============================================================================\n// Factory\n// =============================================================================\n\n/**\n * Create a MimicServerEngine layer.\n *\n * This creates the core document management service. To expose it via WebSocket,\n * use MimicServer.layerHttpLayerRouter().\n *\n * @example\n * ```typescript\n * // 1. Create the engine\n * const Engine = MimicServerEngine.make({\n * schema: DocSchema,\n * initial: { title: \"Untitled\" },\n * presence: CursorPresence,\n * maxIdleTime: \"5 minutes\",\n * snapshot: { interval: \"5 minutes\", transactionThreshold: 100 },\n * })\n *\n * // 2. Create the WebSocket route\n * const MimicRoute = MimicServer.layerHttpLayerRouter({\n * path: \"/mimic\",\n * })\n *\n * // 3. Wire together\n * const MimicLive = MimicRoute.pipe(\n * Layer.provide(Engine),\n * Layer.provide(ColdStorage.InMemory.make()),\n * Layer.provide(HotStorage.InMemory.make()),\n * Layer.provide(MimicAuthService.NoAuth.make()),\n * )\n * ```\n */\nexport const make = <TSchema extends Primitive.AnyPrimitive>(\n config: MimicServerEngineConfig<TSchema>\n): Layer.Layer<\n MimicServerEngineTag,\n never,\n ColdStorageTag | HotStorageTag | MimicAuthServiceTag\n> => {\n const resolvedConfig = resolveConfig(config);\n\n return Layer.scoped(\n MimicServerEngineTag,\n Effect.gen(function* () {\n const coldStorage = yield* ColdStorageTag;\n const hotStorage = yield* HotStorageTag;\n const presenceManager = yield* PresenceManagerTag;\n\n // Store: documentId -> StoreEntry\n const store = yield* Ref.make(\n HashMap.empty<string, StoreEntry<TSchema>>()\n );\n\n /**\n * Get or create a document instance\n */\n const getOrCreateDocument = Effect.fn(\"engine.document.get-or-create\")(\n function* (documentId: string) {\n const current = yield* Ref.get(store);\n const existing = HashMap.get(current, documentId);\n\n if (existing._tag === \"Some\") {\n // Update activity time\n yield* Ref.set(existing.value.lastActivityTime, Date.now());\n return existing.value.instance;\n }\n\n // Create new document instance\n const instance = yield* DocumentInstance.make(\n documentId,\n {\n schema: config.schema,\n initial: config.initial,\n maxTransactionHistory: resolvedConfig.maxTransactionHistory,\n snapshot: resolvedConfig.snapshot,\n },\n coldStorage,\n hotStorage\n );\n\n const lastActivityTime = yield* Ref.make(Date.now());\n\n // Store it\n yield* Ref.update(store, (map) =>\n HashMap.set(map, documentId, { instance, lastActivityTime })\n );\n\n return instance;\n }\n );\n\n /**\n * Start background GC fiber\n */\n const startGCFiber = Effect.fn(\"engine.gc.start\")(function* () {\n const gcLoop = Effect.fn(\"engine.gc.loop\")(function* () {\n const current = yield* Ref.get(store);\n const now = Date.now();\n const maxIdleMs = Duration.toMillis(resolvedConfig.maxIdleTime);\n\n for (const [documentId, entry] of current) {\n const lastActivity = yield* Ref.get(entry.lastActivityTime);\n if (now - lastActivity >= maxIdleMs) {\n // Save final snapshot before eviction (best effort)\n yield* Effect.catchAll(entry.instance.saveSnapshot(), (e) =>\n Effect.logError(\"Failed to save snapshot during eviction\", {\n documentId,\n error: e,\n })\n );\n\n // Remove from store\n yield* Ref.update(store, (map) => HashMap.remove(map, documentId));\n\n // Track eviction metrics\n yield* Metric.increment(Metrics.documentsEvicted);\n yield* Metric.incrementBy(Metrics.documentsActive, -1);\n\n yield* Effect.logInfo(\"Document evicted due to idle timeout\", {\n documentId,\n });\n }\n }\n });\n\n // Run GC every minute\n yield* gcLoop().pipe(\n Effect.repeat(Schedule.spaced(\"1 minute\")),\n Effect.fork\n );\n });\n\n // Start GC fiber\n yield* startGCFiber();\n\n /**\n * Start background snapshot fiber for idle documents.\n * This ensures documents with unsnapshot transactions get persisted\n * even without new transaction activity.\n */\n const startSnapshotFiber = Effect.fn(\"engine.snapshot.fiber.start\")(function* () {\n const idleTimeoutMs = Duration.toMillis(resolvedConfig.snapshot.idleTimeout);\n\n // Skip if idle snapshots are disabled\n if (idleTimeoutMs <= 0) {\n return;\n }\n\n const snapshotLoop = Effect.fn(\"engine.snapshot.loop\")(function* () {\n const current = yield* Ref.get(store);\n const now = Date.now();\n\n for (const [documentId, entry] of current) {\n // Check if document has been idle long enough\n const lastActivity = yield* Ref.get(entry.lastActivityTime);\n const idleDuration = now - lastActivity;\n\n if (idleDuration < idleTimeoutMs) {\n // Document not idle long enough, skip\n continue;\n }\n\n // Check if document has unsnapshot transactions\n const needs = yield* entry.instance.needsSnapshot();\n if (!needs) {\n continue;\n }\n\n // Save snapshot (with error handling)\n yield* Effect.catchAll(entry.instance.saveSnapshot(), (e) =>\n Effect.logWarning(\"Periodic snapshot save failed\", {\n documentId,\n error: e,\n })\n );\n\n // Track metric\n yield* Metric.increment(Metrics.storageIdleSnapshots);\n }\n });\n\n // Run snapshot check every 10 seconds\n yield* snapshotLoop().pipe(\n Effect.repeat(Schedule.spaced(\"10 seconds\")),\n Effect.fork\n );\n });\n\n // Start snapshot fiber\n yield* startSnapshotFiber();\n\n // Cleanup on shutdown\n yield* Effect.addFinalizer(() =>\n Effect.fn(\"engine.shutdown\")(function* () {\n const current = yield* Ref.get(store);\n for (const [documentId, entry] of current) {\n // Best effort save - don't fail shutdown if storage is unavailable\n yield* Effect.catchAll(entry.instance.saveSnapshot(), (e) =>\n Effect.logError(\"Failed to save snapshot during shutdown\", {\n documentId,\n error: e,\n })\n );\n }\n yield* Effect.logInfo(\"MimicServerEngine shutdown complete\");\n })()\n );\n\n const engine: MimicServerEngine = {\n submit: (documentId, transaction) =>\n Effect.gen(function* () {\n const instance = yield* getOrCreateDocument(documentId);\n return yield* instance.submit(transaction);\n }),\n\n getSnapshot: (documentId) =>\n Effect.gen(function* () {\n const instance = yield* getOrCreateDocument(documentId);\n return instance.getSnapshot();\n }),\n\n subscribe: (documentId) =>\n Effect.gen(function* () {\n const instance = yield* getOrCreateDocument(documentId);\n return Stream.fromPubSub(instance.pubsub) as Stream.Stream<\n Protocol.ServerMessage,\n never,\n never\n >;\n }),\n\n touch: (documentId) =>\n Effect.gen(function* () {\n const current = yield* Ref.get(store);\n const existing = HashMap.get(current, documentId);\n if (existing._tag === \"Some\") {\n yield* Ref.set(existing.value.lastActivityTime, Date.now());\n }\n }),\n\n getPresenceSnapshot: (documentId) =>\n presenceManager.getSnapshot(documentId),\n\n setPresence: (documentId, connectionId, entry) =>\n presenceManager.set(documentId, connectionId, entry),\n\n removePresence: (documentId, connectionId) =>\n presenceManager.remove(documentId, connectionId),\n\n subscribePresence: (documentId) =>\n presenceManager.subscribe(documentId),\n\n config: resolvedConfig as ResolvedConfig<Primitive.AnyPrimitive>,\n };\n\n return engine;\n })\n ).pipe(Layer.provide(presenceManagerLayer));\n};\n\n// =============================================================================\n// Re-export namespace\n// =============================================================================\n\nexport const MimicServerEngine = {\n Tag: MimicServerEngineTag,\n make,\n};\n\n// =============================================================================\n// Re-export SubmitResult type\n// =============================================================================\n\nexport type { SubmitResult };\n"],"mappings":"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"}
1
+ {"version":3,"file":"MimicServerEngine.mjs","names":["Metrics.documentsEvicted","Metrics.documentsActive","Metrics.storageIdleSnapshots","presenceManagerLayer"],"sources":["../src/MimicServerEngine.ts"],"sourcesContent":["/**\n * @voidhash/mimic-effect - MimicServerEngine\n *\n * Core document management service for Mimic real-time collaboration.\n * Handles document lifecycle, storage, presence, and transaction processing.\n *\n * This is the engine layer - for WebSocket routes, use MimicServer.layerHttpLayerRouter().\n */\nimport {\n Context,\n Duration,\n Effect,\n HashMap,\n Layer,\n Metric,\n Ref,\n Schedule,\n Scope,\n Stream,\n} from \"effect\";\nimport type { Primitive, Transaction } from \"@voidhash/mimic\";\nimport type {\n MimicServerEngineConfig,\n PresenceEntry,\n PresenceEvent,\n PresenceSnapshot,\n ResolvedConfig,\n} from \"./Types\";\nimport type * as Protocol from \"./Protocol\";\nimport { ColdStorageTag } from \"./ColdStorage\";\nimport { HotStorageTag } from \"./HotStorage\";\nimport { MimicAuthServiceTag } from \"./MimicAuthService\";\nimport {\n DocumentInstance,\n type SubmitResult,\n type DocumentInstance as DocumentInstanceType,\n} from \"./DocumentInstance\";\nimport {\n PresenceManagerTag,\n layer as presenceManagerLayer,\n} from \"./PresenceManager\";\nimport * as Metrics from \"./Metrics\";\nimport type { ColdStorageError, HotStorageError } from \"./Errors\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/**\n * Error type for MimicServerEngine operations\n */\nexport type MimicServerEngineError = ColdStorageError | HotStorageError;\n\n// =============================================================================\n// MimicServerEngine Interface\n// =============================================================================\n\n/**\n * MimicServerEngine service interface.\n *\n * Provides document management operations for Mimic collaboration.\n * Use MimicServer.layerHttpLayerRouter() to create WebSocket routes.\n */\nexport interface MimicServerEngine {\n /**\n * Submit a transaction to a document.\n * Authorization is checked against the auth service.\n * May fail with MimicServerEngineError if storage is unavailable.\n */\n readonly submit: (\n documentId: string,\n transaction: Transaction.Transaction\n ) => Effect.Effect<SubmitResult, MimicServerEngineError>;\n\n /**\n * Get document snapshot (current state and version).\n * Returns flat state format used for storage and synchronization.\n * May fail with MimicServerEngineError if storage is unavailable.\n */\n readonly getSnapshot: (\n documentId: string\n ) => Effect.Effect<{ state: unknown; version: number }, MimicServerEngineError>;\n\n /**\n * Get tree-like snapshot for rendering.\n * Returns a readonly structure where trees are converted from\n * flat state to nested/hierarchical structure suitable for UI rendering.\n * May fail with MimicServerEngineError if storage is unavailable.\n */\n readonly getTreeSnapshot: (\n documentId: string\n ) => Effect.Effect<unknown, MimicServerEngineError>;\n\n /**\n * Subscribe to document broadcasts (transactions).\n * Returns a stream of server messages.\n * Requires a Scope for cleanup when the subscription ends.\n * May fail with MimicServerEngineError if storage is unavailable.\n */\n readonly subscribe: (\n documentId: string\n ) => Effect.Effect<Stream.Stream<Protocol.ServerMessage, never, never>, MimicServerEngineError, Scope.Scope>;\n\n /**\n * Touch document to prevent idle garbage collection.\n */\n readonly touch: (documentId: string) => Effect.Effect<void, never>;\n\n /**\n * Get presence snapshot for a document.\n */\n readonly getPresenceSnapshot: (\n documentId: string\n ) => Effect.Effect<PresenceSnapshot, never>;\n\n /**\n * Set presence for a connection.\n */\n readonly setPresence: (\n documentId: string,\n connectionId: string,\n entry: PresenceEntry\n ) => Effect.Effect<void, never>;\n\n /**\n * Remove presence for a connection.\n */\n readonly removePresence: (\n documentId: string,\n connectionId: string\n ) => Effect.Effect<void, never>;\n\n /**\n * Subscribe to presence events for a document.\n * Requires a Scope for cleanup when the subscription ends.\n */\n readonly subscribePresence: (\n documentId: string\n ) => Effect.Effect<Stream.Stream<PresenceEvent, never, never>, never, Scope.Scope>;\n\n /**\n * Resolved engine configuration.\n * Used by route layer to access schema, presence config, etc.\n */\n readonly config: ResolvedConfig<Primitive.AnyPrimitive>;\n}\n\n// =============================================================================\n// Context Tag\n// =============================================================================\n\n/**\n * Context tag for MimicServerEngine\n */\nexport class MimicServerEngineTag extends Context.Tag(\n \"@voidhash/mimic-effect/MimicServerEngine\"\n)<MimicServerEngineTag, MimicServerEngine>() {}\n\n// =============================================================================\n// Default Configuration\n// =============================================================================\n\nconst DEFAULT_MAX_IDLE_TIME = Duration.minutes(5);\nconst DEFAULT_MAX_TRANSACTION_HISTORY = 1000;\nconst DEFAULT_SNAPSHOT_INTERVAL = Duration.minutes(5);\nconst DEFAULT_SNAPSHOT_THRESHOLD = 100;\nconst DEFAULT_SNAPSHOT_IDLE_TIMEOUT = Duration.seconds(30);\n\n/**\n * Resolve configuration with defaults\n */\nconst resolveConfig = <TSchema extends Primitive.AnyPrimitive>(\n config: MimicServerEngineConfig<TSchema>\n): ResolvedConfig<TSchema> => ({\n schema: config.schema,\n initial: config.initial,\n presence: config.presence,\n maxIdleTime: config.maxIdleTime\n ? Duration.decode(config.maxIdleTime)\n : DEFAULT_MAX_IDLE_TIME,\n maxTransactionHistory:\n config.maxTransactionHistory ?? DEFAULT_MAX_TRANSACTION_HISTORY,\n snapshot: {\n interval: config.snapshot?.interval\n ? Duration.decode(config.snapshot.interval)\n : DEFAULT_SNAPSHOT_INTERVAL,\n transactionThreshold:\n config.snapshot?.transactionThreshold ?? DEFAULT_SNAPSHOT_THRESHOLD,\n idleTimeout: config.snapshot?.idleTimeout\n ? Duration.decode(config.snapshot.idleTimeout)\n : DEFAULT_SNAPSHOT_IDLE_TIMEOUT,\n },\n});\n\n// =============================================================================\n// Internal Types\n// =============================================================================\n\n/**\n * Store entry for a document instance with last activity time\n */\ninterface StoreEntry<TSchema extends Primitive.AnyPrimitive> {\n readonly instance: DocumentInstanceType<TSchema>;\n readonly lastActivityTime: Ref.Ref<number>;\n}\n\n// =============================================================================\n// Factory\n// =============================================================================\n\n/**\n * Create a MimicServerEngine layer.\n *\n * This creates the core document management service. To expose it via WebSocket,\n * use MimicServer.layerHttpLayerRouter().\n *\n * @example\n * ```typescript\n * // 1. Create the engine\n * const Engine = MimicServerEngine.make({\n * schema: DocSchema,\n * initial: { title: \"Untitled\" },\n * presence: CursorPresence,\n * maxIdleTime: \"5 minutes\",\n * snapshot: { interval: \"5 minutes\", transactionThreshold: 100 },\n * })\n *\n * // 2. Create the WebSocket route\n * const MimicRoute = MimicServer.layerHttpLayerRouter({\n * path: \"/mimic\",\n * })\n *\n * // 3. Wire together\n * const MimicLive = MimicRoute.pipe(\n * Layer.provide(Engine),\n * Layer.provide(ColdStorage.InMemory.make()),\n * Layer.provide(HotStorage.InMemory.make()),\n * Layer.provide(MimicAuthService.NoAuth.make()),\n * )\n * ```\n */\nexport const make = <TSchema extends Primitive.AnyPrimitive>(\n config: MimicServerEngineConfig<TSchema>\n): Layer.Layer<\n MimicServerEngineTag,\n never,\n ColdStorageTag | HotStorageTag | MimicAuthServiceTag\n> => {\n const resolvedConfig = resolveConfig(config);\n\n return Layer.scoped(\n MimicServerEngineTag,\n Effect.gen(function* () {\n const coldStorage = yield* ColdStorageTag;\n const hotStorage = yield* HotStorageTag;\n const presenceManager = yield* PresenceManagerTag;\n\n // Store: documentId -> StoreEntry\n const store = yield* Ref.make(\n HashMap.empty<string, StoreEntry<TSchema>>()\n );\n\n /**\n * Get or create a document instance\n */\n const getOrCreateDocument = Effect.fn(\"engine.document.get-or-create\")(\n function* (documentId: string) {\n const current = yield* Ref.get(store);\n const existing = HashMap.get(current, documentId);\n\n if (existing._tag === \"Some\") {\n // Update activity time\n yield* Ref.set(existing.value.lastActivityTime, Date.now());\n return existing.value.instance;\n }\n\n // Create new document instance\n const instance = yield* DocumentInstance.make(\n documentId,\n {\n schema: config.schema,\n initial: config.initial,\n maxTransactionHistory: resolvedConfig.maxTransactionHistory,\n snapshot: resolvedConfig.snapshot,\n },\n coldStorage,\n hotStorage\n );\n\n const lastActivityTime = yield* Ref.make(Date.now());\n\n // Store it\n yield* Ref.update(store, (map) =>\n HashMap.set(map, documentId, { instance, lastActivityTime })\n );\n\n return instance;\n }\n );\n\n /**\n * Start background GC fiber\n */\n const startGCFiber = Effect.fn(\"engine.gc.start\")(function* () {\n const gcLoop = Effect.fn(\"engine.gc.loop\")(function* () {\n const current = yield* Ref.get(store);\n const now = Date.now();\n const maxIdleMs = Duration.toMillis(resolvedConfig.maxIdleTime);\n\n for (const [documentId, entry] of current) {\n const lastActivity = yield* Ref.get(entry.lastActivityTime);\n if (now - lastActivity >= maxIdleMs) {\n // Save final snapshot before eviction (best effort)\n yield* Effect.catchAll(entry.instance.saveSnapshot(), (e) =>\n Effect.logError(\"Failed to save snapshot during eviction\", {\n documentId,\n error: e,\n })\n );\n\n // Remove from store\n yield* Ref.update(store, (map) => HashMap.remove(map, documentId));\n\n // Track eviction metrics\n yield* Metric.increment(Metrics.documentsEvicted);\n yield* Metric.incrementBy(Metrics.documentsActive, -1);\n\n yield* Effect.logInfo(\"Document evicted due to idle timeout\", {\n documentId,\n });\n }\n }\n });\n\n // Run GC every minute\n yield* gcLoop().pipe(\n Effect.repeat(Schedule.spaced(\"1 minute\")),\n Effect.fork\n );\n });\n\n // Start GC fiber\n yield* startGCFiber();\n\n /**\n * Start background snapshot fiber for idle documents.\n * This ensures documents with unsnapshot transactions get persisted\n * even without new transaction activity.\n */\n const startSnapshotFiber = Effect.fn(\"engine.snapshot.fiber.start\")(function* () {\n const idleTimeoutMs = Duration.toMillis(resolvedConfig.snapshot.idleTimeout);\n\n // Skip if idle snapshots are disabled\n if (idleTimeoutMs <= 0) {\n return;\n }\n\n const snapshotLoop = Effect.fn(\"engine.snapshot.loop\")(function* () {\n const current = yield* Ref.get(store);\n const now = Date.now();\n\n for (const [documentId, entry] of current) {\n // Check if document has been idle long enough\n const lastActivity = yield* Ref.get(entry.lastActivityTime);\n const idleDuration = now - lastActivity;\n\n if (idleDuration < idleTimeoutMs) {\n // Document not idle long enough, skip\n continue;\n }\n\n // Check if document has unsnapshot transactions\n const needs = yield* entry.instance.needsSnapshot();\n if (!needs) {\n continue;\n }\n\n // Save snapshot (with error handling)\n yield* Effect.catchAll(entry.instance.saveSnapshot(), (e) =>\n Effect.logWarning(\"Periodic snapshot save failed\", {\n documentId,\n error: e,\n })\n );\n\n // Track metric\n yield* Metric.increment(Metrics.storageIdleSnapshots);\n }\n });\n\n // Run snapshot check every 10 seconds\n yield* snapshotLoop().pipe(\n Effect.repeat(Schedule.spaced(\"10 seconds\")),\n Effect.fork\n );\n });\n\n // Start snapshot fiber\n yield* startSnapshotFiber();\n\n // Cleanup on shutdown\n yield* Effect.addFinalizer(() =>\n Effect.fn(\"engine.shutdown\")(function* () {\n const current = yield* Ref.get(store);\n for (const [documentId, entry] of current) {\n // Best effort save - don't fail shutdown if storage is unavailable\n yield* Effect.catchAll(entry.instance.saveSnapshot(), (e) =>\n Effect.logError(\"Failed to save snapshot during shutdown\", {\n documentId,\n error: e,\n })\n );\n }\n yield* Effect.logInfo(\"MimicServerEngine shutdown complete\");\n })()\n );\n\n const engine: MimicServerEngine = {\n submit: (documentId, transaction) =>\n Effect.gen(function* () {\n const instance = yield* getOrCreateDocument(documentId);\n return yield* instance.submit(transaction);\n }),\n\n getSnapshot: (documentId) =>\n Effect.gen(function* () {\n const instance = yield* getOrCreateDocument(documentId);\n return instance.getSnapshot();\n }),\n\n getTreeSnapshot: (documentId) =>\n Effect.gen(function* () {\n const instance = yield* getOrCreateDocument(documentId);\n return instance.toSnapshot();\n }),\n\n subscribe: (documentId) =>\n Effect.gen(function* () {\n const instance = yield* getOrCreateDocument(documentId);\n return Stream.fromPubSub(instance.pubsub) as Stream.Stream<\n Protocol.ServerMessage,\n never,\n never\n >;\n }),\n\n touch: (documentId) =>\n Effect.gen(function* () {\n const current = yield* Ref.get(store);\n const existing = HashMap.get(current, documentId);\n if (existing._tag === \"Some\") {\n yield* Ref.set(existing.value.lastActivityTime, Date.now());\n }\n }),\n\n getPresenceSnapshot: (documentId) =>\n presenceManager.getSnapshot(documentId),\n\n setPresence: (documentId, connectionId, entry) =>\n presenceManager.set(documentId, connectionId, entry),\n\n removePresence: (documentId, connectionId) =>\n presenceManager.remove(documentId, connectionId),\n\n subscribePresence: (documentId) =>\n presenceManager.subscribe(documentId),\n\n config: resolvedConfig as ResolvedConfig<Primitive.AnyPrimitive>,\n };\n\n return engine;\n })\n ).pipe(Layer.provide(presenceManagerLayer));\n};\n\n// =============================================================================\n// Re-export namespace\n// =============================================================================\n\nexport const MimicServerEngine = {\n Tag: MimicServerEngineTag,\n make,\n};\n\n// =============================================================================\n// Re-export SubmitResult type\n// =============================================================================\n\nexport type { SubmitResult };\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA0JA,IAAa,uBAAb,cAA0C,QAAQ,IAChD,2CACD,EAA2C,CAAC;AAM7C,MAAM,wBAAwB,SAAS,QAAQ,EAAE;AACjD,MAAM,kCAAkC;AACxC,MAAM,4BAA4B,SAAS,QAAQ,EAAE;AACrD,MAAM,6BAA6B;AACnC,MAAM,gCAAgC,SAAS,QAAQ,GAAG;;;;AAK1D,MAAM,iBACJ,WAC4B;;QAAC;EAC7B,QAAQ,OAAO;EACf,SAAS,OAAO;EAChB,UAAU,OAAO;EACjB,aAAa,OAAO,cAChB,SAAS,OAAO,OAAO,YAAY,GACnC;EACJ,gDACE,OAAO,8FAAyB;EAClC,UAAU;GACR,+BAAU,OAAO,8EAAU,YACvB,SAAS,OAAO,OAAO,SAAS,SAAS,GACzC;GACJ,oEACE,OAAO,gFAAU,6FAAwB;GAC3C,mCAAa,OAAO,gFAAU,eAC1B,SAAS,OAAO,OAAO,SAAS,YAAY,GAC5C;GACL;EACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiDD,MAAa,QACX,WAKG;CACH,MAAM,iBAAiB,cAAc,OAAO;AAE5C,QAAO,MAAM,OACX,sBACA,OAAO,IAAI,aAAa;EACtB,MAAM,cAAc,OAAO;EAC3B,MAAM,aAAa,OAAO;EAC1B,MAAM,kBAAkB,OAAO;EAG/B,MAAM,QAAQ,OAAO,IAAI,KACvB,QAAQ,OAAoC,CAC7C;;;;EAKD,MAAM,sBAAsB,OAAO,GAAG,gCAAgC,CACpE,WAAW,YAAoB;GAC7B,MAAM,UAAU,OAAO,IAAI,IAAI,MAAM;GACrC,MAAM,WAAW,QAAQ,IAAI,SAAS,WAAW;AAEjD,OAAI,SAAS,SAAS,QAAQ;AAE5B,WAAO,IAAI,IAAI,SAAS,MAAM,kBAAkB,KAAK,KAAK,CAAC;AAC3D,WAAO,SAAS,MAAM;;GAIxB,MAAM,WAAW,OAAO,iBAAiB,KACvC,YACA;IACE,QAAQ,OAAO;IACf,SAAS,OAAO;IAChB,uBAAuB,eAAe;IACtC,UAAU,eAAe;IAC1B,EACD,aACA,WACD;GAED,MAAM,mBAAmB,OAAO,IAAI,KAAK,KAAK,KAAK,CAAC;AAGpD,UAAO,IAAI,OAAO,QAAQ,QACxB,QAAQ,IAAI,KAAK,YAAY;IAAE;IAAU;IAAkB,CAAC,CAC7D;AAED,UAAO;IAEV;AA4CD,SAvCqB,OAAO,GAAG,kBAAkB,CAAC,aAAa;AAgC7D,UA/Be,OAAO,GAAG,iBAAiB,CAAC,aAAa;IACtD,MAAM,UAAU,OAAO,IAAI,IAAI,MAAM;IACrC,MAAM,MAAM,KAAK,KAAK;IACtB,MAAM,YAAY,SAAS,SAAS,eAAe,YAAY;AAE/D,SAAK,MAAM,CAAC,YAAY,UAAU,QAEhC,KAAI,OADiB,OAAO,IAAI,IAAI,MAAM,iBAAiB,KACjC,WAAW;AAEnC,YAAO,OAAO,SAAS,MAAM,SAAS,cAAc,GAAG,MACrD,OAAO,SAAS,2CAA2C;MACzD;MACA,OAAO;MACR,CAAC,CACH;AAGD,YAAO,IAAI,OAAO,QAAQ,QAAQ,QAAQ,OAAO,KAAK,WAAW,CAAC;AAGlE,YAAO,OAAO,UAAUA,iBAAyB;AACjD,YAAO,OAAO,YAAYC,iBAAyB,GAAG;AAEtD,YAAO,OAAO,QAAQ,wCAAwC,EAC5D,YACD,CAAC;;KAGN,EAGa,CAAC,KACd,OAAO,OAAO,SAAS,OAAO,WAAW,CAAC,EAC1C,OAAO,KACR;IACD,EAGmB;AAwDrB,SAjD2B,OAAO,GAAG,8BAA8B,CAAC,aAAa;GAC/E,MAAM,gBAAgB,SAAS,SAAS,eAAe,SAAS,YAAY;AAG5E,OAAI,iBAAiB,EACnB;AAqCF,UAlCqB,OAAO,GAAG,uBAAuB,CAAC,aAAa;IAClE,MAAM,UAAU,OAAO,IAAI,IAAI,MAAM;IACrC,MAAM,MAAM,KAAK,KAAK;AAEtB,SAAK,MAAM,CAAC,YAAY,UAAU,SAAS;AAKzC,SAFqB,OADA,OAAO,IAAI,IAAI,MAAM,iBAAiB,IAGxC,cAEjB;AAKF,SAAI,EADU,OAAO,MAAM,SAAS,eAAe,EAEjD;AAIF,YAAO,OAAO,SAAS,MAAM,SAAS,cAAc,GAAG,MACrD,OAAO,WAAW,iCAAiC;MACjD;MACA,OAAO;MACR,CAAC,CACH;AAGD,YAAO,OAAO,UAAUC,qBAA6B;;KAEvD,EAGmB,CAAC,KACpB,OAAO,OAAO,SAAS,OAAO,aAAa,CAAC,EAC5C,OAAO,KACR;IACD,EAGyB;AAG3B,SAAO,OAAO,mBACZ,OAAO,GAAG,kBAAkB,CAAC,aAAa;GACxC,MAAM,UAAU,OAAO,IAAI,IAAI,MAAM;AACrC,QAAK,MAAM,CAAC,YAAY,UAAU,QAEhC,QAAO,OAAO,SAAS,MAAM,SAAS,cAAc,GAAG,MACrD,OAAO,SAAS,2CAA2C;IACzD;IACA,OAAO;IACR,CAAC,CACH;AAEH,UAAO,OAAO,QAAQ,sCAAsC;IAC5D,EAAE,CACL;AAuDD,SArDkC;GAChC,SAAS,YAAY,gBACnB,OAAO,IAAI,aAAa;AAEtB,WAAO,QADU,OAAO,oBAAoB,WAAW,EAChC,OAAO,YAAY;KAC1C;GAEJ,cAAc,eACZ,OAAO,IAAI,aAAa;AAEtB,YADiB,OAAO,oBAAoB,WAAW,EACvC,aAAa;KAC7B;GAEJ,kBAAkB,eAChB,OAAO,IAAI,aAAa;AAEtB,YADiB,OAAO,oBAAoB,WAAW,EACvC,YAAY;KAC5B;GAEJ,YAAY,eACV,OAAO,IAAI,aAAa;IACtB,MAAM,WAAW,OAAO,oBAAoB,WAAW;AACvD,WAAO,OAAO,WAAW,SAAS,OAAO;KAKzC;GAEJ,QAAQ,eACN,OAAO,IAAI,aAAa;IACtB,MAAM,UAAU,OAAO,IAAI,IAAI,MAAM;IACrC,MAAM,WAAW,QAAQ,IAAI,SAAS,WAAW;AACjD,QAAI,SAAS,SAAS,OACpB,QAAO,IAAI,IAAI,SAAS,MAAM,kBAAkB,KAAK,KAAK,CAAC;KAE7D;GAEJ,sBAAsB,eACpB,gBAAgB,YAAY,WAAW;GAEzC,cAAc,YAAY,cAAc,UACtC,gBAAgB,IAAI,YAAY,cAAc,MAAM;GAEtD,iBAAiB,YAAY,iBAC3B,gBAAgB,OAAO,YAAY,aAAa;GAElD,oBAAoB,eAClB,gBAAgB,UAAU,WAAW;GAEvC,QAAQ;GACT;GAGD,CACH,CAAC,KAAK,MAAM,QAAQC,MAAqB,CAAC;;AAO7C,MAAa,oBAAoB;CAC/B,KAAK;CACL;CACD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voidhash/mimic-effect",
3
- "version": "1.0.0-beta.11",
3
+ "version": "1.0.0-beta.13",
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.11"
29
+ "@voidhash/tsconfig": "1.0.0-beta.13"
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.11"
36
+ "@voidhash/mimic": "1.0.0-beta.13"
37
37
  },
38
38
  "peerDependenciesMeta": {
39
39
  "@effect/cluster": {
@@ -74,8 +74,14 @@ export interface DocumentInstance<TSchema extends Primitive.AnyPrimitive> {
74
74
  readonly touch: () => Effect.Effect<void>;
75
75
  /** Get current document version */
76
76
  readonly getVersion: () => number;
77
- /** Get document snapshot */
77
+ /** Get document snapshot (flat state format) */
78
78
  readonly getSnapshot: () => { state: unknown; version: number };
79
+ /**
80
+ * Get tree-like snapshot for rendering.
81
+ * The snapshot is a type-safe, readonly structure where trees
82
+ * are converted from flat state to nested/hierarchical structure.
83
+ */
84
+ readonly toSnapshot: () => Primitive.InferSnapshot<TSchema>;
79
85
  /** Check if document has unsnapshot transactions that need persisting */
80
86
  readonly needsSnapshot: () => Effect.Effect<boolean>;
81
87
  /** Get the last activity timestamp for idle detection */
@@ -367,6 +373,7 @@ export const make = <TSchema extends Primitive.AnyPrimitive>(
367
373
  touch,
368
374
  getVersion: () => document.getVersion(),
369
375
  getSnapshot: () => document.getSnapshot(),
376
+ toSnapshot: () => document.toSnapshot(),
370
377
  needsSnapshot,
371
378
  getLastActivityTime,
372
379
  };
@@ -135,11 +135,16 @@ const MimicDocumentEntity = Entity.make("MimicDocument", [
135
135
  success: SubmitResultSchema,
136
136
  }),
137
137
 
138
- // Get document snapshot
138
+ // Get document snapshot (flat state)
139
139
  Rpc.make("GetSnapshot", {
140
140
  success: SnapshotResponseSchema,
141
141
  }),
142
142
 
143
+ // Get tree-like snapshot for rendering
144
+ Rpc.make("GetTreeSnapshot", {
145
+ success: Schema.Unknown,
146
+ }),
147
+
143
148
  // Touch document to prevent idle GC
144
149
  Rpc.make("Touch", {
145
150
  success: Schema.Void,
@@ -343,6 +348,10 @@ const createEntityHandler = <TSchema extends Primitive.AnyPrimitive>(
343
348
  return instance.getSnapshot();
344
349
  }),
345
350
 
351
+ GetTreeSnapshot: Effect.fn("cluster.document.tree-snapshot.get")(function* () {
352
+ return instance.toSnapshot();
353
+ }),
354
+
346
355
  Touch: Effect.fn("cluster.document.touch")(function* () {
347
356
  yield* instance.touch();
348
357
  }),
@@ -590,6 +599,12 @@ export const make = <TSchema extends Primitive.AnyPrimitive>(
590
599
  return yield* client.GetSnapshot(undefined as void).pipe(Effect.orDie);
591
600
  }),
592
601
 
602
+ getTreeSnapshot: (documentId) =>
603
+ Effect.gen(function* () {
604
+ const client = makeClient(documentId);
605
+ return yield* client.GetTreeSnapshot(undefined as void).pipe(Effect.orDie);
606
+ }),
607
+
593
608
  subscribe: (documentId) =>
594
609
  Effect.gen(function* () {
595
610
  const pubsub =
@@ -74,12 +74,23 @@ export interface MimicServerEngine {
74
74
 
75
75
  /**
76
76
  * Get document snapshot (current state and version).
77
+ * Returns flat state format used for storage and synchronization.
77
78
  * May fail with MimicServerEngineError if storage is unavailable.
78
79
  */
79
80
  readonly getSnapshot: (
80
81
  documentId: string
81
82
  ) => Effect.Effect<{ state: unknown; version: number }, MimicServerEngineError>;
82
83
 
84
+ /**
85
+ * Get tree-like snapshot for rendering.
86
+ * Returns a readonly structure where trees are converted from
87
+ * flat state to nested/hierarchical structure suitable for UI rendering.
88
+ * May fail with MimicServerEngineError if storage is unavailable.
89
+ */
90
+ readonly getTreeSnapshot: (
91
+ documentId: string
92
+ ) => Effect.Effect<unknown, MimicServerEngineError>;
93
+
83
94
  /**
84
95
  * Subscribe to document broadcasts (transactions).
85
96
  * Returns a stream of server messages.
@@ -417,6 +428,12 @@ export const make = <TSchema extends Primitive.AnyPrimitive>(
417
428
  return instance.getSnapshot();
418
429
  }),
419
430
 
431
+ getTreeSnapshot: (documentId) =>
432
+ Effect.gen(function* () {
433
+ const instance = yield* getOrCreateDocument(documentId);
434
+ return instance.toSnapshot();
435
+ }),
436
+
420
437
  subscribe: (documentId) =>
421
438
  Effect.gen(function* () {
422
439
  const instance = yield* getOrCreateDocument(documentId);
@@ -190,13 +190,13 @@ describe("MimicServerEngine", () => {
190
190
  Effect.scoped(
191
191
  Effect.gen(function* () {
192
192
  const engine = yield* MimicServerEngineTag;
193
-
193
+
194
194
  // Submit transaction
195
195
  const submitResult = yield* engine.submit("test-doc-2", tx);
196
-
196
+
197
197
  // Get snapshot
198
198
  const snapshot = yield* engine.getSnapshot("test-doc-2");
199
-
199
+
200
200
  return { submitResult, snapshot };
201
201
  })
202
202
  ).pipe(Effect.provide(makeTestLayer({ initial: { title: "Initial" } })))
@@ -210,6 +210,32 @@ describe("MimicServerEngine", () => {
210
210
  expect(result.snapshot.state).toEqual({ title: "Updated Title" });
211
211
  });
212
212
 
213
+ it("should get tree snapshot for rendering", async () => {
214
+ const tx = createValidTransaction("Tree Snapshot Test");
215
+
216
+ const result = await Effect.runPromise(
217
+ Effect.scoped(
218
+ Effect.gen(function* () {
219
+ const engine = yield* MimicServerEngineTag;
220
+
221
+ // Submit transaction
222
+ yield* engine.submit("test-doc-tree", tx);
223
+
224
+ // Get tree snapshot (for rendering)
225
+ const treeSnapshot = yield* engine.getTreeSnapshot("test-doc-tree");
226
+
227
+ return treeSnapshot;
228
+ })
229
+ ).pipe(Effect.provide(makeTestLayer({ initial: { title: "Initial" } })))
230
+ );
231
+
232
+ // Tree snapshot should have the expected structure with defaults resolved
233
+ expect(result).toEqual({
234
+ title: "Tree Snapshot Test",
235
+ count: 0, // Default value
236
+ });
237
+ });
238
+
213
239
  it("should touch document to update activity time", async () => {
214
240
  const result = await Effect.runPromise(
215
241
  Effect.scoped(