@proseql/core 0.1.0

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.
Files changed (342) hide show
  1. package/LICENSE +21 -0
  2. package/dist/errors/crud-errors.d.ts +98 -0
  3. package/dist/errors/crud-errors.d.ts.map +1 -0
  4. package/dist/errors/crud-errors.js +23 -0
  5. package/dist/errors/crud-errors.js.map +1 -0
  6. package/dist/errors/index.d.ts +16 -0
  7. package/dist/errors/index.d.ts.map +1 -0
  8. package/dist/errors/index.js +12 -0
  9. package/dist/errors/index.js.map +1 -0
  10. package/dist/errors/migration-errors.d.ts +22 -0
  11. package/dist/errors/migration-errors.d.ts.map +1 -0
  12. package/dist/errors/migration-errors.js +14 -0
  13. package/dist/errors/migration-errors.js.map +1 -0
  14. package/dist/errors/plugin-errors.d.ts +15 -0
  15. package/dist/errors/plugin-errors.d.ts.map +1 -0
  16. package/dist/errors/plugin-errors.js +11 -0
  17. package/dist/errors/plugin-errors.js.map +1 -0
  18. package/dist/errors/query-errors.d.ts +31 -0
  19. package/dist/errors/query-errors.d.ts.map +1 -0
  20. package/dist/errors/query-errors.js +11 -0
  21. package/dist/errors/query-errors.js.map +1 -0
  22. package/dist/errors/storage-errors.d.ts +30 -0
  23. package/dist/errors/storage-errors.d.ts.map +1 -0
  24. package/dist/errors/storage-errors.js +11 -0
  25. package/dist/errors/storage-errors.js.map +1 -0
  26. package/dist/factories/crud-factory-with-relationships.d.ts +28 -0
  27. package/dist/factories/crud-factory-with-relationships.d.ts.map +1 -0
  28. package/dist/factories/crud-factory-with-relationships.js +8 -0
  29. package/dist/factories/crud-factory-with-relationships.js.map +1 -0
  30. package/dist/factories/crud-factory.d.ts +25 -0
  31. package/dist/factories/crud-factory.d.ts.map +1 -0
  32. package/dist/factories/crud-factory.js +8 -0
  33. package/dist/factories/crud-factory.js.map +1 -0
  34. package/dist/factories/database-effect.d.ts +241 -0
  35. package/dist/factories/database-effect.d.ts.map +1 -0
  36. package/dist/factories/database-effect.js +859 -0
  37. package/dist/factories/database-effect.js.map +1 -0
  38. package/dist/hooks/hook-runner.d.ts +60 -0
  39. package/dist/hooks/hook-runner.d.ts.map +1 -0
  40. package/dist/hooks/hook-runner.js +107 -0
  41. package/dist/hooks/hook-runner.js.map +1 -0
  42. package/dist/index.d.ts +84 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +110 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/indexes/index-lookup.d.ts +33 -0
  47. package/dist/indexes/index-lookup.d.ts.map +1 -0
  48. package/dist/indexes/index-lookup.js +180 -0
  49. package/dist/indexes/index-lookup.js.map +1 -0
  50. package/dist/indexes/index-manager.d.ts +118 -0
  51. package/dist/indexes/index-manager.d.ts.map +1 -0
  52. package/dist/indexes/index-manager.js +345 -0
  53. package/dist/indexes/index-manager.js.map +1 -0
  54. package/dist/indexes/search-index.d.ts +179 -0
  55. package/dist/indexes/search-index.d.ts.map +1 -0
  56. package/dist/indexes/search-index.js +405 -0
  57. package/dist/indexes/search-index.js.map +1 -0
  58. package/dist/migrations/migration-runner.d.ts +70 -0
  59. package/dist/migrations/migration-runner.d.ts.map +1 -0
  60. package/dist/migrations/migration-runner.js +271 -0
  61. package/dist/migrations/migration-runner.js.map +1 -0
  62. package/dist/migrations/migration-types.d.ts +63 -0
  63. package/dist/migrations/migration-types.d.ts.map +1 -0
  64. package/dist/migrations/migration-types.js +5 -0
  65. package/dist/migrations/migration-types.js.map +1 -0
  66. package/dist/operations/crud/create-with-relationships.d.ts +44 -0
  67. package/dist/operations/crud/create-with-relationships.d.ts.map +1 -0
  68. package/dist/operations/crud/create-with-relationships.js +483 -0
  69. package/dist/operations/crud/create-with-relationships.js.map +1 -0
  70. package/dist/operations/crud/create.d.ts +48 -0
  71. package/dist/operations/crud/create.d.ts.map +1 -0
  72. package/dist/operations/crud/create.js +333 -0
  73. package/dist/operations/crud/create.js.map +1 -0
  74. package/dist/operations/crud/delete-with-relationships.d.ts +63 -0
  75. package/dist/operations/crud/delete-with-relationships.d.ts.map +1 -0
  76. package/dist/operations/crud/delete-with-relationships.js +395 -0
  77. package/dist/operations/crud/delete-with-relationships.js.map +1 -0
  78. package/dist/operations/crud/delete.d.ts +58 -0
  79. package/dist/operations/crud/delete.d.ts.map +1 -0
  80. package/dist/operations/crud/delete.js +267 -0
  81. package/dist/operations/crud/delete.js.map +1 -0
  82. package/dist/operations/crud/unique-check.d.ts +114 -0
  83. package/dist/operations/crud/unique-check.d.ts.map +1 -0
  84. package/dist/operations/crud/unique-check.js +383 -0
  85. package/dist/operations/crud/unique-check.js.map +1 -0
  86. package/dist/operations/crud/update-with-relationships.d.ts +45 -0
  87. package/dist/operations/crud/update-with-relationships.d.ts.map +1 -0
  88. package/dist/operations/crud/update-with-relationships.js +516 -0
  89. package/dist/operations/crud/update-with-relationships.js.map +1 -0
  90. package/dist/operations/crud/update.d.ts +91 -0
  91. package/dist/operations/crud/update.d.ts.map +1 -0
  92. package/dist/operations/crud/update.js +505 -0
  93. package/dist/operations/crud/update.js.map +1 -0
  94. package/dist/operations/crud/upsert.d.ts +52 -0
  95. package/dist/operations/crud/upsert.d.ts.map +1 -0
  96. package/dist/operations/crud/upsert.js +386 -0
  97. package/dist/operations/crud/upsert.js.map +1 -0
  98. package/dist/operations/query/aggregate.d.ts +30 -0
  99. package/dist/operations/query/aggregate.d.ts.map +1 -0
  100. package/dist/operations/query/aggregate.js +227 -0
  101. package/dist/operations/query/aggregate.js.map +1 -0
  102. package/dist/operations/query/cursor-stream.d.ts +18 -0
  103. package/dist/operations/query/cursor-stream.d.ts.map +1 -0
  104. package/dist/operations/query/cursor-stream.js +199 -0
  105. package/dist/operations/query/cursor-stream.js.map +1 -0
  106. package/dist/operations/query/filter-stream.d.ts +12 -0
  107. package/dist/operations/query/filter-stream.d.ts.map +1 -0
  108. package/dist/operations/query/filter-stream.js +167 -0
  109. package/dist/operations/query/filter-stream.js.map +1 -0
  110. package/dist/operations/query/filter.d.ts +13 -0
  111. package/dist/operations/query/filter.d.ts.map +1 -0
  112. package/dist/operations/query/filter.js +267 -0
  113. package/dist/operations/query/filter.js.map +1 -0
  114. package/dist/operations/query/paginate-stream.d.ts +11 -0
  115. package/dist/operations/query/paginate-stream.d.ts.map +1 -0
  116. package/dist/operations/query/paginate-stream.js +22 -0
  117. package/dist/operations/query/paginate-stream.js.map +1 -0
  118. package/dist/operations/query/query-helpers.d.ts +14 -0
  119. package/dist/operations/query/query-helpers.d.ts.map +1 -0
  120. package/dist/operations/query/query-helpers.js +22 -0
  121. package/dist/operations/query/query-helpers.js.map +1 -0
  122. package/dist/operations/query/resolve-computed.d.ts +142 -0
  123. package/dist/operations/query/resolve-computed.d.ts.map +1 -0
  124. package/dist/operations/query/resolve-computed.js +197 -0
  125. package/dist/operations/query/resolve-computed.js.map +1 -0
  126. package/dist/operations/query/search.d.ts +110 -0
  127. package/dist/operations/query/search.d.ts.map +1 -0
  128. package/dist/operations/query/search.js +188 -0
  129. package/dist/operations/query/search.js.map +1 -0
  130. package/dist/operations/query/select-stream.d.ts +27 -0
  131. package/dist/operations/query/select-stream.d.ts.map +1 -0
  132. package/dist/operations/query/select-stream.js +88 -0
  133. package/dist/operations/query/select-stream.js.map +1 -0
  134. package/dist/operations/query/select.d.ts +54 -0
  135. package/dist/operations/query/select.d.ts.map +1 -0
  136. package/dist/operations/query/select.js +159 -0
  137. package/dist/operations/query/select.js.map +1 -0
  138. package/dist/operations/query/sort-stream.d.ts +46 -0
  139. package/dist/operations/query/sort-stream.d.ts.map +1 -0
  140. package/dist/operations/query/sort-stream.js +158 -0
  141. package/dist/operations/query/sort-stream.js.map +1 -0
  142. package/dist/operations/query/sort.d.ts +9 -0
  143. package/dist/operations/query/sort.d.ts.map +1 -0
  144. package/dist/operations/query/sort.js +58 -0
  145. package/dist/operations/query/sort.js.map +1 -0
  146. package/dist/operations/relationships/populate-stream.d.ts +29 -0
  147. package/dist/operations/relationships/populate-stream.d.ts.map +1 -0
  148. package/dist/operations/relationships/populate-stream.js +159 -0
  149. package/dist/operations/relationships/populate-stream.js.map +1 -0
  150. package/dist/operations/relationships/populate.d.ts +15 -0
  151. package/dist/operations/relationships/populate.d.ts.map +1 -0
  152. package/dist/operations/relationships/populate.js +228 -0
  153. package/dist/operations/relationships/populate.js.map +1 -0
  154. package/dist/plugins/plugin-hooks.d.ts +25 -0
  155. package/dist/plugins/plugin-hooks.d.ts.map +1 -0
  156. package/dist/plugins/plugin-hooks.js +64 -0
  157. package/dist/plugins/plugin-hooks.js.map +1 -0
  158. package/dist/plugins/plugin-registry.d.ts +26 -0
  159. package/dist/plugins/plugin-registry.d.ts.map +1 -0
  160. package/dist/plugins/plugin-registry.js +150 -0
  161. package/dist/plugins/plugin-registry.js.map +1 -0
  162. package/dist/plugins/plugin-types.d.ts +95 -0
  163. package/dist/plugins/plugin-types.d.ts.map +1 -0
  164. package/dist/plugins/plugin-types.js +6 -0
  165. package/dist/plugins/plugin-types.js.map +1 -0
  166. package/dist/plugins/plugin-validation.d.ts +49 -0
  167. package/dist/plugins/plugin-validation.d.ts.map +1 -0
  168. package/dist/plugins/plugin-validation.js +295 -0
  169. package/dist/plugins/plugin-validation.js.map +1 -0
  170. package/dist/reactive/change-event.d.ts +44 -0
  171. package/dist/reactive/change-event.d.ts.map +1 -0
  172. package/dist/reactive/change-event.js +49 -0
  173. package/dist/reactive/change-event.js.map +1 -0
  174. package/dist/reactive/change-pubsub.d.ts +32 -0
  175. package/dist/reactive/change-pubsub.d.ts.map +1 -0
  176. package/dist/reactive/change-pubsub.js +31 -0
  177. package/dist/reactive/change-pubsub.js.map +1 -0
  178. package/dist/reactive/evaluate-query.d.ts +62 -0
  179. package/dist/reactive/evaluate-query.d.ts.map +1 -0
  180. package/dist/reactive/evaluate-query.js +57 -0
  181. package/dist/reactive/evaluate-query.js.map +1 -0
  182. package/dist/reactive/watch-by-id.d.ts +53 -0
  183. package/dist/reactive/watch-by-id.d.ts.map +1 -0
  184. package/dist/reactive/watch-by-id.js +55 -0
  185. package/dist/reactive/watch-by-id.js.map +1 -0
  186. package/dist/reactive/watch.d.ts +78 -0
  187. package/dist/reactive/watch.d.ts.map +1 -0
  188. package/dist/reactive/watch.js +133 -0
  189. package/dist/reactive/watch.js.map +1 -0
  190. package/dist/serializers/codecs/hjson.d.ts +33 -0
  191. package/dist/serializers/codecs/hjson.d.ts.map +1 -0
  192. package/dist/serializers/codecs/hjson.js +40 -0
  193. package/dist/serializers/codecs/hjson.js.map +1 -0
  194. package/dist/serializers/codecs/json.d.ts +22 -0
  195. package/dist/serializers/codecs/json.d.ts.map +1 -0
  196. package/dist/serializers/codecs/json.js +28 -0
  197. package/dist/serializers/codecs/json.js.map +1 -0
  198. package/dist/serializers/codecs/json5.d.ts +26 -0
  199. package/dist/serializers/codecs/json5.d.ts.map +1 -0
  200. package/dist/serializers/codecs/json5.js +33 -0
  201. package/dist/serializers/codecs/json5.js.map +1 -0
  202. package/dist/serializers/codecs/jsonc.d.ts +29 -0
  203. package/dist/serializers/codecs/jsonc.d.ts.map +1 -0
  204. package/dist/serializers/codecs/jsonc.js +38 -0
  205. package/dist/serializers/codecs/jsonc.js.map +1 -0
  206. package/dist/serializers/codecs/jsonl.d.ts +17 -0
  207. package/dist/serializers/codecs/jsonl.d.ts.map +1 -0
  208. package/dist/serializers/codecs/jsonl.js +31 -0
  209. package/dist/serializers/codecs/jsonl.js.map +1 -0
  210. package/dist/serializers/codecs/prose.d.ts +419 -0
  211. package/dist/serializers/codecs/prose.d.ts.map +1 -0
  212. package/dist/serializers/codecs/prose.js +1060 -0
  213. package/dist/serializers/codecs/prose.js.map +1 -0
  214. package/dist/serializers/codecs/toml.d.ts +23 -0
  215. package/dist/serializers/codecs/toml.d.ts.map +1 -0
  216. package/dist/serializers/codecs/toml.js +66 -0
  217. package/dist/serializers/codecs/toml.js.map +1 -0
  218. package/dist/serializers/codecs/toon.d.ts +20 -0
  219. package/dist/serializers/codecs/toon.d.ts.map +1 -0
  220. package/dist/serializers/codecs/toon.js +33 -0
  221. package/dist/serializers/codecs/toon.js.map +1 -0
  222. package/dist/serializers/codecs/yaml.d.ts +24 -0
  223. package/dist/serializers/codecs/yaml.d.ts.map +1 -0
  224. package/dist/serializers/codecs/yaml.js +31 -0
  225. package/dist/serializers/codecs/yaml.js.map +1 -0
  226. package/dist/serializers/format-codec.d.ts +53 -0
  227. package/dist/serializers/format-codec.d.ts.map +1 -0
  228. package/dist/serializers/format-codec.js +148 -0
  229. package/dist/serializers/format-codec.js.map +1 -0
  230. package/dist/serializers/presets.d.ts +48 -0
  231. package/dist/serializers/presets.d.ts.map +1 -0
  232. package/dist/serializers/presets.js +72 -0
  233. package/dist/serializers/presets.js.map +1 -0
  234. package/dist/serializers/serializer-service.d.ts +11 -0
  235. package/dist/serializers/serializer-service.d.ts.map +1 -0
  236. package/dist/serializers/serializer-service.js +4 -0
  237. package/dist/serializers/serializer-service.js.map +1 -0
  238. package/dist/state/collection-state.d.ts +19 -0
  239. package/dist/state/collection-state.d.ts.map +1 -0
  240. package/dist/state/collection-state.js +15 -0
  241. package/dist/state/collection-state.js.map +1 -0
  242. package/dist/state/state-operations.d.ts +38 -0
  243. package/dist/state/state-operations.d.ts.map +1 -0
  244. package/dist/state/state-operations.js +65 -0
  245. package/dist/state/state-operations.js.map +1 -0
  246. package/dist/storage/in-memory-adapter-layer.d.ts +16 -0
  247. package/dist/storage/in-memory-adapter-layer.d.ts.map +1 -0
  248. package/dist/storage/in-memory-adapter-layer.js +81 -0
  249. package/dist/storage/in-memory-adapter-layer.js.map +1 -0
  250. package/dist/storage/persistence-effect.d.ts +244 -0
  251. package/dist/storage/persistence-effect.d.ts.map +1 -0
  252. package/dist/storage/persistence-effect.js +551 -0
  253. package/dist/storage/persistence-effect.js.map +1 -0
  254. package/dist/storage/storage-service.d.ts +22 -0
  255. package/dist/storage/storage-service.d.ts.map +1 -0
  256. package/dist/storage/storage-service.js +4 -0
  257. package/dist/storage/storage-service.js.map +1 -0
  258. package/dist/storage/transforms.d.ts +183 -0
  259. package/dist/storage/transforms.d.ts.map +1 -0
  260. package/dist/storage/transforms.js +263 -0
  261. package/dist/storage/transforms.js.map +1 -0
  262. package/dist/transactions/transaction.d.ts +87 -0
  263. package/dist/transactions/transaction.d.ts.map +1 -0
  264. package/dist/transactions/transaction.js +240 -0
  265. package/dist/transactions/transaction.js.map +1 -0
  266. package/dist/types/aggregate-types.d.ts +73 -0
  267. package/dist/types/aggregate-types.d.ts.map +1 -0
  268. package/dist/types/aggregate-types.js +14 -0
  269. package/dist/types/aggregate-types.js.map +1 -0
  270. package/dist/types/computed-types.d.ts +71 -0
  271. package/dist/types/computed-types.d.ts.map +1 -0
  272. package/dist/types/computed-types.js +8 -0
  273. package/dist/types/computed-types.js.map +1 -0
  274. package/dist/types/crud-relationship-types.d.ts +180 -0
  275. package/dist/types/crud-relationship-types.d.ts.map +1 -0
  276. package/dist/types/crud-relationship-types.js +17 -0
  277. package/dist/types/crud-relationship-types.js.map +1 -0
  278. package/dist/types/crud-types.d.ts +343 -0
  279. package/dist/types/crud-types.d.ts.map +1 -0
  280. package/dist/types/crud-types.js +43 -0
  281. package/dist/types/crud-types.js.map +1 -0
  282. package/dist/types/cursor-types.d.ts +52 -0
  283. package/dist/types/cursor-types.d.ts.map +1 -0
  284. package/dist/types/cursor-types.js +2 -0
  285. package/dist/types/cursor-types.js.map +1 -0
  286. package/dist/types/database-config-types.d.ts +196 -0
  287. package/dist/types/database-config-types.d.ts.map +1 -0
  288. package/dist/types/database-config-types.js +11 -0
  289. package/dist/types/database-config-types.js.map +1 -0
  290. package/dist/types/hook-types.d.ts +158 -0
  291. package/dist/types/hook-types.d.ts.map +1 -0
  292. package/dist/types/hook-types.js +6 -0
  293. package/dist/types/hook-types.js.map +1 -0
  294. package/dist/types/index-types.d.ts +42 -0
  295. package/dist/types/index-types.d.ts.map +1 -0
  296. package/dist/types/index-types.js +8 -0
  297. package/dist/types/index-types.js.map +1 -0
  298. package/dist/types/operators.d.ts +5 -0
  299. package/dist/types/operators.d.ts.map +1 -0
  300. package/dist/types/operators.js +297 -0
  301. package/dist/types/operators.js.map +1 -0
  302. package/dist/types/query-overloads.d.ts +54 -0
  303. package/dist/types/query-overloads.d.ts.map +1 -0
  304. package/dist/types/query-overloads.js +3 -0
  305. package/dist/types/query-overloads.js.map +1 -0
  306. package/dist/types/reactive-types.d.ts +75 -0
  307. package/dist/types/reactive-types.d.ts.map +1 -0
  308. package/dist/types/reactive-types.js +7 -0
  309. package/dist/types/reactive-types.js.map +1 -0
  310. package/dist/types/schema-types.d.ts +56 -0
  311. package/dist/types/schema-types.d.ts.map +1 -0
  312. package/dist/types/schema-types.js +8 -0
  313. package/dist/types/schema-types.js.map +1 -0
  314. package/dist/types/search-types.d.ts +82 -0
  315. package/dist/types/search-types.d.ts.map +1 -0
  316. package/dist/types/search-types.js +110 -0
  317. package/dist/types/search-types.js.map +1 -0
  318. package/dist/types/types.d.ts +286 -0
  319. package/dist/types/types.d.ts.map +1 -0
  320. package/dist/types/types.js +2 -0
  321. package/dist/types/types.js.map +1 -0
  322. package/dist/utils/id-generator.d.ts +97 -0
  323. package/dist/utils/id-generator.d.ts.map +1 -0
  324. package/dist/utils/id-generator.js +247 -0
  325. package/dist/utils/id-generator.js.map +1 -0
  326. package/dist/utils/nested-path.d.ts +56 -0
  327. package/dist/utils/nested-path.d.ts.map +1 -0
  328. package/dist/utils/nested-path.js +119 -0
  329. package/dist/utils/nested-path.js.map +1 -0
  330. package/dist/utils/path.d.ts +16 -0
  331. package/dist/utils/path.d.ts.map +1 -0
  332. package/dist/utils/path.js +24 -0
  333. package/dist/utils/path.js.map +1 -0
  334. package/dist/validators/foreign-key.d.ts +49 -0
  335. package/dist/validators/foreign-key.d.ts.map +1 -0
  336. package/dist/validators/foreign-key.js +153 -0
  337. package/dist/validators/foreign-key.js.map +1 -0
  338. package/dist/validators/schema-validator.d.ts +19 -0
  339. package/dist/validators/schema-validator.d.ts.map +1 -0
  340. package/dist/validators/schema-validator.js +34 -0
  341. package/dist/validators/schema-validator.js.map +1 -0
  342. package/package.json +57 -0
@@ -0,0 +1,859 @@
1
+ /**
2
+ * Effect-based database factory.
3
+ *
4
+ * Creates an in-memory database with typed collections, each backed by
5
+ * Ref<ReadonlyMap<string, T>> for O(1) ID lookup and atomic state updates.
6
+ *
7
+ * Query pipeline: Ref snapshot → Stream.fromIterable → filter → populate → sort → paginate → select
8
+ * CRUD: Effect-based operations with typed error channels
9
+ * Persistence: Optional debounced save after each CRUD mutation via Effect.fork
10
+ */
11
+ import { Chunk, Effect, Layer, PubSub, Ref, Schema, Stream, } from "effect";
12
+ import { NotFoundError, OperationError, ValidationError, } from "../errors/crud-errors.js";
13
+ import { resolveWithIndex } from "../indexes/index-lookup.js";
14
+ import { buildIndexes, normalizeIndexes } from "../indexes/index-manager.js";
15
+ import { buildSearchIndex, resolveWithSearchIndex, } from "../indexes/search-index.js";
16
+ import { dryRunMigrations, validateMigrationRegistry, } from "../migrations/migration-runner.js";
17
+ import { create, createMany } from "../operations/crud/create.js";
18
+ import { createWithRelationships } from "../operations/crud/create-with-relationships.js";
19
+ import { del, deleteMany } from "../operations/crud/delete.js";
20
+ import { deleteManyWithRelationships, deleteWithRelationships, } from "../operations/crud/delete-with-relationships.js";
21
+ import { normalizeConstraints } from "../operations/crud/unique-check.js";
22
+ import { update, updateMany } from "../operations/crud/update.js";
23
+ import { updateWithRelationships } from "../operations/crud/update-with-relationships.js";
24
+ import { upsert, upsertMany } from "../operations/crud/upsert.js";
25
+ import { computeAggregates, computeGroupedAggregates, } from "../operations/query/aggregate.js";
26
+ import { applyCursor } from "../operations/query/cursor-stream.js";
27
+ import { applyFilter } from "../operations/query/filter-stream.js";
28
+ import { applyPagination } from "../operations/query/paginate-stream.js";
29
+ import { resolveComputedStreamWithLazySkip } from "../operations/query/resolve-computed.js";
30
+ import { applySelect, applySelectToArray, } from "../operations/query/select-stream.js";
31
+ import { applyRelevanceSort, applySort, attachSearchScores, extractSearchConfig, } from "../operations/query/sort-stream.js";
32
+ import { applyPopulate } from "../operations/relationships/populate-stream.js";
33
+ import { mergeGlobalHooks } from "../plugins/plugin-hooks.js";
34
+ import { buildPluginRegistry } from "../plugins/plugin-registry.js";
35
+ import { validateIdGeneratorReferences } from "../plugins/plugin-validation.js";
36
+ import { watch } from "../reactive/watch.js";
37
+ import { watchById } from "../reactive/watch-by-id.js";
38
+ import { mergeSerializerWithPluginCodecs, } from "../serializers/format-codec.js";
39
+ import { SerializerRegistry } from "../serializers/serializer-service.js";
40
+ import { createFileWatcher, loadData, saveData, } from "../storage/persistence-effect.js";
41
+ import { StorageAdapter } from "../storage/storage-service.js";
42
+ import { $transaction as $transactionImpl } from "../transactions/transaction.js";
43
+ import { isGroupedAggregateConfig, } from "../types/aggregate-types.js";
44
+ /**
45
+ * Attach a lazy `runPromise` getter to an Effect value.
46
+ * The effect is only executed when `.runPromise` is accessed.
47
+ */
48
+ const withRunPromise = (effect) => {
49
+ let cached;
50
+ Object.defineProperty(effect, "runPromise", {
51
+ get() {
52
+ if (cached === undefined) {
53
+ cached = Effect.runPromise(effect);
54
+ }
55
+ return cached;
56
+ },
57
+ enumerable: false,
58
+ configurable: true,
59
+ });
60
+ return effect;
61
+ };
62
+ /**
63
+ * Attach a lazy `runPromise` getter to a Stream value.
64
+ * The stream is collected into an array when `.runPromise` is accessed.
65
+ */
66
+ const withStreamRunPromise = (stream) => {
67
+ let cached;
68
+ Object.defineProperty(stream, "runPromise", {
69
+ get() {
70
+ if (cached === undefined) {
71
+ cached = Effect.runPromise(Stream.runCollect(stream).pipe(Effect.map(Chunk.toReadonlyArray)));
72
+ }
73
+ return cached;
74
+ },
75
+ enumerable: false,
76
+ configurable: true,
77
+ });
78
+ return stream;
79
+ };
80
+ /**
81
+ * Attach a lazy `runPromise` getter to an Effect returning CursorPageResult.
82
+ * The effect is only executed when `.runPromise` is accessed.
83
+ */
84
+ const withCursorRunPromise = (effect) => {
85
+ let cached;
86
+ Object.defineProperty(effect, "runPromise", {
87
+ get() {
88
+ if (cached === undefined) {
89
+ cached = Effect.runPromise(effect);
90
+ }
91
+ return cached;
92
+ },
93
+ enumerable: false,
94
+ configurable: true,
95
+ });
96
+ return effect;
97
+ };
98
+ const createPersistenceTrigger = (delayMs, makeSaveEffect) => {
99
+ const pendingTimers = new Map();
100
+ const executeSave = (key) => Effect.runPromise(makeSaveEffect(key).pipe(Effect.catchAll(() => Effect.void)));
101
+ const schedule = (key) => {
102
+ // Cancel existing timer for this key
103
+ const existing = pendingTimers.get(key);
104
+ if (existing !== undefined) {
105
+ clearTimeout(existing);
106
+ }
107
+ // Schedule new debounced write
108
+ const timer = setTimeout(() => {
109
+ pendingTimers.delete(key);
110
+ executeSave(key);
111
+ }, delayMs);
112
+ pendingTimers.set(key, timer);
113
+ };
114
+ const flush = async () => {
115
+ // Take all pending keys, clear timers, execute saves
116
+ const keys = Array.from(pendingTimers.keys());
117
+ for (const [, timer] of pendingTimers) {
118
+ clearTimeout(timer);
119
+ }
120
+ pendingTimers.clear();
121
+ // Execute all saves
122
+ await Promise.all(keys.map((key) => executeSave(key)));
123
+ };
124
+ const pendingCount = () => pendingTimers.size;
125
+ const shutdown = () => {
126
+ for (const [, timer] of pendingTimers) {
127
+ clearTimeout(timer);
128
+ }
129
+ pendingTimers.clear();
130
+ };
131
+ return { schedule, flush, pendingCount, shutdown };
132
+ };
133
+ // ============================================================================
134
+ // Extract Populate Config from Object-based Select
135
+ // ============================================================================
136
+ /**
137
+ * Normalize select config for lazy skip optimization.
138
+ * Returns the select config as a Record if it's object-based, or undefined if it's array-based.
139
+ * The lazy skip optimization only works with object-based select.
140
+ */
141
+ function normalizeSelectForLazySkip(select) {
142
+ if (select === undefined) {
143
+ // undefined means select all, which includes computed fields
144
+ return undefined;
145
+ }
146
+ // Array.isArray works for ReadonlyArray too but TypeScript needs help narrowing
147
+ if (Array.isArray(select)) {
148
+ // Array-based select is rare and we don't optimize for it
149
+ return undefined;
150
+ }
151
+ // TypeScript now knows select is Record<string, unknown>
152
+ return select;
153
+ }
154
+ function extractPopulateFromSelect(select, relationships) {
155
+ const populate = {};
156
+ let hasPopulate = false;
157
+ for (const [key, value] of Object.entries(select)) {
158
+ if (key in relationships) {
159
+ if (value === true) {
160
+ populate[key] = true;
161
+ hasPopulate = true;
162
+ }
163
+ else if (typeof value === "object" &&
164
+ value !== null &&
165
+ !Array.isArray(value)) {
166
+ populate[key] = {
167
+ select: value,
168
+ ...value,
169
+ };
170
+ hasPopulate = true;
171
+ }
172
+ }
173
+ }
174
+ return hasPopulate ? populate : undefined;
175
+ }
176
+ const buildCollection = (collectionName, collectionConfig, ref, stateRefs, dbConfig, afterMutation, indexes, searchIndexRef, searchIndexFields, customOperators, idGeneratorMap, globalHooks, changePubSub, appendOnlyConfig) => {
177
+ const schema = collectionConfig.schema;
178
+ const relationships = collectionConfig.relationships;
179
+ // Merge global plugin hooks with collection-specific hooks.
180
+ // Global hooks run first (cross-cutting concerns), then collection hooks.
181
+ // Type narrowing: globalHooks is HooksConfig<Record<string, unknown>>,
182
+ // collectionHooks is HooksConfig<T>. mergeGlobalHooks returns HooksConfig<T>.
183
+ const collectionHooks = collectionConfig.hooks;
184
+ const hooks = mergeGlobalHooks(globalHooks, collectionHooks);
185
+ // Normalize unique fields constraints (default to empty array if not configured)
186
+ const uniqueFields = normalizeConstraints(collectionConfig.uniqueFields);
187
+ // Get computed fields config (undefined means no computed fields)
188
+ const computed = collectionConfig.computed;
189
+ // Get ID generator name from collection config (used with idGeneratorMap)
190
+ const idGeneratorName = collectionConfig.idGenerator;
191
+ // Build allRelationships map for delete (needs all collections' relationships)
192
+ const allRelationships = {};
193
+ for (const [name, config] of Object.entries(dbConfig)) {
194
+ allRelationships[name] = config.relationships;
195
+ }
196
+ // Query function: read Ref snapshot → Stream pipeline
197
+ // Returns RunnableStream for standard queries, RunnableCursorPage for cursor pagination
198
+ const queryFn = (options) => {
199
+ // Determine populate config: explicit populate or extract from object-based select
200
+ let populateConfig = options?.populate;
201
+ if (!populateConfig && options?.select && !Array.isArray(options.select)) {
202
+ populateConfig = extractPopulateFromSelect(options.select, relationships);
203
+ }
204
+ // Handle cursor pagination: validate and inject implicit sort if needed
205
+ const cursorConfig = options?.cursor;
206
+ let effectiveSort = options?.sort;
207
+ if (cursorConfig) {
208
+ const cursorKey = cursorConfig.key;
209
+ if (options?.sort) {
210
+ // Explicit sort provided: validate cursor key matches primary sort field
211
+ const sortKeys = Object.keys(options.sort);
212
+ if (sortKeys.length === 0) {
213
+ // Empty sort object: inject implicit ascending sort on cursor key
214
+ effectiveSort = { [cursorKey]: "asc" };
215
+ }
216
+ else {
217
+ const primarySortKey = sortKeys[0];
218
+ if (primarySortKey !== cursorKey) {
219
+ // Sort mismatch: return effect that immediately fails
220
+ const errorEffect = Effect.fail(new ValidationError({
221
+ message: "Invalid cursor configuration",
222
+ issues: [
223
+ {
224
+ field: "cursor.key",
225
+ message: `cursor key '${cursorKey}' must match primary sort field '${primarySortKey}'`,
226
+ },
227
+ ],
228
+ }));
229
+ return withCursorRunPromise(errorEffect);
230
+ }
231
+ }
232
+ }
233
+ else {
234
+ // No explicit sort: inject implicit ascending sort on cursor key
235
+ effectiveSort = { [cursorKey]: "asc" };
236
+ }
237
+ // Cursor pagination branch: populate → resolve computed → filter → sort → applyCursor → select
238
+ const cursorEffect = Effect.gen(function* () {
239
+ const map = yield* Ref.get(ref);
240
+ // Try index-accelerated lookup first (equality index, then search index)
241
+ let narrowed = indexes
242
+ ? yield* resolveWithIndex(options?.where, indexes, map)
243
+ : undefined;
244
+ // If equality index didn't help, try search index
245
+ if (narrowed === undefined && searchIndexRef) {
246
+ narrowed = yield* resolveWithSearchIndex(options?.where, searchIndexRef, searchIndexFields, map);
247
+ }
248
+ const items = (narrowed ?? Array.from(map.values()));
249
+ let s = Stream.fromIterable(items);
250
+ // Apply pipeline stages: populate → resolve computed (with lazy skip) → filter → sort
251
+ s = applyPopulate(populateConfig, stateRefs, dbConfig, collectionName)(s);
252
+ s = resolveComputedStreamWithLazySkip(computed, normalizeSelectForLazySkip(options?.select))(s);
253
+ s = applyFilter(options?.where, customOperators)(s);
254
+ // When $search is active, compute and attach relevance scores after filtering
255
+ // (even though cursor pagination uses explicit sort, scores are still computed)
256
+ const cursorSearchConfig = extractSearchConfig(options?.where);
257
+ if (cursorSearchConfig) {
258
+ s = attachSearchScores(cursorSearchConfig)(s);
259
+ }
260
+ s = applySort(effectiveSort)(s);
261
+ // Collect via applyCursor (extracts cursor values from pre-select items)
262
+ const cursorResult = yield* applyCursor(cursorConfig)(s);
263
+ // Apply select to collected items (after cursor extraction)
264
+ const selectedItems = applySelectToArray(cursorResult.items, options?.select);
265
+ // Return CursorPageResult with projected items but original cursor metadata
266
+ return {
267
+ items: selectedItems,
268
+ pageInfo: cursorResult.pageInfo,
269
+ };
270
+ });
271
+ return withCursorRunPromise(cursorEffect);
272
+ }
273
+ // Standard stream branch: populate → resolve computed → filter → sort → paginate → select
274
+ const stream = Stream.unwrap(Effect.gen(function* () {
275
+ const map = yield* Ref.get(ref);
276
+ // Try index-accelerated lookup first (equality index, then search index)
277
+ let narrowed = indexes
278
+ ? yield* resolveWithIndex(options?.where, indexes, map)
279
+ : undefined;
280
+ // If equality index didn't help, try search index
281
+ if (narrowed === undefined && searchIndexRef) {
282
+ narrowed = yield* resolveWithSearchIndex(options?.where, searchIndexRef, searchIndexFields, map);
283
+ }
284
+ const items = (narrowed ?? Array.from(map.values()));
285
+ let s = Stream.fromIterable(items);
286
+ // Apply pipeline stages: populate → resolve computed (with lazy skip) → filter → sort → paginate → select
287
+ s = applyPopulate(populateConfig, stateRefs, dbConfig, collectionName)(s);
288
+ s = resolveComputedStreamWithLazySkip(computed, normalizeSelectForLazySkip(options?.select))(s);
289
+ s = applyFilter(options?.where, customOperators)(s);
290
+ // When $search is active, compute and attach relevance scores after filtering
291
+ const searchConfig = extractSearchConfig(options?.where);
292
+ if (searchConfig) {
293
+ s = attachSearchScores(searchConfig)(s);
294
+ }
295
+ // When $search is active and no explicit sort provided, use relevance sort
296
+ if (searchConfig && !options?.sort) {
297
+ s = applyRelevanceSort(searchConfig)(s);
298
+ }
299
+ else {
300
+ s = applySort(effectiveSort)(s);
301
+ }
302
+ s = applyPagination(options?.offset, options?.limit)(s);
303
+ s = applySelect(options?.select)(s);
304
+ return s;
305
+ }));
306
+ return withStreamRunPromise(stream);
307
+ };
308
+ // Helper to wrap a function so its return value gets .runPromise.
309
+ // When afterMutation is configured, each CRUD method triggers a
310
+ // persistence save schedule after the mutation succeeds (synchronous,
311
+ // non-blocking — the actual save runs in a debounced setTimeout).
312
+ const wrapEffect = (fn) => (...args) => {
313
+ const effect = afterMutation
314
+ ? fn(...args).pipe(Effect.tap(() => afterMutation()))
315
+ : fn(...args);
316
+ return withRunPromise(effect);
317
+ };
318
+ // Helper to create a forbidden operation for append-only collections
319
+ const forbiddenOp = (opName) => (..._args) => withRunPromise(Effect.fail(new OperationError({
320
+ operation: opName,
321
+ reason: "append-only",
322
+ message: `Operation '${opName}' is not allowed on append-only collection '${collectionName}'`,
323
+ })));
324
+ // Wire CRUD operations with runPromise convenience
325
+ const rawCreate = create(collectionName, schema, relationships, ref, stateRefs, indexes, hooks, uniqueFields, computed, searchIndexRef, searchIndexFields, idGeneratorName, idGeneratorMap, changePubSub);
326
+ const rawCreateMany = createMany(collectionName, schema, relationships, ref, stateRefs, indexes, hooks, uniqueFields, computed, searchIndexRef, searchIndexFields, idGeneratorName, idGeneratorMap, changePubSub);
327
+ // For append-only: wrap create to also append each entity to the file
328
+ const createFn = appendOnlyConfig
329
+ ? (...args) => {
330
+ const effect = rawCreate(...args).pipe(Effect.tap((entity) => appendOnlyConfig.onEntityCreated(entity)));
331
+ return withRunPromise(effect);
332
+ }
333
+ : wrapEffect(rawCreate);
334
+ const createManyFn = appendOnlyConfig
335
+ ? (...args) => {
336
+ const effect = rawCreateMany(...args).pipe(Effect.tap((result) => Effect.forEach(result.created, (entity) => appendOnlyConfig.onEntityCreated(entity))));
337
+ return withRunPromise(effect);
338
+ }
339
+ : wrapEffect(rawCreateMany);
340
+ // For append-only: update/updateMany/delete/deleteMany/upsert/upsertMany are forbidden
341
+ const updateFn = appendOnlyConfig
342
+ ? // biome-ignore lint/suspicious/noExplicitAny: forbidden ops return OperationError regardless of input type
343
+ forbiddenOp("update")
344
+ : wrapEffect(update(collectionName, schema, relationships, ref, stateRefs, indexes, hooks, uniqueFields, computed, searchIndexRef, searchIndexFields, changePubSub));
345
+ const updateManyFn = appendOnlyConfig
346
+ ? // biome-ignore lint/suspicious/noExplicitAny: forbidden ops return OperationError regardless of input type
347
+ forbiddenOp("updateMany")
348
+ : wrapEffect(updateMany(collectionName, schema, relationships, ref, stateRefs, indexes, hooks, uniqueFields, computed, searchIndexRef, searchIndexFields, changePubSub));
349
+ // Check if schema defines a deletedAt field for soft delete support
350
+ const supportsSoftDelete = "fields" in schema &&
351
+ "deletedAt" in
352
+ schema
353
+ .fields;
354
+ const deleteFn = appendOnlyConfig
355
+ ? // biome-ignore lint/suspicious/noExplicitAny: forbidden ops return OperationError regardless of input type
356
+ forbiddenOp("delete")
357
+ : wrapEffect(del(collectionName, allRelationships, ref, stateRefs, supportsSoftDelete, indexes, hooks, searchIndexRef, searchIndexFields, changePubSub));
358
+ const deleteManyFn = appendOnlyConfig
359
+ ? // biome-ignore lint/suspicious/noExplicitAny: forbidden ops return OperationError regardless of input type
360
+ forbiddenOp("deleteMany")
361
+ : wrapEffect(deleteMany(collectionName, allRelationships, ref, stateRefs, supportsSoftDelete, indexes, hooks, searchIndexRef, searchIndexFields, changePubSub));
362
+ const upsertFn = appendOnlyConfig
363
+ ? // biome-ignore lint/suspicious/noExplicitAny: forbidden ops return OperationError regardless of input type
364
+ forbiddenOp("upsert")
365
+ : wrapEffect(upsert(collectionName, schema, relationships, ref, stateRefs, indexes, hooks, uniqueFields, searchIndexRef, searchIndexFields, changePubSub));
366
+ const upsertManyFn = appendOnlyConfig
367
+ ? // biome-ignore lint/suspicious/noExplicitAny: forbidden ops return OperationError regardless of input type
368
+ forbiddenOp("upsertMany")
369
+ : wrapEffect(upsertMany(collectionName, schema, relationships, ref, stateRefs, indexes, hooks, uniqueFields, searchIndexRef, searchIndexFields, changePubSub));
370
+ const createWithRelsFn = wrapEffect(createWithRelationships(collectionName, schema, relationships, ref, stateRefs, dbConfig, computed, changePubSub));
371
+ const updateWithRelsFn = appendOnlyConfig
372
+ ? // biome-ignore lint/suspicious/noExplicitAny: forbidden ops return OperationError regardless of input type
373
+ forbiddenOp("updateWithRelationships")
374
+ : wrapEffect(updateWithRelationships(collectionName, schema, relationships, ref, stateRefs, dbConfig, computed, changePubSub));
375
+ const deleteWithRelsFn = appendOnlyConfig
376
+ ? // biome-ignore lint/suspicious/noExplicitAny: forbidden ops return OperationError regardless of input type
377
+ forbiddenOp("deleteWithRelationships")
378
+ : wrapEffect(deleteWithRelationships(collectionName, relationships, ref, stateRefs, dbConfig, changePubSub));
379
+ const deleteManyWithRelsFn = appendOnlyConfig
380
+ ? // biome-ignore lint/suspicious/noExplicitAny: forbidden ops return OperationError regardless of input type
381
+ forbiddenOp("deleteManyWithRelationships")
382
+ : wrapEffect(deleteManyWithRelationships(collectionName, relationships, ref, stateRefs, dbConfig, changePubSub));
383
+ // findById: O(1) lookup directly from the ReadonlyMap
384
+ const findByIdFn = (id) => {
385
+ const effect = Effect.gen(function* () {
386
+ const map = yield* Ref.get(ref);
387
+ const entity = map.get(id);
388
+ if (entity === undefined) {
389
+ return yield* new NotFoundError({
390
+ collection: collectionName,
391
+ id,
392
+ message: `Entity with id "${id}" not found in collection "${collectionName}"`,
393
+ });
394
+ }
395
+ return entity;
396
+ });
397
+ return withRunPromise(effect);
398
+ };
399
+ // aggregate: read Ref → filter → collect → delegate to aggregate functions
400
+ const aggregateFn = (config) => {
401
+ const effect = Effect.gen(function* () {
402
+ // 1. Read Ref snapshot
403
+ const map = yield* Ref.get(ref);
404
+ const items = Array.from(map.values());
405
+ // 2. Create stream and apply filter
406
+ let s = Stream.fromIterable(items);
407
+ s = applyFilter(config.where, customOperators)(s);
408
+ // 3. Collect filtered entities
409
+ const chunk = yield* Stream.runCollect(s);
410
+ const entities = Chunk.toReadonlyArray(chunk);
411
+ // 4. Delegate to appropriate aggregate function based on groupBy presence
412
+ if (isGroupedAggregateConfig(config)) {
413
+ return computeGroupedAggregates(entities, config);
414
+ }
415
+ return computeAggregates(entities, config);
416
+ });
417
+ // Type assertion needed because TypeScript can't infer the conditional return type
418
+ return withRunPromise(effect);
419
+ };
420
+ // watch: create reactive subscription to query results
421
+ // Requires changePubSub to be available; throws if called within a transaction
422
+ const watchFn = (config) => {
423
+ if (changePubSub === undefined) {
424
+ // This happens when called within a transaction context
425
+ // Reactive queries aren't supported within transactions since transaction
426
+ // data is isolated and temporary (rolled back or committed atomically)
427
+ return Effect.die(new Error(`watch() is not supported within transactions. ` +
428
+ `Reactive queries can only be used on the main database collections.`));
429
+ }
430
+ return watch(changePubSub, ref, collectionName, config);
431
+ };
432
+ // watchById: create reactive subscription for a single entity
433
+ // Requires changePubSub to be available; throws if called within a transaction
434
+ const watchByIdFn = (id) => {
435
+ if (changePubSub === undefined) {
436
+ // This happens when called within a transaction context
437
+ // Reactive queries aren't supported within transactions since transaction
438
+ // data is isolated and temporary (rolled back or committed atomically)
439
+ return Effect.die(new Error(`watchById() is not supported within transactions. ` +
440
+ `Reactive queries can only be used on the main database collections.`));
441
+ }
442
+ return watchById(changePubSub, ref, collectionName, id);
443
+ };
444
+ return {
445
+ query: queryFn,
446
+ findById: findByIdFn,
447
+ create: createFn,
448
+ createMany: createManyFn,
449
+ update: updateFn,
450
+ updateMany: updateManyFn,
451
+ delete: deleteFn,
452
+ deleteMany: deleteManyFn,
453
+ upsert: upsertFn,
454
+ upsertMany: upsertManyFn,
455
+ createWithRelationships: createWithRelsFn,
456
+ updateWithRelationships: updateWithRelsFn,
457
+ deleteWithRelationships: deleteWithRelsFn,
458
+ deleteManyWithRelationships: deleteManyWithRelsFn,
459
+ aggregate: aggregateFn,
460
+ watch: watchFn,
461
+ watchById: watchByIdFn,
462
+ };
463
+ };
464
+ /**
465
+ * Create a `buildCollectionForTx` callback that mirrors `buildCollection` but
466
+ * accepts a transaction-aware `afterMutation`. The returned callback creates
467
+ * collection accessors that record mutations to the transaction's set instead
468
+ * of triggering persistence writes.
469
+ *
470
+ * Used by `createTransaction` and `$transaction` to provide collection accessors
471
+ * that participate in transaction semantics.
472
+ *
473
+ * @param config - The database configuration
474
+ * @param stateRefs - Shared state refs for cross-collection access
475
+ * @param typedRefs - Typed refs for each collection
476
+ * @param collectionIndexes - Pre-built indexes for each collection
477
+ * @param searchIndexRefs - Pre-built search indexes for each collection (optional)
478
+ * @param searchIndexFields - Fields covered by search index for each collection (optional)
479
+ * @param customOperators - Custom operators from plugins for query filtering (optional)
480
+ * @param idGeneratorMap - ID generators from plugins for custom ID generation (optional)
481
+ * @returns A callback matching the BuildCollectionForTx type
482
+ */
483
+ const makeBuildCollectionForTx = (config, stateRefs, typedRefs, collectionIndexes, searchIndexRefs, searchIndexFields, customOperators, idGeneratorMap, globalHooks) => {
484
+ return (collectionName, addMutation) => {
485
+ // Transaction-aware afterMutation: records mutation instead of scheduling persistence
486
+ const afterMutation = () => Effect.sync(() => addMutation(collectionName));
487
+ // Explicitly pass undefined for changePubSub to suppress reactive events during transactions.
488
+ // Individual mutations within a transaction should not publish change events;
489
+ // events are only published after commit (see task 7.3).
490
+ return buildCollection(collectionName, config[collectionName], typedRefs[collectionName], stateRefs, config, afterMutation, collectionIndexes[collectionName], searchIndexRefs?.[collectionName], searchIndexFields?.[collectionName], customOperators, idGeneratorMap, globalHooks, undefined);
491
+ };
492
+ };
493
+ // ============================================================================
494
+ // Database Factory
495
+ // ============================================================================
496
+ /**
497
+ * Create an Effect-based in-memory database.
498
+ *
499
+ * Accepts a DatabaseConfig and optional initial data (arrays keyed by collection name).
500
+ * Returns an Effect that initializes Ref state for each collection and wires up
501
+ * the query pipeline and CRUD methods.
502
+ *
503
+ * Optionally accepts plugins that provide custom codecs, operators, ID generators,
504
+ * and global lifecycle hooks.
505
+ *
506
+ * Usage:
507
+ * ```ts
508
+ * const db = yield* createEffectDatabase(config, {
509
+ * users: [{ id: "1", name: "Alice", age: 30 }],
510
+ * companies: [{ id: "c1", name: "TechCorp" }],
511
+ * })
512
+ *
513
+ * // Query
514
+ * const results = yield* Stream.runCollect(db.users.query({ where: { age: { $gt: 18 } } }))
515
+ *
516
+ * // CRUD
517
+ * const user = yield* db.users.create({ name: "Bob", age: 25 })
518
+ * ```
519
+ *
520
+ * With plugins:
521
+ * ```ts
522
+ * const db = yield* createEffectDatabase(config, initialData, {
523
+ * plugins: [regexPlugin, snowflakeIdPlugin]
524
+ * })
525
+ * ```
526
+ */
527
+ export const createEffectDatabase = (config, initialData, options) => Effect.gen(function* () {
528
+ // 0. Validate migration registries for all versioned collections at startup
529
+ for (const collectionName of Object.keys(config)) {
530
+ const collectionConfig = config[collectionName];
531
+ if (collectionConfig.version !== undefined) {
532
+ yield* validateMigrationRegistry(collectionName, collectionConfig.version, collectionConfig.migrations ?? []);
533
+ }
534
+ }
535
+ // 0b. Build plugin registry (validates plugins, merges contributions)
536
+ const pluginRegistry = yield* buildPluginRegistry(options?.plugins);
537
+ // 0c. Validate that all idGenerator references in collection configs exist
538
+ yield* validateIdGeneratorReferences(config, pluginRegistry.idGenerators);
539
+ // 0d. Run plugin initialize effects
540
+ for (const plugin of options?.plugins ?? []) {
541
+ if (plugin.initialize !== undefined) {
542
+ yield* plugin.initialize();
543
+ }
544
+ }
545
+ // 1. Create transaction lock for single-writer isolation
546
+ const transactionLock = yield* Ref.make(false);
547
+ // 2. Create Ref for each collection from initial data
548
+ const stateRefs = {};
549
+ const typedRefs = {};
550
+ for (const collectionName of Object.keys(config)) {
551
+ const items = (initialData?.[collectionName] ??
552
+ []);
553
+ const map = new Map(items.map((item) => [item.id, item]));
554
+ const ref = yield* Ref.make(map);
555
+ stateRefs[collectionName] = ref;
556
+ typedRefs[collectionName] = ref;
557
+ }
558
+ // 3. Build indexes for each collection from initial data
559
+ const collectionIndexes = {};
560
+ for (const collectionName of Object.keys(config)) {
561
+ const collectionConfig = config[collectionName];
562
+ const normalizedIndexes = normalizeIndexes(collectionConfig.indexes);
563
+ const items = (initialData?.[collectionName] ??
564
+ []);
565
+ const indexes = yield* buildIndexes(normalizedIndexes, items);
566
+ collectionIndexes[collectionName] = indexes;
567
+ }
568
+ // 3b. Build search indexes for collections that have searchIndex configured
569
+ const searchIndexRefs = {};
570
+ const searchIndexFields = {};
571
+ for (const collectionName of Object.keys(config)) {
572
+ const collectionConfig = config[collectionName];
573
+ if (collectionConfig.searchIndex &&
574
+ collectionConfig.searchIndex.length > 0) {
575
+ const items = (initialData?.[collectionName] ??
576
+ []);
577
+ const searchIdx = yield* buildSearchIndex(collectionConfig.searchIndex, items);
578
+ searchIndexRefs[collectionName] = searchIdx;
579
+ searchIndexFields[collectionName] = collectionConfig.searchIndex;
580
+ }
581
+ }
582
+ // 4. Create shared PubSub for reactive change notifications (one per database)
583
+ // This PubSub is shared by all collections and broadcasts ChangeEvents to reactive subscribers
584
+ const changePubSub = yield* PubSub.unbounded();
585
+ // 5. Build each collection with its Ref, indexes, and shared state refs
586
+ // Pass plugin registry data: custom operators, ID generators, and global hooks
587
+ // Pass the shared changePubSub so CRUD operations publish ChangeEvents
588
+ const collections = {};
589
+ for (const collectionName of Object.keys(config)) {
590
+ collections[collectionName] = buildCollection(collectionName, config[collectionName], typedRefs[collectionName], stateRefs, config, undefined, // afterMutation
591
+ collectionIndexes[collectionName], searchIndexRefs[collectionName], searchIndexFields[collectionName], pluginRegistry.operators, pluginRegistry.idGenerators, pluginRegistry.globalHooks, changePubSub);
592
+ }
593
+ // 5. Build transaction support
594
+ const buildCollectionForTx = makeBuildCollectionForTx(config, stateRefs, typedRefs, collectionIndexes, searchIndexRefs, searchIndexFields, pluginRegistry.operators, pluginRegistry.idGenerators, pluginRegistry.globalHooks);
595
+ // Create the $transaction method
596
+ const $transactionMethod = (fn) => $transactionImpl(stateRefs, transactionLock, buildCollectionForTx, undefined, // no persistence trigger for in-memory database
597
+ changePubSub, // shared PubSub for reactive change notifications
598
+ fn);
599
+ // Return database with $transaction method
600
+ return Object.assign(collections, {
601
+ $transaction: $transactionMethod,
602
+ });
603
+ });
604
+ /**
605
+ * Create an Effect-based in-memory database with persistence.
606
+ *
607
+ * Like `createEffectDatabase`, but additionally wires debounced persistence hooks
608
+ * so that each CRUD mutation triggers a fire-and-forget save to disk.
609
+ *
610
+ * Collections with a `file` field in their config are persisted. Collections
611
+ * without a `file` are in-memory only.
612
+ *
613
+ * Requires `StorageAdapter` and `SerializerRegistry` services in the environment.
614
+ *
615
+ * Optionally accepts plugins that provide custom codecs, operators, ID generators,
616
+ * and global lifecycle hooks.
617
+ *
618
+ * Usage:
619
+ * ```ts
620
+ * const db = yield* createPersistentEffectDatabase(config, initialData, { writeDebounce: 200 })
621
+ * // CRUD mutations now trigger debounced saves
622
+ * yield* db.users.create({ name: "Alice", age: 30 })
623
+ * // Flush all pending writes before shutdown
624
+ * yield* db.flush()
625
+ * ```
626
+ *
627
+ * With plugins:
628
+ * ```ts
629
+ * const db = yield* createPersistentEffectDatabase(config, initialData, persistenceConfig, {
630
+ * plugins: [regexPlugin, snowflakeIdPlugin]
631
+ * })
632
+ * ```
633
+ */
634
+ export const createPersistentEffectDatabase = (config, initialData, persistenceConfig, options) => Effect.gen(function* () {
635
+ // 0. Validate migration registries for all versioned collections at startup
636
+ for (const collectionName of Object.keys(config)) {
637
+ const collectionConfig = config[collectionName];
638
+ if (collectionConfig.version !== undefined) {
639
+ yield* validateMigrationRegistry(collectionName, collectionConfig.version, collectionConfig.migrations ?? []);
640
+ }
641
+ }
642
+ // 0b. Build plugin registry (validates plugins, merges contributions)
643
+ const pluginRegistry = yield* buildPluginRegistry(options?.plugins);
644
+ // 0c. Validate that all idGenerator references in collection configs exist
645
+ yield* validateIdGeneratorReferences(config, pluginRegistry.idGenerators);
646
+ // 0d. Run plugin initialize effects
647
+ for (const plugin of options?.plugins ?? []) {
648
+ if (plugin.initialize !== undefined) {
649
+ yield* plugin.initialize();
650
+ }
651
+ }
652
+ // 0e. Register plugin shutdown effects as scope finalizers.
653
+ // Register BEFORE the flush finalizer so they run AFTER flush (LIFO order).
654
+ // Iterate in registration order so that when finalizers run in LIFO order,
655
+ // plugins shut down in reverse registration order (last registered = first to shut down).
656
+ // This matches typical dependency patterns: if plugin A depends on plugin B,
657
+ // B is registered first, so A (registered later) shuts down first.
658
+ for (const plugin of options?.plugins ?? []) {
659
+ if (plugin.shutdown !== undefined) {
660
+ // Capture shutdown function to avoid optional chaining in the finalizer
661
+ const shutdownFn = plugin.shutdown;
662
+ yield* Effect.addFinalizer(() => shutdownFn().pipe(Effect.catchAll(() => Effect.void)));
663
+ }
664
+ }
665
+ // 1. Resolve services from the environment and capture as a Layer
666
+ // so save effects can be executed outside the creation runtime.
667
+ const storageAdapter = yield* StorageAdapter;
668
+ const baseSerializerRegistry = yield* SerializerRegistry;
669
+ // 1a. Merge plugin codecs with the base serializer registry
670
+ // Plugin codecs from the registry take precedence, followed by any codecs
671
+ // passed via _pluginCodecs (for backwards compatibility)
672
+ const allPluginCodecs = [
673
+ ...pluginRegistry.codecs,
674
+ ...(persistenceConfig?._pluginCodecs ?? []),
675
+ ];
676
+ const serializerRegistry = allPluginCodecs.length > 0
677
+ ? mergeSerializerWithPluginCodecs(baseSerializerRegistry, allPluginCodecs)
678
+ : baseSerializerRegistry;
679
+ const serviceLayer = Layer.merge(Layer.succeed(StorageAdapter, storageAdapter), Layer.succeed(SerializerRegistry, serializerRegistry));
680
+ // 2. Create transaction lock for single-writer isolation
681
+ const transactionLock = yield* Ref.make(false);
682
+ // 3. Load data from files for persistent collections, then merge with initialData.
683
+ // initialData takes precedence (allows overriding file data for testing/seeding).
684
+ const stateRefs = {};
685
+ const typedRefs = {};
686
+ for (const collectionName of Object.keys(config)) {
687
+ const collectionConfig = config[collectionName];
688
+ const filePath = collectionConfig.file;
689
+ // Load from file if configured, passing version and migrations for auto-migration
690
+ let loadedData = new Map();
691
+ if (filePath) {
692
+ // Only pass version options when collection is versioned
693
+ // Build options object conditionally to satisfy exactOptionalPropertyTypes
694
+ const loadOptions = collectionConfig.version !== undefined
695
+ ? collectionConfig.migrations !== undefined
696
+ ? {
697
+ version: collectionConfig.version,
698
+ migrations: collectionConfig.migrations,
699
+ collectionName,
700
+ }
701
+ : { version: collectionConfig.version, collectionName }
702
+ : undefined;
703
+ loadedData = yield* loadData(filePath, collectionConfig.schema, loadOptions);
704
+ }
705
+ // Merge with initialData (initialData takes precedence)
706
+ const providedItems = (initialData?.[collectionName] ??
707
+ []);
708
+ const mergedMap = new Map(loadedData);
709
+ for (const item of providedItems) {
710
+ mergedMap.set(item.id, item);
711
+ }
712
+ const ref = yield* Ref.make(mergedMap);
713
+ stateRefs[collectionName] = ref;
714
+ typedRefs[collectionName] = ref;
715
+ }
716
+ // 4. Build indexes for each collection from loaded/merged data
717
+ const collectionIndexes = {};
718
+ for (const collectionName of Object.keys(config)) {
719
+ const collectionConfig = config[collectionName];
720
+ const normalizedIndexes = normalizeIndexes(collectionConfig.indexes);
721
+ // Use actual data from the Ref (loaded from file + initialData)
722
+ const dataMap = yield* Ref.get(typedRefs[collectionName]);
723
+ const items = Array.from(dataMap.values());
724
+ const indexes = yield* buildIndexes(normalizedIndexes, items);
725
+ collectionIndexes[collectionName] = indexes;
726
+ }
727
+ // 4b. Build search indexes for collections that have searchIndex configured
728
+ const searchIndexRefs = {};
729
+ const searchIndexFields = {};
730
+ for (const collectionName of Object.keys(config)) {
731
+ const collectionConfig = config[collectionName];
732
+ if (collectionConfig.searchIndex &&
733
+ collectionConfig.searchIndex.length > 0) {
734
+ // Use actual data from the Ref (loaded from file + initialData)
735
+ const dataMap = yield* Ref.get(typedRefs[collectionName]);
736
+ const items = Array.from(dataMap.values());
737
+ const searchIdx = yield* buildSearchIndex(collectionConfig.searchIndex, items);
738
+ searchIndexRefs[collectionName] = searchIdx;
739
+ searchIndexFields[collectionName] = collectionConfig.searchIndex;
740
+ }
741
+ }
742
+ // 5. Build the save effect factory. Each save reads the Ref at execution
743
+ // time (capturing latest state) and writes through saveData with services.
744
+ const collectionFilePaths = {};
745
+ for (const collectionName of Object.keys(config)) {
746
+ const filePath = config[collectionName].file;
747
+ if (filePath) {
748
+ collectionFilePaths[collectionName] = filePath;
749
+ }
750
+ }
751
+ const makeSaveEffect = (collectionName) => {
752
+ const filePath = collectionFilePaths[collectionName];
753
+ if (!filePath)
754
+ return Effect.void;
755
+ const collectionConfig = config[collectionName];
756
+ return Effect.provide(Effect.gen(function* () {
757
+ const currentData = yield* Ref.get(typedRefs[collectionName]);
758
+ yield* saveData(filePath, collectionConfig.schema, currentData,
759
+ // Pass version option to stamp _version in output for versioned collections
760
+ collectionConfig.version !== undefined
761
+ ? { version: collectionConfig.version }
762
+ : undefined);
763
+ }), serviceLayer);
764
+ };
765
+ // 6. Create the runtime-independent persistence trigger
766
+ const trigger = createPersistenceTrigger(persistenceConfig?.writeDebounce ?? 100, makeSaveEffect);
767
+ // 7. Register scope finalizer: flush pending writes and shut down timers
768
+ yield* Effect.addFinalizer(() => Effect.promise(() => trigger.flush()).pipe(Effect.catchAll(() => Effect.void), Effect.tap(() => Effect.sync(() => trigger.shutdown()))));
769
+ // 8. Create shared PubSub for reactive change notifications (one per database)
770
+ // This PubSub is shared by all collections and broadcasts ChangeEvents to reactive subscribers
771
+ const changePubSub = yield* PubSub.unbounded();
772
+ // 9. Build each collection with its Ref, indexes, state refs, and persistence hooks
773
+ // Pass the shared changePubSub so CRUD operations publish ChangeEvents
774
+ const collections = {};
775
+ for (const collectionName of Object.keys(config)) {
776
+ const collectionConfig = config[collectionName];
777
+ const filePath = collectionConfig.file;
778
+ const isAppendOnly = collectionConfig.appendOnly === true;
779
+ // For append-only collections, afterMutation is undefined (no debounced full-file save).
780
+ // Instead, each create appends a single JSONL line via appendOnlyConfig.
781
+ const afterMutation = filePath && !isAppendOnly
782
+ ? () => Ref.get(transactionLock).pipe(Effect.flatMap((isLocked) => isLocked
783
+ ? Effect.void // Skip persistence during transactions
784
+ : Effect.sync(() => trigger.schedule(collectionName))))
785
+ : undefined;
786
+ // Build appendOnlyConfig for append-only persistent collections
787
+ let appendOnlyConfig;
788
+ if (isAppendOnly && filePath) {
789
+ const appendSchema = collectionConfig.schema;
790
+ appendOnlyConfig = {
791
+ onEntityCreated: (entity) => Effect.provide(Effect.gen(function* () {
792
+ const storage = yield* StorageAdapter;
793
+ const encode = Schema.encode(appendSchema);
794
+ const encoded = yield* encode(entity).pipe(Effect.catchAll(() => Effect.succeed(entity)));
795
+ const line = `${JSON.stringify(encoded)}\n`;
796
+ yield* storage.ensureDir(filePath);
797
+ yield* storage.append(filePath, line);
798
+ }).pipe(
799
+ // Catch storage errors to avoid propagating them to the caller.
800
+ // The entity was already created in memory; storage failure is logged but not fatal.
801
+ Effect.catchAll(() => Effect.void)), serviceLayer),
802
+ };
803
+ }
804
+ collections[collectionName] = buildCollection(collectionName, collectionConfig, typedRefs[collectionName], stateRefs, config, afterMutation, collectionIndexes[collectionName], searchIndexRefs[collectionName], searchIndexFields[collectionName], pluginRegistry.operators, pluginRegistry.idGenerators, pluginRegistry.globalHooks, changePubSub, appendOnlyConfig);
805
+ }
806
+ const db = collections;
807
+ // 10. Create file watchers for persistent collections to detect external file changes
808
+ // Each watcher monitors its file and reloads data into the Ref on changes,
809
+ // publishing a reload event to the changePubSub for reactive query subscribers.
810
+ // File watching is best-effort: if the storage adapter doesn't support watching
811
+ // (e.g., browser adapters in test environments), the database still functions
812
+ // without reactive file change detection.
813
+ for (const collectionName of Object.keys(config)) {
814
+ const collectionConfig = config[collectionName];
815
+ const filePath = collectionConfig.file;
816
+ if (filePath) {
817
+ yield* createFileWatcher({
818
+ filePath,
819
+ schema: collectionConfig.schema,
820
+ ref: typedRefs[collectionName],
821
+ changePubSub,
822
+ collectionName,
823
+ }).pipe(Effect.catchAll(() => Effect.void));
824
+ }
825
+ }
826
+ // Build the $dryRunMigrations method
827
+ const dryRunMigrationsFn = () => {
828
+ const effect = Effect.provide(dryRunMigrations(config, stateRefs), serviceLayer);
829
+ return withRunPromise(effect);
830
+ };
831
+ // Build transaction support
832
+ const buildCollectionForTx = makeBuildCollectionForTx(config, stateRefs, typedRefs, collectionIndexes, searchIndexRefs, searchIndexFields, pluginRegistry.operators, pluginRegistry.idGenerators, pluginRegistry.globalHooks);
833
+ // Create the $transaction method with persistence trigger
834
+ const $transactionMethod = (fn) => $transactionImpl(stateRefs, transactionLock, buildCollectionForTx, trigger, // persistence trigger for debounced saves on commit
835
+ changePubSub, // shared PubSub for reactive change notifications
836
+ fn);
837
+ // Build the flush method: flushes debounced writes AND writes canonical files
838
+ // for append-only collections (which don't use the debounced trigger).
839
+ const flushAll = async () => {
840
+ // Flush debounced writes for non-append-only collections
841
+ await trigger.flush();
842
+ // Write canonical JSONL files for append-only collections
843
+ const appendOnlyFlushes = [];
844
+ for (const collectionName of Object.keys(config)) {
845
+ const cc = config[collectionName];
846
+ if (cc.appendOnly && cc.file) {
847
+ appendOnlyFlushes.push(Effect.runPromise(makeSaveEffect(collectionName).pipe(Effect.catchAll(() => Effect.void))));
848
+ }
849
+ }
850
+ await Promise.all(appendOnlyFlushes);
851
+ };
852
+ return Object.assign(db, {
853
+ flush: flushAll,
854
+ pendingCount: () => trigger.pendingCount(),
855
+ $dryRunMigrations: dryRunMigrationsFn,
856
+ $transaction: $transactionMethod,
857
+ });
858
+ });
859
+ //# sourceMappingURL=database-effect.js.map