@voidhash/mimic-effect 0.0.8 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +93 -89
- package/README.md +385 -0
- package/dist/ColdStorage.cjs +60 -0
- package/dist/ColdStorage.d.cts +53 -0
- package/dist/ColdStorage.d.cts.map +1 -0
- package/dist/ColdStorage.d.mts +53 -0
- package/dist/ColdStorage.d.mts.map +1 -0
- package/dist/ColdStorage.mjs +60 -0
- package/dist/ColdStorage.mjs.map +1 -0
- package/dist/DocumentManager.cjs +193 -82
- package/dist/DocumentManager.d.cts +33 -19
- package/dist/DocumentManager.d.cts.map +1 -1
- package/dist/DocumentManager.d.mts +33 -19
- package/dist/DocumentManager.d.mts.map +1 -1
- package/dist/DocumentManager.mjs +189 -67
- package/dist/DocumentManager.mjs.map +1 -1
- package/dist/Errors.cjs +45 -0
- package/dist/Errors.d.cts +81 -0
- package/dist/Errors.d.cts.map +1 -0
- package/dist/Errors.d.mts +81 -0
- package/dist/Errors.d.mts.map +1 -0
- package/dist/Errors.mjs +40 -0
- package/dist/Errors.mjs.map +1 -0
- package/dist/HotStorage.cjs +77 -0
- package/dist/HotStorage.d.cts +54 -0
- package/dist/HotStorage.d.cts.map +1 -0
- package/dist/HotStorage.d.mts +54 -0
- package/dist/HotStorage.d.mts.map +1 -0
- package/dist/HotStorage.mjs +77 -0
- package/dist/HotStorage.mjs.map +1 -0
- package/dist/Metrics.cjs +121 -0
- package/dist/Metrics.d.cts +27 -0
- package/dist/Metrics.d.cts.map +1 -0
- package/dist/Metrics.d.mts +27 -0
- package/dist/Metrics.d.mts.map +1 -0
- package/dist/Metrics.mjs +106 -0
- package/dist/Metrics.mjs.map +1 -0
- package/dist/MimicAuthService.cjs +61 -45
- package/dist/MimicAuthService.d.cts +61 -48
- package/dist/MimicAuthService.d.cts.map +1 -1
- package/dist/MimicAuthService.d.mts +61 -48
- package/dist/MimicAuthService.d.mts.map +1 -1
- package/dist/MimicAuthService.mjs +60 -36
- package/dist/MimicAuthService.mjs.map +1 -1
- package/dist/MimicClusterServerEngine.cjs +443 -0
- package/dist/MimicClusterServerEngine.d.cts +17 -0
- package/dist/MimicClusterServerEngine.d.cts.map +1 -0
- package/dist/MimicClusterServerEngine.d.mts +17 -0
- package/dist/MimicClusterServerEngine.d.mts.map +1 -0
- package/dist/MimicClusterServerEngine.mjs +445 -0
- package/dist/MimicClusterServerEngine.mjs.map +1 -0
- package/dist/MimicServer.cjs +205 -96
- package/dist/MimicServer.d.cts +9 -110
- package/dist/MimicServer.d.cts.map +1 -1
- package/dist/MimicServer.d.mts +9 -110
- package/dist/MimicServer.d.mts.map +1 -1
- package/dist/MimicServer.mjs +206 -90
- package/dist/MimicServer.mjs.map +1 -1
- package/dist/MimicServerEngine.cjs +97 -0
- package/dist/MimicServerEngine.d.cts +75 -0
- package/dist/MimicServerEngine.d.cts.map +1 -0
- package/dist/MimicServerEngine.d.mts +75 -0
- package/dist/MimicServerEngine.d.mts.map +1 -0
- package/dist/MimicServerEngine.mjs +97 -0
- package/dist/MimicServerEngine.mjs.map +1 -0
- package/dist/PresenceManager.cjs +75 -91
- package/dist/PresenceManager.d.cts +17 -66
- package/dist/PresenceManager.d.cts.map +1 -1
- package/dist/PresenceManager.d.mts +17 -66
- package/dist/PresenceManager.d.mts.map +1 -1
- package/dist/PresenceManager.mjs +74 -78
- package/dist/PresenceManager.mjs.map +1 -1
- package/dist/Protocol.cjs +146 -0
- package/dist/Protocol.d.cts +203 -0
- package/dist/Protocol.d.cts.map +1 -0
- package/dist/Protocol.d.mts +203 -0
- package/dist/Protocol.d.mts.map +1 -0
- package/dist/Protocol.mjs +132 -0
- package/dist/Protocol.mjs.map +1 -0
- package/dist/Types.d.cts +172 -0
- package/dist/Types.d.cts.map +1 -0
- package/dist/Types.d.mts +172 -0
- package/dist/Types.d.mts.map +1 -0
- package/dist/_virtual/rolldown_runtime.cjs +1 -25
- package/dist/_virtual/rolldown_runtime.mjs +4 -1
- package/dist/index.cjs +37 -75
- package/dist/index.d.cts +13 -12
- package/dist/index.d.mts +13 -12
- package/dist/index.mjs +12 -12
- package/package.json +14 -6
- package/src/ColdStorage.ts +136 -0
- package/src/DocumentManager.ts +445 -193
- package/src/Errors.ts +100 -0
- package/src/HotStorage.ts +165 -0
- package/src/Metrics.ts +163 -0
- package/src/MimicAuthService.ts +126 -64
- package/src/MimicClusterServerEngine.ts +824 -0
- package/src/MimicServer.ts +448 -195
- package/src/MimicServerEngine.ts +272 -0
- package/src/PresenceManager.ts +169 -240
- package/src/Protocol.ts +350 -0
- package/src/Types.ts +231 -0
- package/src/index.ts +57 -23
- package/tests/ColdStorage.test.ts +136 -0
- package/tests/DocumentManager.test.ts +158 -287
- package/tests/HotStorage.test.ts +143 -0
- package/tests/MimicAuthService.test.ts +102 -134
- package/tests/MimicClusterServerEngine.test.ts +587 -0
- package/tests/MimicServer.test.ts +90 -226
- package/tests/MimicServerEngine.test.ts +521 -0
- package/tests/PresenceManager.test.ts +22 -63
- package/tests/Protocol.test.ts +190 -0
- package/tsconfig.json +1 -1
- package/dist/DocumentProtocol.cjs +0 -94
- package/dist/DocumentProtocol.d.cts +0 -113
- package/dist/DocumentProtocol.d.cts.map +0 -1
- package/dist/DocumentProtocol.d.mts +0 -113
- package/dist/DocumentProtocol.d.mts.map +0 -1
- package/dist/DocumentProtocol.mjs +0 -89
- package/dist/DocumentProtocol.mjs.map +0 -1
- package/dist/MimicConfig.cjs +0 -60
- package/dist/MimicConfig.d.cts +0 -141
- package/dist/MimicConfig.d.cts.map +0 -1
- package/dist/MimicConfig.d.mts +0 -141
- package/dist/MimicConfig.d.mts.map +0 -1
- package/dist/MimicConfig.mjs +0 -50
- package/dist/MimicConfig.mjs.map +0 -1
- package/dist/MimicDataStorage.cjs +0 -83
- package/dist/MimicDataStorage.d.cts +0 -113
- package/dist/MimicDataStorage.d.cts.map +0 -1
- package/dist/MimicDataStorage.d.mts +0 -113
- package/dist/MimicDataStorage.d.mts.map +0 -1
- package/dist/MimicDataStorage.mjs +0 -74
- package/dist/MimicDataStorage.mjs.map +0 -1
- package/dist/WebSocketHandler.cjs +0 -365
- package/dist/WebSocketHandler.d.cts +0 -34
- package/dist/WebSocketHandler.d.cts.map +0 -1
- package/dist/WebSocketHandler.d.mts +0 -34
- package/dist/WebSocketHandler.d.mts.map +0 -1
- package/dist/WebSocketHandler.mjs +0 -355
- package/dist/WebSocketHandler.mjs.map +0 -1
- package/dist/auth/NoAuth.cjs +0 -43
- package/dist/auth/NoAuth.d.cts +0 -22
- package/dist/auth/NoAuth.d.cts.map +0 -1
- package/dist/auth/NoAuth.d.mts +0 -22
- package/dist/auth/NoAuth.d.mts.map +0 -1
- package/dist/auth/NoAuth.mjs +0 -36
- package/dist/auth/NoAuth.mjs.map +0 -1
- package/dist/errors.cjs +0 -74
- package/dist/errors.d.cts +0 -89
- package/dist/errors.d.cts.map +0 -1
- package/dist/errors.d.mts +0 -89
- package/dist/errors.d.mts.map +0 -1
- package/dist/errors.mjs +0 -67
- package/dist/errors.mjs.map +0 -1
- package/dist/storage/InMemoryDataStorage.cjs +0 -57
- package/dist/storage/InMemoryDataStorage.d.cts +0 -19
- package/dist/storage/InMemoryDataStorage.d.cts.map +0 -1
- package/dist/storage/InMemoryDataStorage.d.mts +0 -19
- package/dist/storage/InMemoryDataStorage.d.mts.map +0 -1
- package/dist/storage/InMemoryDataStorage.mjs +0 -48
- package/dist/storage/InMemoryDataStorage.mjs.map +0 -1
- package/src/DocumentProtocol.ts +0 -112
- package/src/MimicConfig.ts +0 -211
- package/src/MimicDataStorage.ts +0 -157
- package/src/WebSocketHandler.ts +0 -735
- package/src/auth/NoAuth.ts +0 -46
- package/src/errors.ts +0 -113
- package/src/storage/InMemoryDataStorage.ts +0 -66
- package/tests/DocumentProtocol.test.ts +0 -113
- package/tests/InMemoryDataStorage.test.ts +0 -190
- package/tests/MimicConfig.test.ts +0 -290
- package/tests/MimicDataStorage.test.ts +0 -190
- package/tests/NoAuth.test.ts +0 -94
- package/tests/WebSocketHandler.test.ts +0 -321
- package/tests/errors.test.ts +0 -77
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MimicAuthService.mjs","names":[],"sources":["../src/MimicAuthService.ts"],"sourcesContent":["/**\n * @
|
|
1
|
+
{"version":3,"file":"MimicAuthService.mjs","names":[],"sources":["../src/MimicAuthService.ts"],"sourcesContent":["/**\n * @voidhash/mimic-effect - MimicAuthService\n *\n * Authentication and authorization service interface and implementations.\n */\nimport { Context, Effect, Layer } from \"effect\";\nimport type { AuthContext, Permission } from \"./Types\";\nimport { AuthenticationError } from \"./Errors\";\n\n// =============================================================================\n// MimicAuthService Interface\n// =============================================================================\n\n/**\n * MimicAuthService interface for authentication and authorization.\n *\n * The `authenticate` method receives the token from the client's auth message\n * and the document ID being accessed. It should return an AuthContext on success\n * or fail with AuthenticationError on failure.\n *\n * The permission in AuthContext determines what the user can do:\n * - \"read\": Can subscribe, receive transactions, get snapshots\n * - \"write\": All of the above, plus can submit transactions and set presence\n */\nexport interface MimicAuthService {\n /**\n * Authenticate a connection and return authorization context.\n *\n * @param token - The token provided by the client\n * @param documentId - The document ID being accessed\n * @returns AuthContext with userId and permission level\n */\n readonly authenticate: (\n token: string,\n documentId: string\n ) => Effect.Effect<AuthContext, AuthenticationError>;\n}\n\n// =============================================================================\n// Context Tag\n// =============================================================================\n\n/**\n * Context tag for MimicAuthService\n */\nexport class MimicAuthServiceTag extends Context.Tag(\n \"@voidhash/mimic-effect/MimicAuthService\"\n)<MimicAuthServiceTag, MimicAuthService>() {}\n\n// =============================================================================\n// Factory\n// =============================================================================\n\n/**\n * Create a MimicAuthService layer from an Effect that produces the service.\n *\n * This allows you to access other Effect services when implementing authentication.\n *\n * @example\n * ```typescript\n * const Auth = MimicAuthService.make(\n * Effect.gen(function*() {\n * const db = yield* DatabaseService\n * const jwt = yield* JwtService\n *\n * return {\n * authenticate: (token, documentId) =>\n * Effect.gen(function*() {\n * const payload = yield* jwt.verify(token).pipe(\n * Effect.mapError(() => new AuthenticationError({ reason: \"Invalid token\" }))\n * )\n *\n * const permission = yield* db.getDocumentPermission(payload.userId, documentId)\n *\n * return { userId: payload.userId, permission }\n * })\n * }\n * })\n * )\n * ```\n */\nexport const make = <E, R>(\n effect: Effect.Effect<MimicAuthService, E, R>\n): Layer.Layer<MimicAuthServiceTag, E, R> =>\n Layer.effect(MimicAuthServiceTag, effect);\n\n// =============================================================================\n// NoAuth Implementation\n// =============================================================================\n\n/**\n * No-authentication implementation.\n *\n * Everyone gets write access with userId \"anonymous\".\n * ONLY USE FOR DEVELOPMENT/TESTING.\n */\nexport namespace NoAuth {\n /**\n * Create a NoAuth layer.\n * All connections are authenticated with write permission.\n */\n export const make = (): Layer.Layer<MimicAuthServiceTag> =>\n Layer.succeed(MimicAuthServiceTag, {\n authenticate: (_token, _documentId) =>\n Effect.succeed({\n userId: \"anonymous\",\n permission: \"write\" as const,\n }),\n });\n}\n\n// =============================================================================\n// Static Implementation\n// =============================================================================\n\n/**\n * Static permissions implementation.\n *\n * Permissions are defined at configuration time.\n * The token is treated as the userId.\n */\nexport namespace Static {\n export interface Options {\n /**\n * Map of userId (token) to permission level\n */\n readonly permissions: Record<string, Permission>;\n /**\n * Default permission for users not in the permissions map.\n * If undefined, unknown users will fail authentication.\n */\n readonly defaultPermission?: Permission;\n }\n\n /**\n * Create a Static auth layer.\n * The token is treated as the userId, and permissions are looked up from the config.\n */\n export const make = (options: Options): Layer.Layer<MimicAuthServiceTag> =>\n Layer.succeed(MimicAuthServiceTag, {\n authenticate: (token, _documentId) => {\n const permission = options.permissions[token] ?? options.defaultPermission;\n if (permission === undefined) {\n return Effect.fail(\n new AuthenticationError({ reason: \"Unknown user\" })\n );\n }\n return Effect.succeed({\n userId: token,\n permission,\n });\n },\n });\n}\n\n// =============================================================================\n// Re-export namespace\n// =============================================================================\n\nexport const MimicAuthService = {\n Tag: MimicAuthServiceTag,\n make,\n NoAuth,\n Static,\n};\n"],"mappings":";;;;;;;;;;;;AA6CA,IAAa,sBAAb,cAAyC,QAAQ,IAC/C,0CACD,EAAyC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkC3C,MAAa,QACX,WAEA,MAAM,OAAO,qBAAqB,OAAO;;;sBAkBvC,MAAM,QAAQ,qBAAqB,EACjC,eAAe,QAAQ,gBACrB,OAAO,QAAQ;EACb,QAAQ;EACR,YAAY;EACb,CAAC,EACL,CAAC;;;;iBA8BiB,YACnB,MAAM,QAAQ,qBAAqB,EACjC,eAAe,OAAO,gBAAgB;;EACpC,MAAM,sCAAa,QAAQ,YAAY,+EAAU,QAAQ;AACzD,MAAI,eAAe,OACjB,QAAO,OAAO,KACZ,IAAI,oBAAoB,EAAE,QAAQ,gBAAgB,CAAC,CACpD;AAEH,SAAO,OAAO,QAAQ;GACpB,QAAQ;GACR;GACD,CAAC;IAEL,CAAC;;AAON,MAAa,mBAAmB;CAC9B,KAAK;CACL;CACA;CACA;CACD"}
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
const require_ColdStorage = require('./ColdStorage.cjs');
|
|
2
|
+
const require_HotStorage = require('./HotStorage.cjs');
|
|
3
|
+
const require_Metrics = require('./Metrics.cjs');
|
|
4
|
+
const require_objectSpread2 = require('./_virtual/_@oxc-project_runtime@0.103.0/helpers/objectSpread2.cjs');
|
|
5
|
+
const require_MimicServerEngine = require('./MimicServerEngine.cjs');
|
|
6
|
+
let effect = require("effect");
|
|
7
|
+
let _voidhash_mimic_server = require("@voidhash/mimic/server");
|
|
8
|
+
let _effect_cluster = require("@effect/cluster");
|
|
9
|
+
let _effect_rpc = require("@effect/rpc");
|
|
10
|
+
|
|
11
|
+
//#region src/MimicClusterServerEngine.ts
|
|
12
|
+
/**
|
|
13
|
+
* @voidhash/mimic-effect - MimicClusterServerEngine
|
|
14
|
+
*
|
|
15
|
+
* Clustered document management service using Effect Cluster for horizontal scaling.
|
|
16
|
+
* Each document becomes a cluster Entity with automatic sharding, failover, and location-transparent routing.
|
|
17
|
+
*
|
|
18
|
+
* This is an alternative to MimicServerEngine for distributed deployments.
|
|
19
|
+
*/
|
|
20
|
+
const DEFAULT_MAX_IDLE_TIME = effect.Duration.minutes(5);
|
|
21
|
+
const DEFAULT_MAX_TRANSACTION_HISTORY = 1e3;
|
|
22
|
+
const DEFAULT_SNAPSHOT_INTERVAL = effect.Duration.minutes(5);
|
|
23
|
+
const DEFAULT_SNAPSHOT_THRESHOLD = 100;
|
|
24
|
+
const DEFAULT_SHARD_GROUP = "mimic-documents";
|
|
25
|
+
/**
|
|
26
|
+
* Schema for encoded transaction (wire format)
|
|
27
|
+
*/
|
|
28
|
+
const EncodedTransactionSchema = effect.Schema.Struct({
|
|
29
|
+
id: effect.Schema.String,
|
|
30
|
+
ops: effect.Schema.Array(effect.Schema.Unknown)
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* Schema for submit result
|
|
34
|
+
*/
|
|
35
|
+
const SubmitResultSchema = effect.Schema.Union(effect.Schema.Struct({
|
|
36
|
+
success: effect.Schema.Literal(true),
|
|
37
|
+
version: effect.Schema.Number
|
|
38
|
+
}), effect.Schema.Struct({
|
|
39
|
+
success: effect.Schema.Literal(false),
|
|
40
|
+
reason: effect.Schema.String
|
|
41
|
+
}));
|
|
42
|
+
/**
|
|
43
|
+
* Schema for snapshot response
|
|
44
|
+
*/
|
|
45
|
+
const SnapshotResponseSchema = effect.Schema.Struct({
|
|
46
|
+
state: effect.Schema.Unknown,
|
|
47
|
+
version: effect.Schema.Number
|
|
48
|
+
});
|
|
49
|
+
/**
|
|
50
|
+
* Schema for presence entry
|
|
51
|
+
*/
|
|
52
|
+
const PresenceEntrySchema = effect.Schema.Struct({
|
|
53
|
+
data: effect.Schema.Unknown,
|
|
54
|
+
userId: effect.Schema.optional(effect.Schema.String)
|
|
55
|
+
});
|
|
56
|
+
/**
|
|
57
|
+
* Schema for presence snapshot response
|
|
58
|
+
*/
|
|
59
|
+
const PresenceSnapshotResponseSchema = effect.Schema.Struct({ presences: effect.Schema.Record({
|
|
60
|
+
key: effect.Schema.String,
|
|
61
|
+
value: PresenceEntrySchema
|
|
62
|
+
}) });
|
|
63
|
+
effect.Schema.Union(effect.Schema.Struct({
|
|
64
|
+
type: effect.Schema.Literal("presence_update"),
|
|
65
|
+
id: effect.Schema.String,
|
|
66
|
+
data: effect.Schema.Unknown,
|
|
67
|
+
userId: effect.Schema.optional(effect.Schema.String)
|
|
68
|
+
}), effect.Schema.Struct({
|
|
69
|
+
type: effect.Schema.Literal("presence_remove"),
|
|
70
|
+
id: effect.Schema.String
|
|
71
|
+
}));
|
|
72
|
+
effect.Schema.Unknown;
|
|
73
|
+
/**
|
|
74
|
+
* Define the Mimic Document Entity with its RPC protocol.
|
|
75
|
+
* This entity handles document operations for a single documentId.
|
|
76
|
+
*/
|
|
77
|
+
const MimicDocumentEntity = _effect_cluster.Entity.make("MimicDocument", [
|
|
78
|
+
_effect_rpc.Rpc.make("Submit", {
|
|
79
|
+
payload: { transaction: EncodedTransactionSchema },
|
|
80
|
+
success: SubmitResultSchema
|
|
81
|
+
}),
|
|
82
|
+
_effect_rpc.Rpc.make("GetSnapshot", { success: SnapshotResponseSchema }),
|
|
83
|
+
_effect_rpc.Rpc.make("Touch", { success: effect.Schema.Void }),
|
|
84
|
+
_effect_rpc.Rpc.make("SetPresence", {
|
|
85
|
+
payload: {
|
|
86
|
+
connectionId: effect.Schema.String,
|
|
87
|
+
entry: PresenceEntrySchema
|
|
88
|
+
},
|
|
89
|
+
success: effect.Schema.Void
|
|
90
|
+
}),
|
|
91
|
+
_effect_rpc.Rpc.make("RemovePresence", {
|
|
92
|
+
payload: { connectionId: effect.Schema.String },
|
|
93
|
+
success: effect.Schema.Void
|
|
94
|
+
}),
|
|
95
|
+
_effect_rpc.Rpc.make("GetPresenceSnapshot", { success: PresenceSnapshotResponseSchema })
|
|
96
|
+
]);
|
|
97
|
+
/**
|
|
98
|
+
* Context tag for cluster engine configuration
|
|
99
|
+
*/
|
|
100
|
+
var MimicClusterConfigTag = class extends effect.Context.Tag("@voidhash/mimic-effect/MimicClusterConfig")() {};
|
|
101
|
+
const resolveClusterConfig = (config) => {
|
|
102
|
+
var _config$maxTransactio, _config$snapshot, _config$snapshot$tran, _config$snapshot2, _config$shardGroup;
|
|
103
|
+
return {
|
|
104
|
+
schema: config.schema,
|
|
105
|
+
initial: config.initial,
|
|
106
|
+
presence: config.presence,
|
|
107
|
+
maxIdleTime: config.maxIdleTime ? effect.Duration.decode(config.maxIdleTime) : DEFAULT_MAX_IDLE_TIME,
|
|
108
|
+
maxTransactionHistory: (_config$maxTransactio = config.maxTransactionHistory) !== null && _config$maxTransactio !== void 0 ? _config$maxTransactio : DEFAULT_MAX_TRANSACTION_HISTORY,
|
|
109
|
+
snapshot: {
|
|
110
|
+
interval: ((_config$snapshot = config.snapshot) === null || _config$snapshot === void 0 ? void 0 : _config$snapshot.interval) ? effect.Duration.decode(config.snapshot.interval) : DEFAULT_SNAPSHOT_INTERVAL,
|
|
111
|
+
transactionThreshold: (_config$snapshot$tran = (_config$snapshot2 = config.snapshot) === null || _config$snapshot2 === void 0 ? void 0 : _config$snapshot2.transactionThreshold) !== null && _config$snapshot$tran !== void 0 ? _config$snapshot$tran : DEFAULT_SNAPSHOT_THRESHOLD
|
|
112
|
+
},
|
|
113
|
+
shardGroup: (_config$shardGroup = config.shardGroup) !== null && _config$shardGroup !== void 0 ? _config$shardGroup : DEFAULT_SHARD_GROUP
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Decode an encoded transaction to a Transaction object
|
|
118
|
+
*/
|
|
119
|
+
const decodeTransaction = (encoded) => {
|
|
120
|
+
const { Transaction } = require("@voidhash/mimic");
|
|
121
|
+
return Transaction.decode(encoded);
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Encode a Transaction to wire format
|
|
125
|
+
*/
|
|
126
|
+
const encodeTransaction = (tx) => {
|
|
127
|
+
const { Transaction } = require("@voidhash/mimic");
|
|
128
|
+
return Transaction.encode(tx);
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Create the entity handler for MimicDocument
|
|
132
|
+
*/
|
|
133
|
+
const createEntityHandler = (config, coldStorage, hotStorage) => effect.Effect.gen(function* () {
|
|
134
|
+
const documentId = (yield* _effect_cluster.Entity.CurrentAddress).entityId;
|
|
135
|
+
const SCHEMA_VERSION = 1;
|
|
136
|
+
const computeInitialState = () => {
|
|
137
|
+
if (config.initial === void 0) return effect.Effect.succeed(void 0);
|
|
138
|
+
if (typeof config.initial === "function") return config.initial({ documentId });
|
|
139
|
+
return effect.Effect.succeed(config.initial);
|
|
140
|
+
};
|
|
141
|
+
const storedDoc = yield* effect.Effect.catchAll(coldStorage.load(documentId), () => effect.Effect.succeed(void 0));
|
|
142
|
+
let initialState;
|
|
143
|
+
let initialVersion = 0;
|
|
144
|
+
if (storedDoc) {
|
|
145
|
+
initialState = storedDoc.state;
|
|
146
|
+
initialVersion = storedDoc.version;
|
|
147
|
+
} else initialState = yield* computeInitialState();
|
|
148
|
+
const broadcastPubSub = yield* effect.PubSub.unbounded();
|
|
149
|
+
const presencePubSub = yield* effect.PubSub.unbounded();
|
|
150
|
+
const stateRef = yield* effect.Ref.make({
|
|
151
|
+
document: void 0,
|
|
152
|
+
broadcastPubSub,
|
|
153
|
+
presences: effect.HashMap.empty(),
|
|
154
|
+
presencePubSub,
|
|
155
|
+
lastSnapshotVersion: initialVersion,
|
|
156
|
+
lastSnapshotTime: Date.now(),
|
|
157
|
+
transactionsSinceSnapshot: 0
|
|
158
|
+
});
|
|
159
|
+
const document = _voidhash_mimic_server.ServerDocument.make({
|
|
160
|
+
schema: config.schema,
|
|
161
|
+
initialState,
|
|
162
|
+
initialVersion,
|
|
163
|
+
maxTransactionHistory: config.maxTransactionHistory,
|
|
164
|
+
onBroadcast: (message) => {
|
|
165
|
+
effect.Effect.runSync(effect.PubSub.publish(broadcastPubSub, {
|
|
166
|
+
type: "transaction",
|
|
167
|
+
transaction: message.transaction,
|
|
168
|
+
version: message.version
|
|
169
|
+
}));
|
|
170
|
+
},
|
|
171
|
+
onRejection: (transactionId, reason) => {
|
|
172
|
+
effect.Effect.runSync(effect.PubSub.publish(broadcastPubSub, {
|
|
173
|
+
type: "error",
|
|
174
|
+
transactionId,
|
|
175
|
+
reason
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
yield* effect.Ref.update(stateRef, (s) => require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2({}, s), {}, { document }));
|
|
180
|
+
const walEntries = yield* effect.Effect.catchAll(hotStorage.getEntries(documentId, initialVersion), () => effect.Effect.succeed([]));
|
|
181
|
+
for (const entry of walEntries) {
|
|
182
|
+
const result = document.submit(entry.transaction);
|
|
183
|
+
if (!result.success) yield* effect.Effect.logWarning("Skipping corrupted WAL entry", {
|
|
184
|
+
documentId,
|
|
185
|
+
version: entry.version,
|
|
186
|
+
reason: result.reason
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (storedDoc) yield* effect.Metric.increment(require_Metrics.documentsRestored);
|
|
190
|
+
else yield* effect.Metric.increment(require_Metrics.documentsCreated);
|
|
191
|
+
yield* effect.Metric.incrementBy(require_Metrics.documentsActive, 1);
|
|
192
|
+
/**
|
|
193
|
+
* Save snapshot to ColdStorage
|
|
194
|
+
*/
|
|
195
|
+
const saveSnapshot = effect.Effect.gen(function* () {
|
|
196
|
+
const state = yield* effect.Ref.get(stateRef);
|
|
197
|
+
const docState = state.document.get();
|
|
198
|
+
const version = state.document.getVersion();
|
|
199
|
+
if (docState === void 0) return;
|
|
200
|
+
const storedDocument = {
|
|
201
|
+
state: docState,
|
|
202
|
+
version,
|
|
203
|
+
schemaVersion: SCHEMA_VERSION,
|
|
204
|
+
savedAt: Date.now()
|
|
205
|
+
};
|
|
206
|
+
const snapshotStartTime = Date.now();
|
|
207
|
+
yield* effect.Effect.catchAll(coldStorage.save(documentId, storedDocument), (e) => effect.Effect.logError("Failed to save snapshot", {
|
|
208
|
+
documentId,
|
|
209
|
+
error: e
|
|
210
|
+
}));
|
|
211
|
+
const snapshotDuration = Date.now() - snapshotStartTime;
|
|
212
|
+
yield* effect.Metric.increment(require_Metrics.storageSnapshots);
|
|
213
|
+
yield* effect.Metric.update(require_Metrics.storageSnapshotLatency, snapshotDuration);
|
|
214
|
+
yield* effect.Effect.catchAll(hotStorage.truncate(documentId, version), (e) => effect.Effect.logError("Failed to truncate WAL", {
|
|
215
|
+
documentId,
|
|
216
|
+
error: e
|
|
217
|
+
}));
|
|
218
|
+
yield* effect.Ref.update(stateRef, (s) => require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2({}, s), {}, {
|
|
219
|
+
lastSnapshotVersion: version,
|
|
220
|
+
lastSnapshotTime: Date.now(),
|
|
221
|
+
transactionsSinceSnapshot: 0
|
|
222
|
+
}));
|
|
223
|
+
});
|
|
224
|
+
/**
|
|
225
|
+
* Check if snapshot should be triggered
|
|
226
|
+
*/
|
|
227
|
+
const checkSnapshotTriggers = effect.Effect.gen(function* () {
|
|
228
|
+
const state = yield* effect.Ref.get(stateRef);
|
|
229
|
+
const now = Date.now();
|
|
230
|
+
const intervalMs = effect.Duration.toMillis(config.snapshot.interval);
|
|
231
|
+
const threshold = config.snapshot.transactionThreshold;
|
|
232
|
+
if (state.transactionsSinceSnapshot >= threshold) {
|
|
233
|
+
yield* saveSnapshot;
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (now - state.lastSnapshotTime >= intervalMs) {
|
|
237
|
+
yield* saveSnapshot;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
yield* effect.Effect.addFinalizer(() => effect.Effect.gen(function* () {
|
|
242
|
+
yield* saveSnapshot;
|
|
243
|
+
yield* effect.Metric.incrementBy(require_Metrics.documentsActive, -1);
|
|
244
|
+
yield* effect.Metric.increment(require_Metrics.documentsEvicted);
|
|
245
|
+
yield* effect.Effect.logDebug("Entity finalized", { documentId });
|
|
246
|
+
}));
|
|
247
|
+
return {
|
|
248
|
+
Submit: effect.Effect.fnUntraced(function* ({ payload }) {
|
|
249
|
+
const submitStartTime = Date.now();
|
|
250
|
+
const state = yield* effect.Ref.get(stateRef);
|
|
251
|
+
const transaction = decodeTransaction(payload.transaction);
|
|
252
|
+
const result = state.document.submit(transaction);
|
|
253
|
+
const latency = Date.now() - submitStartTime;
|
|
254
|
+
yield* effect.Metric.update(require_Metrics.transactionsLatency, latency);
|
|
255
|
+
if (result.success) {
|
|
256
|
+
yield* effect.Metric.increment(require_Metrics.transactionsProcessed);
|
|
257
|
+
const walEntry = {
|
|
258
|
+
transaction,
|
|
259
|
+
version: result.version,
|
|
260
|
+
timestamp: Date.now()
|
|
261
|
+
};
|
|
262
|
+
yield* effect.Effect.catchAll(hotStorage.append(documentId, walEntry), (e) => effect.Effect.logError("Failed to append to WAL", {
|
|
263
|
+
documentId,
|
|
264
|
+
error: e
|
|
265
|
+
}));
|
|
266
|
+
yield* effect.Metric.increment(require_Metrics.storageWalAppends);
|
|
267
|
+
yield* effect.Ref.update(stateRef, (s) => require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2({}, s), {}, { transactionsSinceSnapshot: s.transactionsSinceSnapshot + 1 }));
|
|
268
|
+
yield* checkSnapshotTriggers;
|
|
269
|
+
} else yield* effect.Metric.increment(require_Metrics.transactionsRejected);
|
|
270
|
+
return result;
|
|
271
|
+
}),
|
|
272
|
+
GetSnapshot: effect.Effect.fnUntraced(function* () {
|
|
273
|
+
return (yield* effect.Ref.get(stateRef)).document.getSnapshot();
|
|
274
|
+
}),
|
|
275
|
+
Touch: effect.Effect.fnUntraced(function* () {}),
|
|
276
|
+
SetPresence: effect.Effect.fnUntraced(function* ({ payload }) {
|
|
277
|
+
const { connectionId, entry } = payload;
|
|
278
|
+
yield* effect.Ref.update(stateRef, (s) => require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2({}, s), {}, { presences: effect.HashMap.set(s.presences, connectionId, entry) }));
|
|
279
|
+
yield* effect.Metric.increment(require_Metrics.presenceUpdates);
|
|
280
|
+
yield* effect.Metric.incrementBy(require_Metrics.presenceActive, 1);
|
|
281
|
+
const state = yield* effect.Ref.get(stateRef);
|
|
282
|
+
const event = {
|
|
283
|
+
type: "presence_update",
|
|
284
|
+
id: connectionId,
|
|
285
|
+
data: entry.data,
|
|
286
|
+
userId: entry.userId
|
|
287
|
+
};
|
|
288
|
+
yield* effect.PubSub.publish(state.presencePubSub, event);
|
|
289
|
+
}),
|
|
290
|
+
RemovePresence: effect.Effect.fnUntraced(function* ({ payload }) {
|
|
291
|
+
const { connectionId } = payload;
|
|
292
|
+
const state = yield* effect.Ref.get(stateRef);
|
|
293
|
+
if (!effect.HashMap.has(state.presences, connectionId)) return;
|
|
294
|
+
yield* effect.Ref.update(stateRef, (s) => require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2({}, s), {}, { presences: effect.HashMap.remove(s.presences, connectionId) }));
|
|
295
|
+
yield* effect.Metric.incrementBy(require_Metrics.presenceActive, -1);
|
|
296
|
+
const event = {
|
|
297
|
+
type: "presence_remove",
|
|
298
|
+
id: connectionId
|
|
299
|
+
};
|
|
300
|
+
yield* effect.PubSub.publish(state.presencePubSub, event);
|
|
301
|
+
}),
|
|
302
|
+
GetPresenceSnapshot: effect.Effect.fnUntraced(function* () {
|
|
303
|
+
const state = yield* effect.Ref.get(stateRef);
|
|
304
|
+
const presences = {};
|
|
305
|
+
for (const [id, entry] of state.presences) presences[id] = entry;
|
|
306
|
+
return { presences };
|
|
307
|
+
})
|
|
308
|
+
};
|
|
309
|
+
});
|
|
310
|
+
var SubscriptionStoreTag = class extends effect.Context.Tag("@voidhash/mimic-effect/SubscriptionStore")() {};
|
|
311
|
+
const subscriptionStoreLayer = effect.Layer.effect(SubscriptionStoreTag, effect.Effect.gen(function* () {
|
|
312
|
+
const documentPubSubs = yield* effect.Ref.make(effect.HashMap.empty());
|
|
313
|
+
const presencePubSubs = yield* effect.Ref.make(effect.HashMap.empty());
|
|
314
|
+
return {
|
|
315
|
+
getOrCreatePubSub: (documentId) => effect.Effect.gen(function* () {
|
|
316
|
+
const current = yield* effect.Ref.get(documentPubSubs);
|
|
317
|
+
const existing = effect.HashMap.get(current, documentId);
|
|
318
|
+
if (existing._tag === "Some") return existing.value;
|
|
319
|
+
const pubsub = yield* effect.PubSub.unbounded();
|
|
320
|
+
yield* effect.Ref.update(documentPubSubs, (map) => effect.HashMap.set(map, documentId, pubsub));
|
|
321
|
+
return pubsub;
|
|
322
|
+
}),
|
|
323
|
+
getOrCreatePresencePubSub: (documentId) => effect.Effect.gen(function* () {
|
|
324
|
+
const current = yield* effect.Ref.get(presencePubSubs);
|
|
325
|
+
const existing = effect.HashMap.get(current, documentId);
|
|
326
|
+
if (existing._tag === "Some") return existing.value;
|
|
327
|
+
const pubsub = yield* effect.PubSub.unbounded();
|
|
328
|
+
yield* effect.Ref.update(presencePubSubs, (map) => effect.HashMap.set(map, documentId, pubsub));
|
|
329
|
+
return pubsub;
|
|
330
|
+
})
|
|
331
|
+
};
|
|
332
|
+
}));
|
|
333
|
+
/**
|
|
334
|
+
* Create a MimicClusterServerEngine layer.
|
|
335
|
+
*
|
|
336
|
+
* This creates a clustered document management service using Effect Cluster.
|
|
337
|
+
* Each document becomes a cluster Entity with automatic sharding and failover.
|
|
338
|
+
*
|
|
339
|
+
* @example
|
|
340
|
+
* ```typescript
|
|
341
|
+
* // 1. Create the engine
|
|
342
|
+
* const Engine = MimicClusterServerEngine.make({
|
|
343
|
+
* schema: DocSchema,
|
|
344
|
+
* initial: { title: "Untitled" },
|
|
345
|
+
* presence: CursorPresence,
|
|
346
|
+
* maxIdleTime: "5 minutes",
|
|
347
|
+
* snapshot: { interval: "5 minutes", transactionThreshold: 100 },
|
|
348
|
+
* shardGroup: "documents",
|
|
349
|
+
* })
|
|
350
|
+
*
|
|
351
|
+
* // 2. Create the WebSocket route
|
|
352
|
+
* const MimicRoute = MimicServer.layerHttpLayerRouter({
|
|
353
|
+
* path: "/mimic",
|
|
354
|
+
* })
|
|
355
|
+
*
|
|
356
|
+
* // 3. Wire together with cluster infrastructure
|
|
357
|
+
* const MimicLive = MimicRoute.pipe(
|
|
358
|
+
* Layer.provide(Engine),
|
|
359
|
+
* Layer.provide(ColdStorage.S3.make(...)),
|
|
360
|
+
* Layer.provide(HotStorage.Redis.make(...)),
|
|
361
|
+
* Layer.provide(MimicAuthService.make(...)),
|
|
362
|
+
* Layer.provide(ClusterInfrastructure),
|
|
363
|
+
* )
|
|
364
|
+
* ```
|
|
365
|
+
*/
|
|
366
|
+
const make = (config) => {
|
|
367
|
+
const resolvedConfig = resolveClusterConfig(config);
|
|
368
|
+
const configLayer = effect.Layer.succeed(MimicClusterConfigTag, resolvedConfig);
|
|
369
|
+
const entityLayer = MimicDocumentEntity.toLayer(effect.Effect.gen(function* () {
|
|
370
|
+
return yield* createEntityHandler(yield* MimicClusterConfigTag, yield* require_ColdStorage.ColdStorageTag, yield* require_HotStorage.HotStorageTag);
|
|
371
|
+
}), {
|
|
372
|
+
maxIdleTime: resolvedConfig.maxIdleTime,
|
|
373
|
+
concurrency: 1,
|
|
374
|
+
mailboxCapacity: 4096
|
|
375
|
+
});
|
|
376
|
+
const engineLayer = effect.Layer.scoped(require_MimicServerEngine.MimicServerEngineTag, effect.Effect.gen(function* () {
|
|
377
|
+
const makeClient = yield* MimicDocumentEntity.client;
|
|
378
|
+
const subscriptionStore = yield* SubscriptionStoreTag;
|
|
379
|
+
return {
|
|
380
|
+
submit: (documentId, transaction) => effect.Effect.gen(function* () {
|
|
381
|
+
const client = makeClient(documentId);
|
|
382
|
+
const encodedTx = encodeTransaction(transaction);
|
|
383
|
+
const result = yield* client.Submit({ transaction: encodedTx }).pipe(effect.Effect.catchAll((error) => effect.Effect.succeed({
|
|
384
|
+
success: false,
|
|
385
|
+
reason: `Cluster error: ${String(error)}`
|
|
386
|
+
})));
|
|
387
|
+
if (result.success) {
|
|
388
|
+
const pubsub = yield* subscriptionStore.getOrCreatePubSub(documentId);
|
|
389
|
+
yield* effect.PubSub.publish(pubsub, {
|
|
390
|
+
type: "transaction",
|
|
391
|
+
transaction,
|
|
392
|
+
version: result.version
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
return result;
|
|
396
|
+
}),
|
|
397
|
+
getSnapshot: (documentId) => effect.Effect.gen(function* () {
|
|
398
|
+
return yield* makeClient(documentId).GetSnapshot(void 0).pipe(effect.Effect.orDie);
|
|
399
|
+
}),
|
|
400
|
+
subscribe: (documentId) => effect.Effect.gen(function* () {
|
|
401
|
+
const pubsub = yield* subscriptionStore.getOrCreatePubSub(documentId);
|
|
402
|
+
return effect.Stream.fromPubSub(pubsub);
|
|
403
|
+
}),
|
|
404
|
+
touch: (documentId) => effect.Effect.gen(function* () {
|
|
405
|
+
yield* makeClient(documentId).Touch(void 0).pipe(effect.Effect.orDie);
|
|
406
|
+
}),
|
|
407
|
+
getPresenceSnapshot: (documentId) => effect.Effect.gen(function* () {
|
|
408
|
+
return yield* makeClient(documentId).GetPresenceSnapshot(void 0).pipe(effect.Effect.orDie);
|
|
409
|
+
}),
|
|
410
|
+
setPresence: (documentId, connectionId, entry) => effect.Effect.gen(function* () {
|
|
411
|
+
yield* makeClient(documentId).SetPresence({
|
|
412
|
+
connectionId,
|
|
413
|
+
entry
|
|
414
|
+
}).pipe(effect.Effect.orDie);
|
|
415
|
+
const pubsub = yield* subscriptionStore.getOrCreatePresencePubSub(documentId);
|
|
416
|
+
yield* effect.PubSub.publish(pubsub, {
|
|
417
|
+
type: "presence_update",
|
|
418
|
+
id: connectionId,
|
|
419
|
+
data: entry.data,
|
|
420
|
+
userId: entry.userId
|
|
421
|
+
});
|
|
422
|
+
}),
|
|
423
|
+
removePresence: (documentId, connectionId) => effect.Effect.gen(function* () {
|
|
424
|
+
yield* makeClient(documentId).RemovePresence({ connectionId }).pipe(effect.Effect.orDie);
|
|
425
|
+
const pubsub = yield* subscriptionStore.getOrCreatePresencePubSub(documentId);
|
|
426
|
+
yield* effect.PubSub.publish(pubsub, {
|
|
427
|
+
type: "presence_remove",
|
|
428
|
+
id: connectionId
|
|
429
|
+
});
|
|
430
|
+
}),
|
|
431
|
+
subscribePresence: (documentId) => effect.Effect.gen(function* () {
|
|
432
|
+
const pubsub = yield* subscriptionStore.getOrCreatePresencePubSub(documentId);
|
|
433
|
+
return effect.Stream.fromPubSub(pubsub);
|
|
434
|
+
}),
|
|
435
|
+
config: resolvedConfig
|
|
436
|
+
};
|
|
437
|
+
}));
|
|
438
|
+
return effect.Layer.mergeAll(entityLayer, engineLayer).pipe(effect.Layer.provideMerge(subscriptionStoreLayer), effect.Layer.provideMerge(configLayer));
|
|
439
|
+
};
|
|
440
|
+
const MimicClusterServerEngine = { make };
|
|
441
|
+
|
|
442
|
+
//#endregion
|
|
443
|
+
exports.MimicClusterServerEngine = MimicClusterServerEngine;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { MimicClusterServerEngineConfig } from "./Types.cjs";
|
|
2
|
+
import { ColdStorageTag } from "./ColdStorage.cjs";
|
|
3
|
+
import { HotStorageTag } from "./HotStorage.cjs";
|
|
4
|
+
import { MimicAuthServiceTag } from "./MimicAuthService.cjs";
|
|
5
|
+
import { MimicServerEngineTag } from "./MimicServerEngine.cjs";
|
|
6
|
+
import { Layer } from "effect";
|
|
7
|
+
import { Primitive } from "@voidhash/mimic";
|
|
8
|
+
import { Sharding } from "@effect/cluster";
|
|
9
|
+
|
|
10
|
+
//#region src/MimicClusterServerEngine.d.ts
|
|
11
|
+
|
|
12
|
+
declare const MimicClusterServerEngine: {
|
|
13
|
+
make: <TSchema extends Primitive.AnyPrimitive>(config: MimicClusterServerEngineConfig<TSchema>) => Layer.Layer<MimicServerEngineTag, never, ColdStorageTag | HotStorageTag | MimicAuthServiceTag | Sharding.Sharding>;
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
export { MimicClusterServerEngine };
|
|
17
|
+
//# sourceMappingURL=MimicClusterServerEngine.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MimicClusterServerEngine.d.cts","names":[],"sources":["../src/MimicClusterServerEngine.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;cAqzBa;yBA3JwB,SAAA,CAAU,sBACrC,+BAA+B,aACtC,KAAA,CAAM,MACP,6BAEA,iBAAiB,gBAAgB,sBAAsB,QAAA,CAAS"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { MimicClusterServerEngineConfig } from "./Types.mjs";
|
|
2
|
+
import { ColdStorageTag } from "./ColdStorage.mjs";
|
|
3
|
+
import { HotStorageTag } from "./HotStorage.mjs";
|
|
4
|
+
import { MimicAuthServiceTag } from "./MimicAuthService.mjs";
|
|
5
|
+
import { MimicServerEngineTag } from "./MimicServerEngine.mjs";
|
|
6
|
+
import { Layer } from "effect";
|
|
7
|
+
import { Sharding } from "@effect/cluster";
|
|
8
|
+
import { Primitive } from "@voidhash/mimic";
|
|
9
|
+
|
|
10
|
+
//#region src/MimicClusterServerEngine.d.ts
|
|
11
|
+
|
|
12
|
+
declare const MimicClusterServerEngine: {
|
|
13
|
+
make: <TSchema extends Primitive.AnyPrimitive>(config: MimicClusterServerEngineConfig<TSchema>) => Layer.Layer<MimicServerEngineTag, never, ColdStorageTag | HotStorageTag | MimicAuthServiceTag | Sharding.Sharding>;
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
export { MimicClusterServerEngine };
|
|
17
|
+
//# sourceMappingURL=MimicClusterServerEngine.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MimicClusterServerEngine.d.mts","names":[],"sources":["../src/MimicClusterServerEngine.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;cAqzBa;yBA3JwB,SAAA,CAAU,sBACrC,+BAA+B,aACtC,KAAA,CAAM,MACP,6BAEA,iBAAiB,gBAAgB,sBAAsB,QAAA,CAAS"}
|