promidas 2.0.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 (295) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +179 -0
  3. package/dist/builder.d.ts +158 -0
  4. package/dist/builder.d.ts.map +1 -0
  5. package/dist/builder.js +255 -0
  6. package/dist/builder.js.map +1 -0
  7. package/dist/factory.d.ts +154 -0
  8. package/dist/factory.d.ts.map +1 -0
  9. package/dist/factory.js +243 -0
  10. package/dist/factory.js.map +1 -0
  11. package/dist/fetcher/client/config.d.ts +140 -0
  12. package/dist/fetcher/client/config.d.ts.map +1 -0
  13. package/dist/fetcher/client/config.js +2 -0
  14. package/dist/fetcher/client/config.js.map +1 -0
  15. package/dist/fetcher/client/fetch-with-progress.d.ts +156 -0
  16. package/dist/fetcher/client/fetch-with-progress.d.ts.map +1 -0
  17. package/dist/fetcher/client/fetch-with-progress.js +313 -0
  18. package/dist/fetcher/client/fetch-with-progress.js.map +1 -0
  19. package/dist/fetcher/client/fetch-with-timeout.d.ts +6 -0
  20. package/dist/fetcher/client/fetch-with-timeout.d.ts.map +1 -0
  21. package/dist/fetcher/client/fetch-with-timeout.js +48 -0
  22. package/dist/fetcher/client/fetch-with-timeout.js.map +1 -0
  23. package/dist/fetcher/client/protopedia-api-custom-client.d.ts +141 -0
  24. package/dist/fetcher/client/protopedia-api-custom-client.d.ts.map +1 -0
  25. package/dist/fetcher/client/protopedia-api-custom-client.js +268 -0
  26. package/dist/fetcher/client/protopedia-api-custom-client.js.map +1 -0
  27. package/dist/fetcher/client/select-custom-fetch.d.ts +58 -0
  28. package/dist/fetcher/client/select-custom-fetch.d.ts.map +1 -0
  29. package/dist/fetcher/client/select-custom-fetch.js +58 -0
  30. package/dist/fetcher/client/select-custom-fetch.js.map +1 -0
  31. package/dist/fetcher/errors/fetcher-error.d.ts +10 -0
  32. package/dist/fetcher/errors/fetcher-error.d.ts.map +1 -0
  33. package/dist/fetcher/errors/fetcher-error.js +15 -0
  34. package/dist/fetcher/errors/fetcher-error.js.map +1 -0
  35. package/dist/fetcher/index.d.ts +73 -0
  36. package/dist/fetcher/index.d.ts.map +1 -0
  37. package/dist/fetcher/index.js +70 -0
  38. package/dist/fetcher/index.js.map +1 -0
  39. package/dist/fetcher/types/index.d.ts +9 -0
  40. package/dist/fetcher/types/index.d.ts.map +1 -0
  41. package/dist/fetcher/types/index.js +7 -0
  42. package/dist/fetcher/types/index.js.map +1 -0
  43. package/dist/fetcher/types/progress-event.types.d.ts +221 -0
  44. package/dist/fetcher/types/progress-event.types.d.ts.map +1 -0
  45. package/dist/fetcher/types/progress-event.types.js +10 -0
  46. package/dist/fetcher/types/progress-event.types.js.map +1 -0
  47. package/dist/fetcher/types/prototype-api.types.d.ts +106 -0
  48. package/dist/fetcher/types/prototype-api.types.d.ts.map +1 -0
  49. package/dist/fetcher/types/prototype-api.types.js +2 -0
  50. package/dist/fetcher/types/prototype-api.types.js.map +1 -0
  51. package/dist/fetcher/types/result.types.d.ts +75 -0
  52. package/dist/fetcher/types/result.types.d.ts.map +1 -0
  53. package/dist/fetcher/types/result.types.js +2 -0
  54. package/dist/fetcher/types/result.types.js.map +1 -0
  55. package/dist/fetcher/utils/create-client-fetch.d.ts +63 -0
  56. package/dist/fetcher/utils/create-client-fetch.d.ts.map +1 -0
  57. package/dist/fetcher/utils/create-client-fetch.js +89 -0
  58. package/dist/fetcher/utils/create-client-fetch.js.map +1 -0
  59. package/dist/fetcher/utils/create-fetch-with-stripped-headers.d.ts +6 -0
  60. package/dist/fetcher/utils/create-fetch-with-stripped-headers.d.ts.map +1 -0
  61. package/dist/fetcher/utils/create-fetch-with-stripped-headers.js +40 -0
  62. package/dist/fetcher/utils/create-fetch-with-stripped-headers.js.map +1 -0
  63. package/dist/fetcher/utils/errors/handler.d.ts +58 -0
  64. package/dist/fetcher/utils/errors/handler.d.ts.map +1 -0
  65. package/dist/fetcher/utils/errors/handler.js +243 -0
  66. package/dist/fetcher/utils/errors/handler.js.map +1 -0
  67. package/dist/fetcher/utils/errors/messages.d.ts +75 -0
  68. package/dist/fetcher/utils/errors/messages.d.ts.map +1 -0
  69. package/dist/fetcher/utils/errors/messages.js +88 -0
  70. package/dist/fetcher/utils/errors/messages.js.map +1 -0
  71. package/dist/fetcher/utils/index.d.ts +13 -0
  72. package/dist/fetcher/utils/index.d.ts.map +1 -0
  73. package/dist/fetcher/utils/index.js +12 -0
  74. package/dist/fetcher/utils/index.js.map +1 -0
  75. package/dist/fetcher/utils/log-timestamp-normalization-warnings.d.ts +10 -0
  76. package/dist/fetcher/utils/log-timestamp-normalization-warnings.d.ts.map +1 -0
  77. package/dist/fetcher/utils/log-timestamp-normalization-warnings.js +32 -0
  78. package/dist/fetcher/utils/log-timestamp-normalization-warnings.js.map +1 -0
  79. package/dist/fetcher/utils/normalize-protopedia-timestamp.d.ts +59 -0
  80. package/dist/fetcher/utils/normalize-protopedia-timestamp.d.ts.map +1 -0
  81. package/dist/fetcher/utils/normalize-protopedia-timestamp.js +81 -0
  82. package/dist/fetcher/utils/normalize-protopedia-timestamp.js.map +1 -0
  83. package/dist/fetcher/utils/normalize-prototype.d.ts +56 -0
  84. package/dist/fetcher/utils/normalize-prototype.d.ts.map +1 -0
  85. package/dist/fetcher/utils/normalize-prototype.js +113 -0
  86. package/dist/fetcher/utils/normalize-prototype.js.map +1 -0
  87. package/dist/fetcher/utils/sanitize-options.d.ts +14 -0
  88. package/dist/fetcher/utils/sanitize-options.d.ts.map +1 -0
  89. package/dist/fetcher/utils/sanitize-options.js +16 -0
  90. package/dist/fetcher/utils/sanitize-options.js.map +1 -0
  91. package/dist/fetcher/utils/string-parsers.d.ts +45 -0
  92. package/dist/fetcher/utils/string-parsers.d.ts.map +1 -0
  93. package/dist/fetcher/utils/string-parsers.js +53 -0
  94. package/dist/fetcher/utils/string-parsers.js.map +1 -0
  95. package/dist/index.d.ts +66 -0
  96. package/dist/index.d.ts.map +1 -0
  97. package/dist/index.js +70 -0
  98. package/dist/index.js.map +1 -0
  99. package/dist/logger/console-logger.d.ts +74 -0
  100. package/dist/logger/console-logger.d.ts.map +1 -0
  101. package/dist/logger/console-logger.js +113 -0
  102. package/dist/logger/console-logger.js.map +1 -0
  103. package/dist/logger/factory.d.ts +88 -0
  104. package/dist/logger/factory.d.ts.map +1 -0
  105. package/dist/logger/factory.js +94 -0
  106. package/dist/logger/factory.js.map +1 -0
  107. package/dist/logger/index.d.ts +42 -0
  108. package/dist/logger/index.d.ts.map +1 -0
  109. package/dist/logger/index.js +41 -0
  110. package/dist/logger/index.js.map +1 -0
  111. package/dist/logger/logger.types.d.ts +49 -0
  112. package/dist/logger/logger.types.d.ts.map +1 -0
  113. package/dist/logger/logger.types.js +2 -0
  114. package/dist/logger/logger.types.js.map +1 -0
  115. package/dist/repository/errors/validation-error.d.ts +24 -0
  116. package/dist/repository/errors/validation-error.d.ts.map +1 -0
  117. package/dist/repository/errors/validation-error.js +26 -0
  118. package/dist/repository/errors/validation-error.js.map +1 -0
  119. package/dist/repository/index.d.ts +122 -0
  120. package/dist/repository/index.d.ts.map +1 -0
  121. package/dist/repository/index.js +44 -0
  122. package/dist/repository/index.js.map +1 -0
  123. package/dist/repository/protopedia-in-memory-repository.d.ts +560 -0
  124. package/dist/repository/protopedia-in-memory-repository.d.ts.map +1 -0
  125. package/dist/repository/protopedia-in-memory-repository.js +929 -0
  126. package/dist/repository/protopedia-in-memory-repository.js.map +1 -0
  127. package/dist/repository/schemas/index.d.ts +9 -0
  128. package/dist/repository/schemas/index.d.ts.map +1 -0
  129. package/dist/repository/schemas/index.js +11 -0
  130. package/dist/repository/schemas/index.js.map +1 -0
  131. package/dist/repository/schemas/params.d.ts +44 -0
  132. package/dist/repository/schemas/params.d.ts.map +1 -0
  133. package/dist/repository/schemas/params.js +44 -0
  134. package/dist/repository/schemas/params.js.map +1 -0
  135. package/dist/repository/schemas/serializable-snapshot.d.ts +33 -0
  136. package/dist/repository/schemas/serializable-snapshot.d.ts.map +1 -0
  137. package/dist/repository/schemas/serializable-snapshot.js +45 -0
  138. package/dist/repository/schemas/serializable-snapshot.js.map +1 -0
  139. package/dist/repository/types/analysis.types.d.ts +89 -0
  140. package/dist/repository/types/analysis.types.d.ts.map +1 -0
  141. package/dist/repository/types/analysis.types.js +2 -0
  142. package/dist/repository/types/analysis.types.js.map +1 -0
  143. package/dist/repository/types/index.d.ts +12 -0
  144. package/dist/repository/types/index.d.ts.map +1 -0
  145. package/dist/repository/types/index.js +7 -0
  146. package/dist/repository/types/index.js.map +1 -0
  147. package/dist/repository/types/repository-events.types.d.ts +110 -0
  148. package/dist/repository/types/repository-events.types.d.ts.map +1 -0
  149. package/dist/repository/types/repository-events.types.js +2 -0
  150. package/dist/repository/types/repository-events.types.js.map +1 -0
  151. package/dist/repository/types/repository.types.d.ts +330 -0
  152. package/dist/repository/types/repository.types.d.ts.map +1 -0
  153. package/dist/repository/types/repository.types.js +2 -0
  154. package/dist/repository/types/repository.types.js.map +1 -0
  155. package/dist/repository/types/result.types.d.ts +55 -0
  156. package/dist/repository/types/result.types.d.ts.map +1 -0
  157. package/dist/repository/types/result.types.js +2 -0
  158. package/dist/repository/types/result.types.js.map +1 -0
  159. package/dist/repository/types/serialization.types.d.ts +61 -0
  160. package/dist/repository/types/serialization.types.d.ts.map +1 -0
  161. package/dist/repository/types/serialization.types.js +2 -0
  162. package/dist/repository/types/serialization.types.js.map +1 -0
  163. package/dist/repository/types/snapshot-operation.types.d.ts +140 -0
  164. package/dist/repository/types/snapshot-operation.types.d.ts.map +1 -0
  165. package/dist/repository/types/snapshot-operation.types.js +2 -0
  166. package/dist/repository/types/snapshot-operation.types.js.map +1 -0
  167. package/dist/repository/utils/convert-fetch-result.d.ts +46 -0
  168. package/dist/repository/utils/convert-fetch-result.d.ts.map +1 -0
  169. package/dist/repository/utils/convert-fetch-result.js +59 -0
  170. package/dist/repository/utils/convert-fetch-result.js.map +1 -0
  171. package/dist/repository/utils/convert-store-result.d.ts +36 -0
  172. package/dist/repository/utils/convert-store-result.d.ts.map +1 -0
  173. package/dist/repository/utils/convert-store-result.js +36 -0
  174. package/dist/repository/utils/convert-store-result.js.map +1 -0
  175. package/dist/repository/utils/emit-repository-event-safely.d.ts +5 -0
  176. package/dist/repository/utils/emit-repository-event-safely.d.ts.map +1 -0
  177. package/dist/repository/utils/emit-repository-event-safely.js +17 -0
  178. package/dist/repository/utils/emit-repository-event-safely.js.map +1 -0
  179. package/dist/repository/utils/index.d.ts +3 -0
  180. package/dist/repository/utils/index.d.ts.map +1 -0
  181. package/dist/repository/utils/index.js +3 -0
  182. package/dist/repository/utils/index.js.map +1 -0
  183. package/dist/repository/validation/index.d.ts +9 -0
  184. package/dist/repository/validation/index.d.ts.map +1 -0
  185. package/dist/repository/validation/index.js +10 -0
  186. package/dist/repository/validation/index.js.map +1 -0
  187. package/dist/repository/validation/params-validators.d.ts +46 -0
  188. package/dist/repository/validation/params-validators.d.ts.map +1 -0
  189. package/dist/repository/validation/params-validators.js +68 -0
  190. package/dist/repository/validation/params-validators.js.map +1 -0
  191. package/dist/repository/validation/serializable-snapshot.d.ts +47 -0
  192. package/dist/repository/validation/serializable-snapshot.d.ts.map +1 -0
  193. package/dist/repository/validation/serializable-snapshot.js +104 -0
  194. package/dist/repository/validation/serializable-snapshot.js.map +1 -0
  195. package/dist/schemas/index.d.ts +8 -0
  196. package/dist/schemas/index.d.ts.map +1 -0
  197. package/dist/schemas/index.js +8 -0
  198. package/dist/schemas/index.js.map +1 -0
  199. package/dist/schemas/normalized-prototype.d.ts +56 -0
  200. package/dist/schemas/normalized-prototype.d.ts.map +1 -0
  201. package/dist/schemas/normalized-prototype.js +123 -0
  202. package/dist/schemas/normalized-prototype.js.map +1 -0
  203. package/dist/store/errors/store-error.d.ts +148 -0
  204. package/dist/store/errors/store-error.d.ts.map +1 -0
  205. package/dist/store/errors/store-error.js +156 -0
  206. package/dist/store/errors/store-error.js.map +1 -0
  207. package/dist/store/index.d.ts +84 -0
  208. package/dist/store/index.d.ts.map +1 -0
  209. package/dist/store/index.js +83 -0
  210. package/dist/store/index.js.map +1 -0
  211. package/dist/store/store.d.ts +295 -0
  212. package/dist/store/store.d.ts.map +1 -0
  213. package/dist/store/store.js +411 -0
  214. package/dist/store/store.js.map +1 -0
  215. package/dist/store/types/index.d.ts +2 -0
  216. package/dist/store/types/index.d.ts.map +1 -0
  217. package/dist/store/types/index.js +2 -0
  218. package/dist/store/types/index.js.map +1 -0
  219. package/dist/store/types/result.types.d.ts +67 -0
  220. package/dist/store/types/result.types.d.ts.map +1 -0
  221. package/dist/store/types/result.types.js +2 -0
  222. package/dist/store/types/result.types.js.map +1 -0
  223. package/dist/types/codes.d.ts +44 -0
  224. package/dist/types/codes.d.ts.map +1 -0
  225. package/dist/types/codes.js +9 -0
  226. package/dist/types/codes.js.map +1 -0
  227. package/dist/types/index.d.ts +61 -0
  228. package/dist/types/index.d.ts.map +1 -0
  229. package/dist/types/index.js +60 -0
  230. package/dist/types/index.js.map +1 -0
  231. package/dist/types/normalized-prototype.d.ts +95 -0
  232. package/dist/types/normalized-prototype.d.ts.map +1 -0
  233. package/dist/types/normalized-prototype.js +2 -0
  234. package/dist/types/normalized-prototype.js.map +1 -0
  235. package/dist/utils/converters/index.d.ts +15 -0
  236. package/dist/utils/converters/index.d.ts.map +1 -0
  237. package/dist/utils/converters/index.js +15 -0
  238. package/dist/utils/converters/index.js.map +1 -0
  239. package/dist/utils/converters/license-type.d.ts +23 -0
  240. package/dist/utils/converters/license-type.d.ts.map +1 -0
  241. package/dist/utils/converters/license-type.js +38 -0
  242. package/dist/utils/converters/license-type.js.map +1 -0
  243. package/dist/utils/converters/release-flag.d.ts +24 -0
  244. package/dist/utils/converters/release-flag.d.ts.map +1 -0
  245. package/dist/utils/converters/release-flag.js +40 -0
  246. package/dist/utils/converters/release-flag.js.map +1 -0
  247. package/dist/utils/converters/status.d.ts +23 -0
  248. package/dist/utils/converters/status.d.ts.map +1 -0
  249. package/dist/utils/converters/status.js +40 -0
  250. package/dist/utils/converters/status.js.map +1 -0
  251. package/dist/utils/converters/thanks-flag.d.ts +25 -0
  252. package/dist/utils/converters/thanks-flag.d.ts.map +1 -0
  253. package/dist/utils/converters/thanks-flag.js +41 -0
  254. package/dist/utils/converters/thanks-flag.js.map +1 -0
  255. package/dist/utils/deep-merge.d.ts +38 -0
  256. package/dist/utils/deep-merge.d.ts.map +1 -0
  257. package/dist/utils/deep-merge.js +85 -0
  258. package/dist/utils/deep-merge.js.map +1 -0
  259. package/dist/utils/index.d.ts +80 -0
  260. package/dist/utils/index.d.ts.map +1 -0
  261. package/dist/utils/index.js +85 -0
  262. package/dist/utils/index.js.map +1 -0
  263. package/dist/utils/logger-utils.d.ts +100 -0
  264. package/dist/utils/logger-utils.d.ts.map +1 -0
  265. package/dist/utils/logger-utils.js +265 -0
  266. package/dist/utils/logger-utils.js.map +1 -0
  267. package/dist/utils/time/constants.d.ts +14 -0
  268. package/dist/utils/time/constants.d.ts.map +1 -0
  269. package/dist/utils/time/constants.js +14 -0
  270. package/dist/utils/time/constants.js.map +1 -0
  271. package/dist/utils/time/index.d.ts +28 -0
  272. package/dist/utils/time/index.d.ts.map +1 -0
  273. package/dist/utils/time/index.js +28 -0
  274. package/dist/utils/time/index.js.map +1 -0
  275. package/dist/utils/time/parser.d.ts +91 -0
  276. package/dist/utils/time/parser.d.ts.map +1 -0
  277. package/dist/utils/time/parser.js +143 -0
  278. package/dist/utils/time/parser.js.map +1 -0
  279. package/dist/utils/validation/index.d.ts +8 -0
  280. package/dist/utils/validation/index.d.ts.map +1 -0
  281. package/dist/utils/validation/index.js +7 -0
  282. package/dist/utils/validation/index.js.map +1 -0
  283. package/dist/utils/validation/normalized-prototype.d.ts +64 -0
  284. package/dist/utils/validation/normalized-prototype.d.ts.map +1 -0
  285. package/dist/utils/validation/normalized-prototype.js +97 -0
  286. package/dist/utils/validation/normalized-prototype.js.map +1 -0
  287. package/dist/utils/validation/types.d.ts +62 -0
  288. package/dist/utils/validation/types.d.ts.map +1 -0
  289. package/dist/utils/validation/types.js +8 -0
  290. package/dist/utils/validation/types.js.map +1 -0
  291. package/dist/version.d.ts +6 -0
  292. package/dist/version.d.ts.map +1 -0
  293. package/dist/version.js +6 -0
  294. package/dist/version.js.map +1 -0
  295. package/package.json +138 -0
@@ -0,0 +1,929 @@
1
+ /**
2
+ * In-memory repository implementation for ProtoPedia prototypes.
3
+ *
4
+ * This module contains the concrete implementation of the repository pattern
5
+ * for managing ProtoPedia prototype data in memory with snapshot-based access.
6
+ *
7
+ * ## Architecture
8
+ *
9
+ * The {@link ProtopediaInMemoryRepositoryImpl} class orchestrates:
10
+ *
11
+ * - **API Client**: ProtoPedia API v2 client for fetching prototype data
12
+ * - **Memory Store**: {@link PrototypeInMemoryStore} for snapshot management
13
+ * - **Repository Interface**: {@link ProtopediaInMemoryRepository} for high-level operations
14
+ *
15
+ * ## Design Patterns
16
+ *
17
+ * ### Repository Pattern
18
+ * Abstracts data access behind a clean interface, isolating business logic
19
+ * from data fetching and storage mechanisms.
20
+ *
21
+ * ### Snapshot Isolation
22
+ * All read operations work against an immutable in-memory snapshot.
23
+ * Network I/O only occurs during explicit setup/refresh operations.
24
+ *
25
+ * ### Private Fields
26
+ * Uses ECMAScript private fields (#) for proper encapsulation:
27
+ * - `#store` - Internal memory store instance
28
+ * - `#apiClient` - HTTP client for ProtoPedia API
29
+ * - `#lastFetchParams` - Cache of last fetch parameters
30
+ *
31
+ * ## Performance Optimizations
32
+ *
33
+ * 1. **O(1) Lookups**: Direct Map access by prototype ID
34
+ * 2. **Hybrid Sampling**: Adaptive algorithm based on sample size ratio
35
+ * - Small samples (< 50%): Set-based random selection
36
+ * - Large samples (≥ 50%): Fisher-Yates shuffle
37
+ * 3. **Efficient Checks**: Use `store.size` instead of array operations
38
+ * 4. **Parameter Validation**: Zod schemas for runtime type safety
39
+ *
40
+ * ## Usage Recommendation
41
+ *
42
+ * For normal usage, import from `promidas` instead of direct instantiation:
43
+ * - `createPromidasForLocal()` for local development
44
+ * - `createPromidasForServer()` for server environments
45
+ * - `PromidasRepositoryBuilder` for advanced customization
46
+ *
47
+ * Direct instantiation is only recommended for:
48
+ * - Testing scenarios
49
+ * - Advanced customization needs
50
+ * - Framework integration
51
+ *
52
+ * @module
53
+ * @see {@link ProtopediaInMemoryRepository} for the public interface
54
+ */
55
+ import { EventEmitter } from 'events';
56
+ import { ProtopediaApiCustomClient } from '../fetcher/index.js';
57
+ import { ConsoleLogger } from '../logger/index.js';
58
+ import { DataSizeExceededError, PrototypeInMemoryStore, SizeEstimationError, } from '../store/index.js';
59
+ import { sanitizeDataForLogging } from '../utils/index.js';
60
+ import { ValidationError } from './errors/validation-error.js';
61
+ import { emitRepositoryEventSafely } from './utils/emit-repository-event-safely.js';
62
+ import { convertFetchResult, convertStoreResult } from './utils/index.js';
63
+ import { RepositoryParamsValidator, validateSerializableSnapshot, } from './validation/index.js';
64
+ const DEFAULT_FETCH_PARAMS = {
65
+ offset: 0,
66
+ limit: 10,
67
+ };
68
+ /**
69
+ * Version identifier for snapshot serialization format.
70
+ * Increment when making incompatible changes to SerializableSnapshot schema.
71
+ */
72
+ const SNAPSHOT_SERIALIZATION_VERSION = '1.0.0';
73
+ /**
74
+ * Threshold ratio for choosing sampling strategy in getRandomSampleFromSnapshot.
75
+ *
76
+ * When requested sample size exceeds this ratio of total items,
77
+ * use "Fisher-Yates shuffle" instead of "Set-based random selection" for better performance.
78
+ *
79
+ * @example
80
+ * // With 100 items total:
81
+ * // - size=40 (40%) → Set-based random selection (O(size))
82
+ * // - size=60 (60%) → Fisher-Yates shuffle (O(n))
83
+ */
84
+ const SAMPLE_SIZE_THRESHOLD_RATIO = 0.5;
85
+ /**
86
+ * Implementation class for the ProtoPedia in-memory repository.
87
+ *
88
+ * This class provides the concrete implementation of {@link ProtopediaInMemoryRepository}
89
+ * with full encapsulation using ECMAScript private fields.
90
+ *
91
+ * ## Responsibilities
92
+ *
93
+ * 1. **Dependency Management**
94
+ * - Receives {@link PrototypeInMemoryStore} and {@link ProtopediaApiCustomClient} via DI
95
+ * - Maintains internal state for fetch parameters
96
+ *
97
+ * 2. **Snapshot Operations**
98
+ * - {@link setupSnapshot} - Initial data fetch and population
99
+ * - {@link refreshSnapshot} - Update snapshot with fresh API data
100
+ *
101
+ * 3. **Data Access**
102
+ * - {@link getAllFromSnapshot} - Retrieve all prototypes
103
+ * - {@link getPrototypeFromSnapshotByPrototypeId} - Fast ID lookup
104
+ * - {@link getRandomPrototypeFromSnapshot} - Single random sample
105
+ * - {@link getRandomSampleFromSnapshot} - Multiple random samples
106
+ * - {@link getPrototypeIdsFromSnapshot} - Get all IDs efficiently
107
+ *
108
+ * 4. **Analysis & Metadata**
109
+ * - {@link analyzePrototypes} - Statistical analysis (min/max)
110
+ * - {@link getStats} - Snapshot statistics and TTL info
111
+ * - {@link getConfig} - Store configuration details
112
+ *
113
+ * ## Implementation Details
114
+ *
115
+ * ### Validation
116
+ * All public methods validate their parameters using {@link RepositoryParamsValidator}:
117
+ * - {@link RepositoryParamsValidator.validatePrototypeId} - Ensures valid prototype IDs
118
+ * - {@link RepositoryParamsValidator.validateSampleSize} - Ensures valid sample sizes
119
+ *
120
+ * ### Error Handling
121
+ * Network operations return {@link SnapshotOperationResult}:
122
+ * - `{ ok: true, ... }` - Success with metadata
123
+ * - `{ ok: false, error: string }` - Failure with error message
124
+ *
125
+ * ### Performance
126
+ * - Uses `store.size` for O(1) empty checks
127
+ * - Implements hybrid sampling algorithm (see {@link SAMPLE_SIZE_THRESHOLD_RATIO})
128
+ * - Returns read-only data to prevent accidental mutations
129
+ *
130
+ * ## Usage
131
+ *
132
+ * **Recommended**: Import from `promidas` instead of direct instantiation
133
+ *
134
+ * **Direct instantiation** (advanced):
135
+ * ```typescript
136
+ * // Dependencies must be created manually
137
+ * const store = new PrototypeInMemoryStore({ ... });
138
+ * const client = new ProtopediaApiCustomClient({ ... });
139
+ *
140
+ * const repo = new ProtopediaInMemoryRepositoryImpl({
141
+ * store,
142
+ * apiClient: client
143
+ * });
144
+ * ```
145
+ *
146
+ * @see {@link ProtopediaInMemoryRepository} for the public interface contract
147
+ */
148
+ export class ProtopediaInMemoryRepositoryImpl {
149
+ /**
150
+ * Event emitter for snapshot operation notifications.
151
+ *
152
+ * Only defined when enableEvents: true is set in repository configuration.
153
+ */
154
+ events;
155
+ /**
156
+ * Internal logger instance.
157
+ */
158
+ #logger;
159
+ /**
160
+ * Log level for the repository.
161
+ */
162
+ #logLevel;
163
+ /**
164
+ * Underlying in-memory store instance.
165
+ */
166
+ #store;
167
+ /**
168
+ * Underlying API client instance.
169
+ */
170
+ #apiClient;
171
+ /**
172
+ * Cache of the last successful fetch parameters.
173
+ *
174
+ * - `undefined`: No API fetch has been performed, or reset after loading serialized data
175
+ * - `ListPrototypesParams`: Parameters from the last successful setupSnapshot() call
176
+ */
177
+ #lastFetchParams = undefined;
178
+ /**
179
+ * Ongoing fetch promise for concurrency control.
180
+ */
181
+ #ongoingFetch = null;
182
+ /**
183
+ * Creates a new ProtoPedia in-memory repository instance.
184
+ *
185
+ * @param dependencies - Dependency injection object
186
+ * @param dependencies.store - Pre-configured in-memory store instance
187
+ * @param dependencies.apiClient - Pre-configured API client instance
188
+ * @param dependencies.repositoryConfig - Repository-level configuration (optional)
189
+ */
190
+ constructor({ store, apiClient, repositoryConfig = {}, }) {
191
+ // Fastify-style logger configuration for repository
192
+ const { logger, logLevel, enableEvents } = repositoryConfig;
193
+ if (logger) {
194
+ this.#logger = logger;
195
+ this.#logLevel = logLevel ?? 'info';
196
+ // If logLevel is specified, update logger's level property (if mutable)
197
+ if (logLevel !== undefined && 'level' in logger) {
198
+ logger.level = logLevel;
199
+ }
200
+ }
201
+ else {
202
+ const resolvedLogLevel = logLevel ?? 'info';
203
+ this.#logger = new ConsoleLogger(resolvedLogLevel);
204
+ this.#logLevel = resolvedLogLevel;
205
+ }
206
+ // Initialize event emitter if events are enabled
207
+ if (enableEvents === true) {
208
+ this.events = new EventEmitter();
209
+ this.events.setMaxListeners(0); // Allow unlimited listeners
210
+ }
211
+ this.#logger.info('ProtopediaInMemoryRepository constructor called', {
212
+ repositoryConfig: sanitizeDataForLogging(repositoryConfig),
213
+ storeConfig: store.getConfig(),
214
+ eventsEnabled: enableEvents === true,
215
+ });
216
+ this.#store = store;
217
+ this.#apiClient = apiClient;
218
+ }
219
+ /**
220
+ * Fetch and normalize prototypes from the ProtoPedia API.
221
+ *
222
+ * @param params - Fetch parameters to merge with {@link DEFAULT_FETCH_PARAMS}
223
+ * @returns {@link FetchPrototypesResult} from the API client
224
+ *
225
+ * @remarks
226
+ * This method delegates directly to the API client's `fetchPrototypes()` method,
227
+ * which handles all error cases and returns them as {@link FetchPrototypesFailure}
228
+ * instead of throwing exceptions.
229
+ *
230
+ * **Responsibility Separation**:
231
+ * - This method only fetches and normalizes data
232
+ * - The caller is responsible for storing the data via {@link storeSnapshot}
233
+ * - The caller must update lastFetchParams on successful storage
234
+ *
235
+ * **Error Handling**:
236
+ * - The API client never throws exceptions under normal operation
237
+ * - The try-catch block is defensive programming for unexpected cases
238
+ * - All expected errors are returned as part of {@link FetchPrototypesResult}
239
+ *
240
+ * **Logging**:
241
+ * - Success: `debug` level with fetch count and parameters
242
+ * - Failure: No logging (API client layer already logs failures)
243
+ * - Unexpected exceptions: `error` level (defensive fallback)
244
+ *
245
+ * @see {@link ProtopediaApiCustomClient.fetchPrototypes} for API client implementation
246
+ * @internal
247
+ */
248
+ async fetchAndNormalize(params) {
249
+ const mergedParams = {
250
+ ...DEFAULT_FETCH_PARAMS,
251
+ ...params,
252
+ };
253
+ try {
254
+ const result = await this.#apiClient.fetchPrototypes(mergedParams);
255
+ // Log success for diagnostics
256
+ if (result.ok) {
257
+ this.#logger.debug('Fetch operation completed', {
258
+ count: result.data.length,
259
+ params: mergedParams,
260
+ });
261
+ }
262
+ return result;
263
+ }
264
+ catch (error) {
265
+ // This should never happen as the API client catches all errors,
266
+ // but we handle it defensively in case of unexpected exceptions.
267
+ this.#logger.error('Unexpected exception from API client', {
268
+ error: sanitizeDataForLogging(error),
269
+ params: mergedParams,
270
+ });
271
+ return {
272
+ ok: false,
273
+ origin: 'fetcher',
274
+ kind: 'unknown',
275
+ code: 'UNKNOWN',
276
+ error: error instanceof Error ? error.message : String(error),
277
+ details: {
278
+ req: {
279
+ method: 'GET',
280
+ },
281
+ },
282
+ };
283
+ }
284
+ }
285
+ /**
286
+ * Store normalized prototypes in the in-memory snapshot.
287
+ *
288
+ * This private method handles:
289
+ * - Storing data via the memory store
290
+ * - Converting store errors to {@link SetFailure}
291
+ * - Logging detailed operation information with sanitized data
292
+ *
293
+ * @param data - Array of normalized prototypes to store
294
+ * @returns {@link SetResult} - Success with stats or failure details
295
+ *
296
+ * @remarks
297
+ * **Error Handling**:
298
+ * - {@link DataSizeExceededError} → {@link SetFailure} with kind='storage_limit'
299
+ * - {@link SizeEstimationError} → {@link SetFailure} with kind='serialization'
300
+ * - Unexpected errors → {@link SetFailure} with kind='unknown'
301
+ *
302
+ * All store errors include `dataState` to indicate whether existing data was preserved.
303
+ *
304
+ * **Logging**:
305
+ * - Success: `debug` level with store size and data size
306
+ * - DataSizeExceededError: `warn` level (configuration issue)
307
+ * - SizeEstimationError: `error` level (serialization failure)
308
+ * - Unexpected errors: `error` level (unknown failures)
309
+ *
310
+ * @internal
311
+ */
312
+ storeSnapshot(data) {
313
+ try {
314
+ this.#store.setAll(data);
315
+ const stats = this.#store.getStats();
316
+ this.#logger.debug('Store operation completed', {
317
+ size: stats.size,
318
+ dataSizeBytes: stats.dataSizeBytes,
319
+ });
320
+ return {
321
+ ok: true,
322
+ stats,
323
+ };
324
+ }
325
+ catch (error) {
326
+ // Log detailed Store error information
327
+ if (error instanceof DataSizeExceededError) {
328
+ this.#logger.warn('Snapshot storage failed: data size exceeded', {
329
+ dataSizeBytes: error.dataSizeBytes,
330
+ maxDataSizeBytes: error.maxDataSizeBytes,
331
+ dataState: error.dataState,
332
+ });
333
+ return {
334
+ ok: false,
335
+ origin: 'store',
336
+ kind: 'storage_limit',
337
+ code: 'STORE_CAPACITY_EXCEEDED',
338
+ message: `Data size ${error.dataSizeBytes} bytes exceeds maximum ${error.maxDataSizeBytes} bytes`,
339
+ dataState: error.dataState,
340
+ };
341
+ }
342
+ else if (error instanceof SizeEstimationError) {
343
+ this.#logger.error('Snapshot storage failed: size estimation error', {
344
+ dataState: error.dataState,
345
+ cause: sanitizeDataForLogging(error.cause),
346
+ });
347
+ return {
348
+ ok: false,
349
+ origin: 'store',
350
+ kind: 'serialization',
351
+ code: 'STORE_SERIALIZATION_FAILED',
352
+ message: 'Failed to estimate data size during serialization',
353
+ dataState: error.dataState,
354
+ cause: sanitizeDataForLogging(error.cause),
355
+ };
356
+ }
357
+ else {
358
+ // Unexpected store error - map to unknown store error
359
+ this.#logger.error('Snapshot storage failed: unexpected error', {
360
+ error: sanitizeDataForLogging(error),
361
+ });
362
+ return {
363
+ ok: false,
364
+ origin: 'store',
365
+ kind: 'unknown',
366
+ code: 'STORE_UNKNOWN',
367
+ message: error instanceof Error ? error.message : String(error),
368
+ dataState: 'UNKNOWN',
369
+ cause: sanitizeDataForLogging(error),
370
+ };
371
+ }
372
+ }
373
+ }
374
+ /**
375
+ * Return the configuration used to initialize the underlying store.
376
+ */
377
+ getConfig() {
378
+ return this.#store.getConfig();
379
+ }
380
+ /**
381
+ * Return stats for the current snapshot from the underlying store.
382
+ */
383
+ getStats() {
384
+ return this.#store.getStats();
385
+ }
386
+ /**
387
+ * Fetch prototypes from the API and store them in memory.
388
+ *
389
+ * This is the core implementation shared by {@link setupSnapshot} and {@link refreshSnapshot}.
390
+ * It encapsulates the complete fetch-and-store workflow with proper error handling and
391
+ * optional parameter caching.
392
+ *
393
+ * @param params - Fetch parameters to merge with {@link DEFAULT_FETCH_PARAMS}
394
+ * @param updateLastFetchParams - Whether to update {@link #lastFetchParams} on successful storage.
395
+ * Set to `true` for {@link setupSnapshot} to cache parameters for future {@link refreshSnapshot} calls.
396
+ * Set to `false` for {@link refreshSnapshot} to avoid overwriting cached parameters.
397
+ * @returns {@link SnapshotOperationResult} indicating success or failure
398
+ *
399
+ * @remarks
400
+ * **Operation Flow**:
401
+ * 1. Fetch and normalize prototypes via {@link fetchAndNormalize}
402
+ * 2. Convert fetch result (both success and failure) via {@link convertFetchResult}
403
+ * 3. Return early if fetch failed
404
+ * 4. Store the fetched data in memory via {@link storeSnapshot}
405
+ * 5. Convert store result via {@link convertStoreResult}
406
+ * 6. Update {@link #lastFetchParams} only if `updateLastFetchParams` is `true` AND storage succeeds
407
+ *
408
+ * **Parameter Handling**:
409
+ * - Input `params` are merged with {@link DEFAULT_FETCH_PARAMS} by {@link fetchAndNormalize}
410
+ * - Merged parameters are cached in {@link #lastFetchParams} only when:
411
+ * - `updateLastFetchParams` is `true` (typically {@link setupSnapshot})
412
+ * - Storage operation succeeds
413
+ * - Failed operations never update {@link #lastFetchParams}
414
+ *
415
+ * **Concurrency Control**:
416
+ * - Uses {@link #executeWithCoalescing} to prevent concurrent API calls
417
+ * - Multiple concurrent calls are coalesced into a single API request
418
+ * - All callers receive the same result
419
+ * - The first caller's operation (fetch + store) is executed
420
+ * - Subsequent concurrent callers wait for the same result
421
+ * - This is necessary because async operations can run concurrently (await allows other calls to start)
422
+ *
423
+ * **Error Handling**:
424
+ * - Returns error result if API fetch fails (network, timeout, API errors)
425
+ * - Returns error result if storage fails (e.g., {@link DataSizeExceededError})
426
+ * - Previous snapshot remains intact on failure
427
+ * - Never throws exceptions - all errors are returned as {@link SnapshotOperationResult}
428
+ *
429
+ * @internal This method is private and used only by {@link setupSnapshot} and {@link refreshSnapshot}.
430
+ * It can be accessed in tests via `(repo as any).fetchAndStore(...)` for unit testing.
431
+ *
432
+ * @see {@link setupSnapshot} for initial snapshot setup with parameter caching
433
+ * @see {@link refreshSnapshot} for refreshing with cached parameters
434
+ */
435
+ async fetchAndStore(params, updateLastFetchParams) {
436
+ return this.#executeWithCoalescing(async () => {
437
+ // Fetch and normalize prototypes
438
+ const fetchResult = await this.fetchAndNormalize(params);
439
+ // Convert fetch result (handles both success and failure)
440
+ const convertedFetchResult = convertFetchResult(fetchResult);
441
+ // Return early on fetch failure
442
+ if (!convertedFetchResult.ok) {
443
+ return convertedFetchResult;
444
+ }
445
+ // Type guard: convertedFetchResult is FetchPrototypesSuccess here
446
+ /* istanbul ignore next */
447
+ if (!('data' in convertedFetchResult)) {
448
+ // This should never happen, but TypeScript needs the check
449
+ throw new Error('Unexpected: success result missing data field');
450
+ }
451
+ // Store the fetched data
452
+ const storeResult = this.storeSnapshot(convertedFetchResult.data);
453
+ // Return early on store failure, converting to SnapshotOperationFailure
454
+ if (!storeResult.ok) {
455
+ return convertStoreResult(storeResult);
456
+ }
457
+ // Update lastFetchParams only on successful storage if requested
458
+ if (updateLastFetchParams) {
459
+ this.#lastFetchParams = { ...DEFAULT_FETCH_PARAMS, ...params };
460
+ }
461
+ return convertStoreResult(storeResult);
462
+ });
463
+ }
464
+ /**
465
+ * Initialize the in-memory snapshot using the provided fetch parameters.
466
+ *
467
+ * Typically called once at startup. Fetches data from the API, stores it in memory,
468
+ * and caches the parameters for future {@link refreshSnapshot} calls.
469
+ *
470
+ * @param params - Fetch parameters to merge with {@link DEFAULT_FETCH_PARAMS}
471
+ * @returns {@link SnapshotOperationResult} indicating success or failure
472
+ *
473
+ * @remarks
474
+ * Delegates to {@link fetchAndStore} with `updateLastFetchParams: true`.
475
+ * Emits `snapshotStarted('setup')` event before operation (if events enabled).
476
+ *
477
+ * See {@link fetchAndStore} for operation flow, concurrency control, and error handling details.
478
+ *
479
+ * @see {@link fetchAndStore}
480
+ * @see {@link refreshSnapshot}
481
+ */
482
+ async setupSnapshot(params) {
483
+ emitRepositoryEventSafely(this.events, this.#logger, 'snapshotStarted', 'setup');
484
+ return this.fetchAndStore(params, true);
485
+ }
486
+ /**
487
+ * Refresh the in-memory snapshot using cached fetch parameters.
488
+ *
489
+ * Typically called periodically to refresh expired data. Uses parameters
490
+ * cached by the last successful {@link setupSnapshot} call.
491
+ *
492
+ * **Prerequisite**: {@link setupSnapshot} must have been called successfully at least once.
493
+ * If called before {@link setupSnapshot}, returns an error with code `REPOSITORY_INVALID_STATE`.
494
+ *
495
+ * @returns Promise resolving to {@link SnapshotOperationResult}:
496
+ * - **Success** (`ok: true`): Contains {@link PrototypeInMemoryStats} with snapshot metadata
497
+ * - **Failure** (`ok: false`): One of:
498
+ * - {@link RepositorySnapshotFailure} - Invalid state (setupSnapshot not called)
499
+ * - {@link FetcherSnapshotFailure} - Network/HTTP errors from API
500
+ * - {@link StoreSnapshotFailure} - Storage errors (size limits, serialization)
501
+ * - {@link UnknownSnapshotFailure} - Unexpected errors
502
+ *
503
+ * @remarks
504
+ * **Operation Flow**:
505
+ * 1. Validate that {@link setupSnapshot} has been called (check `#lastFetchParams`)
506
+ * 2. Return error immediately if validation fails (before coalescing)
507
+ * 3. Emit `snapshotStarted('refresh')` event (if events enabled)
508
+ * 4. Delegate to {@link fetchAndStore} with `updateLastFetchParams: false`
509
+ * 5. Preserve existing snapshot on failure (atomic operation)
510
+ *
511
+ * **Concurrency Control**:
512
+ * - Multiple concurrent calls are coalesced into a single API request
513
+ * - All callers receive the same result
514
+ * - Validation check executes before coalescing, ensuring deterministic failure
515
+ *
516
+ * **Error Handling**:
517
+ * - Never throws exceptions
518
+ * - All errors returned as Result type with `ok: false`
519
+ * - Use `result.origin` to discriminate error types
520
+ *
521
+ * @example
522
+ * ```typescript
523
+ * // Typical usage: periodic refresh
524
+ * const result = await repo.refreshSnapshot();
525
+ * if (result.ok) {
526
+ * console.log(`Refreshed ${result.stats.size} prototypes`);
527
+ * } else {
528
+ * console.error(`Refresh failed: ${result.message}`);
529
+ * }
530
+ * ```
531
+ *
532
+ * @example
533
+ * ```typescript
534
+ * // Error handling by origin
535
+ * const result = await repo.refreshSnapshot();
536
+ * if (!result.ok) {
537
+ * if (result.origin === 'repository') {
538
+ * console.error('Call setupSnapshot first');
539
+ * } else if (result.origin === 'fetcher') {
540
+ * console.error(`HTTP ${result.status}: ${result.message}`);
541
+ * }
542
+ * }
543
+ * ```
544
+ *
545
+ * @see {@link fetchAndStore} - Core implementation
546
+ * @see {@link setupSnapshot} - Initial snapshot setup
547
+ * @see {@link SnapshotOperationResult} - Return type details
548
+ */
549
+ async refreshSnapshot() {
550
+ // Check lastFetchParams before coalescing to ensure refresh is not called
551
+ // before setupSnapshot has established parameters, even during concurrent calls
552
+ if (this.#lastFetchParams === undefined) {
553
+ const error = {
554
+ ok: false,
555
+ origin: 'repository',
556
+ kind: 'invalid_state',
557
+ code: 'REPOSITORY_INVALID_STATE',
558
+ message: 'Cannot refresh snapshot: No previous API fetch parameters available. ' +
559
+ 'Call setupSnapshot() first to establish fetch parameters.',
560
+ };
561
+ emitRepositoryEventSafely(this.events, this.#logger, 'snapshotFailed', error);
562
+ return error;
563
+ }
564
+ // Capture lastFetchParams before entering fetchAndStore to prevent race conditions
565
+ // where setupSnapshot might update it during concurrent execution
566
+ const paramsToUse = this.#lastFetchParams;
567
+ emitRepositoryEventSafely(this.events, this.#logger, 'snapshotStarted', 'refresh');
568
+ return this.fetchAndStore(paramsToUse, false);
569
+ }
570
+ /**
571
+ * Load and validate snapshot data, then store it in memory.
572
+ *
573
+ * This is the core implementation for {@link setupSnapshotFromSerializedData}.
574
+ * It encapsulates the complete validation-and-store workflow with proper error handling.
575
+ *
576
+ * @param data - Serializable snapshot object to validate and store
577
+ * @returns {@link SnapshotOperationResult} indicating success or failure
578
+ *
579
+ * @remarks
580
+ * **Operation Flow**:
581
+ * 1. Validate data structure via {@link validateSerializableSnapshot}
582
+ * 2. Return early if validation failed (origin: 'repository')
583
+ * 3. Store the validated prototypes via {@link storeSnapshot}
584
+ * 4. Return success or store failure result
585
+ *
586
+ * **Concurrency Control**:
587
+ * Unlike {@link fetchAndStore}, this method does NOT use concurrency control because:
588
+ * - This is a synchronous method (no await points)
589
+ * - JavaScript's single-threaded nature prevents concurrent execution
590
+ * - Multiple calls will execute sequentially, not simultaneously
591
+ * - Each call will update the store in order
592
+ *
593
+ * **Error Handling**:
594
+ * - Returns {@link RepositorySnapshotFailure} if validation fails
595
+ * - Returns {@link StoreSnapshotFailure} if storage fails (e.g., {@link DataSizeExceededError})
596
+ * - Previous snapshot remains intact on failure
597
+ * - Never throws exceptions - all errors are returned as {@link SnapshotOperationResult}
598
+ *
599
+ * **Logging**:
600
+ * - Validation: Handled by {@link validateSerializableSnapshot}
601
+ * - Storage success: Handled by {@link storeSnapshot} at `debug` level
602
+ * - Storage failure: Handled by {@link storeSnapshot}
603
+ *
604
+ * @internal
605
+ * This method is private and used only by {@link setupSnapshotFromSerializedData}.
606
+ *
607
+ * @see {@link setupSnapshotFromSerializedData} - Public API
608
+ * @see {@link validateSerializableSnapshot} - Validation logic
609
+ * @see {@link storeSnapshot} - Storage implementation
610
+ * @see {@link fetchAndStore} - Async equivalent with concurrency control
611
+ */
612
+ loadAndStore(data) {
613
+ // Validate data structure
614
+ const validationResult = validateSerializableSnapshot(data, this.#logger);
615
+ if (!validationResult.ok) {
616
+ return {
617
+ ok: false,
618
+ origin: 'repository',
619
+ kind: 'validation',
620
+ // validationResult.code -> "REPOSITORY_VALIDATION_ERROR"
621
+ code: 'REPOSITORY_VALIDATION_ERROR',
622
+ message: validationResult.message,
623
+ };
624
+ }
625
+ // Store the validated data
626
+ const storeResult = this.storeSnapshot(data.prototypes);
627
+ // Return early on store failure, converting to SnapshotOperationFailure
628
+ if (!storeResult.ok) {
629
+ return convertStoreResult(storeResult);
630
+ }
631
+ return convertStoreResult(storeResult);
632
+ }
633
+ /**
634
+ * Setup snapshot from previously serialized data.
635
+ *
636
+ * Alternative to {@link setupSnapshot} for offline/cached initialization.
637
+ * Validates the data structure and populates the in-memory store.
638
+ *
639
+ * **Important**: This method resets `#lastFetchParams` to `undefined`, preventing
640
+ * {@link refreshSnapshot} from working until {@link setupSnapshot} is called.
641
+ * This ensures that API parameters don't become mismatched with serialized data.
642
+ *
643
+ * @param data - Serializable snapshot object (previously exported via {@link getSerializableSnapshot})
644
+ * @returns {@link SnapshotOperationResult} indicating success or failure
645
+ *
646
+ * @remarks
647
+ * **Caller Responsibilities**:
648
+ * - Load snapshot file (e.g., `fs.readFile()`)
649
+ * - Parse JSON to JavaScript object (e.g., `JSON.parse()`)
650
+ * - Pass the parsed object to this method
651
+ *
652
+ * **Operation Flow**:
653
+ * 1. Reset `#lastFetchParams` to `undefined`
654
+ * 2. Emit `snapshotStarted('setupFromSerializedData')` event (if enabled)
655
+ * 3. Validate data structure via {@link loadAndStore}
656
+ * 4. Store validated prototypes if validation succeeds
657
+ * 5. Return operation result
658
+ *
659
+ * **Side Effects**:
660
+ * - Resets cached API fetch parameters
661
+ * - Subsequent {@link refreshSnapshot} calls will fail with `REPOSITORY_INVALID_STATE`
662
+ * - Use {@link setupSnapshot} after this method to re-enable {@link refreshSnapshot}
663
+ *
664
+ * **This method does NOT**:
665
+ * - Perform file I/O operations
666
+ * - Parse JSON strings
667
+ * - Make network requests
668
+ *
669
+ * @example
670
+ * ```typescript
671
+ * // Import from file
672
+ * const json = await fs.readFile('snapshot.json', 'utf-8');
673
+ * const data = JSON.parse(json);
674
+ * const result = repo.setupSnapshotFromSerializedData(data);
675
+ *
676
+ * if (result.ok) {
677
+ * console.log(`Loaded ${result.stats.size} prototypes`);
678
+ * } else {
679
+ * console.error(`Import failed: ${result.message}`);
680
+ * }
681
+ * ```
682
+ *
683
+ * @see {@link loadAndStore} for the core implementation
684
+ * @see {@link getSerializableSnapshot} for exporting snapshots
685
+ * @see {@link setupSnapshot} for API-based initialization
686
+ */
687
+ setupSnapshotFromSerializedData(data) {
688
+ // Reset fetch parameters immediately when attempting to load from serialized data.
689
+ // This ensures that any subsequent refreshSnapshot() calls will fail explicitly,
690
+ // rather than using potentially mismatched API parameters from a previous setupSnapshot().
691
+ this.#lastFetchParams = undefined;
692
+ emitRepositoryEventSafely(this.events, this.#logger, 'snapshotStarted', 'setupFromSerializedData');
693
+ const result = this.loadAndStore(data);
694
+ // Emit events based on result
695
+ if (result.ok) {
696
+ emitRepositoryEventSafely(this.events, this.#logger, 'snapshotCompleted', result.stats);
697
+ }
698
+ else {
699
+ emitRepositoryEventSafely(this.events, this.#logger, 'snapshotFailed', result);
700
+ }
701
+ return result;
702
+ }
703
+ /**
704
+ * Get current snapshot as a serializable object.
705
+ *
706
+ * Returns a plain JavaScript object containing all prototypes from the current
707
+ * snapshot along with metadata. The returned object can be passed to
708
+ * JSON.stringify() for persistence.
709
+ *
710
+ * This method does NOT perform file I/O or JSON.stringify().
711
+ * The caller is responsible for serialization and storage.
712
+ *
713
+ * @returns Serializable snapshot object with version, timestamp, and prototypes
714
+ *
715
+ * @example
716
+ * ```typescript
717
+ * // Export to file
718
+ * const snapshot = repo.getSerializableSnapshot();
719
+ * const json = JSON.stringify(snapshot, null, 2);
720
+ * await fs.writeFile('snapshot.json', json, 'utf-8');
721
+ * ```
722
+ */
723
+ getSerializableSnapshot() {
724
+ const prototypes = this.#store.getAll();
725
+ // Create deep mutable copies for serialization
726
+ // This removes readonly constraints while preserving all data
727
+ const serializablePrototypes = prototypes.map((p) => ({
728
+ ...p,
729
+ users: [...p.users],
730
+ tags: [...p.tags],
731
+ materials: [...p.materials],
732
+ events: [...p.events],
733
+ awards: [...p.awards],
734
+ }));
735
+ return {
736
+ version: SNAPSHOT_SERIALIZATION_VERSION,
737
+ serializedAt: new Date().toISOString(),
738
+ prototypes: serializablePrototypes,
739
+ };
740
+ }
741
+ /**
742
+ * Execute a fetch operation with promise coalescing to prevent concurrent API calls.
743
+ *
744
+ * If a fetch is already in progress, returns the existing promise instead of
745
+ * starting a new fetch. This ensures that multiple concurrent calls result in
746
+ * only one API request.
747
+ *
748
+ * @param fetchFn - Function that performs the actual fetch operation
749
+ * @returns Promise that resolves to the fetch result
750
+ */
751
+ async #executeWithCoalescing(fetchFn) {
752
+ // If a fetch is already in progress, return the existing promise
753
+ if (this.#ongoingFetch) {
754
+ return this.#ongoingFetch;
755
+ }
756
+ // Start new fetch and store the promise
757
+ this.#ongoingFetch = fetchFn();
758
+ try {
759
+ const result = await this.#ongoingFetch;
760
+ // Emit events based on result
761
+ if (result.ok) {
762
+ emitRepositoryEventSafely(this.events, this.#logger, 'snapshotCompleted', result.stats);
763
+ }
764
+ else {
765
+ emitRepositoryEventSafely(this.events, this.#logger, 'snapshotFailed', result);
766
+ }
767
+ return result;
768
+ }
769
+ finally {
770
+ // Clear the ongoing fetch regardless of success or failure
771
+ this.#ongoingFetch = null;
772
+ }
773
+ }
774
+ /**
775
+ * Look up a prototype by id in the current snapshot.
776
+ * Never performs HTTP requests.
777
+ *
778
+ * @param prototypeId - The prototype ID to look up. Must be a positive integer.
779
+ * @returns The prototype if found, null otherwise
780
+ * @throws {ValidationError} If prototypeId is not a positive integer
781
+ */
782
+ async getPrototypeFromSnapshotByPrototypeId(prototypeId) {
783
+ RepositoryParamsValidator.validatePrototypeId(prototypeId);
784
+ return this.#store.getByPrototypeId(prototypeId);
785
+ }
786
+ /**
787
+ * Return a random prototype from the current snapshot, or null
788
+ * when the snapshot is empty. Never performs HTTP requests.
789
+ *
790
+ * @remarks
791
+ * **Implementation Note**: This method uses `store.size` for O(1) empty check,
792
+ * then `store.getAll()` to select a random element. While it might seem wasteful
793
+ * to copy all objects just to select one, the alternative would require:
794
+ *
795
+ * 1. Call `getPrototypeIds()` - O(n) to iterate Map keys
796
+ * 2. Select random ID - O(1)
797
+ * 3. Call `getByPrototypeId(id)` - O(1)
798
+ *
799
+ * This approach still costs O(n) for step 1, making it no better than
800
+ * `getAll()` in terms of time complexity, but with additional function
801
+ * call overhead. The current implementation is simpler and equally
802
+ * efficient.
803
+ */
804
+ async getRandomPrototypeFromSnapshot() {
805
+ if (this.#store.size === 0) {
806
+ return null;
807
+ }
808
+ const all = this.#store.getAll();
809
+ const index = Math.floor(Math.random() * all.length);
810
+ return all[index] ?? null;
811
+ }
812
+ /**
813
+ * Return random samples from the current snapshot.
814
+ *
815
+ * Returns up to `size` random prototypes without duplicates.
816
+ * If `size` exceeds the available data, returns all prototypes in random order.
817
+ * Never performs HTTP requests.
818
+ *
819
+ * @param size - Maximum number of samples to return. Must be an integer.
820
+ * @returns Array of random prototypes (empty array if size <= 0 or snapshot is empty)
821
+ * @throws {ValidationError} If size is not an integer
822
+ *
823
+ * @remarks
824
+ * **Implementation Note**: Uses a hybrid approach for optimal performance:
825
+ *
826
+ * - `store.size` provides O(1) empty check
827
+ * - `store.getAll()` is O(1) (returns reference to internal array)
828
+ * - For small samples (< 50% of total): Set-based random selection, O(size)
829
+ * - For large samples (≥ 50% of total): Fisher-Yates shuffle, O(n)
830
+ *
831
+ * This hybrid approach optimizes for the common case where sample size is
832
+ * much smaller than the total population, while avoiding performance
833
+ * degradation when the sample size approaches the total size.
834
+ */
835
+ async getRandomSampleFromSnapshot(size) {
836
+ RepositoryParamsValidator.validateSampleSize(size);
837
+ if (size <= 0 || this.#store.size === 0) {
838
+ return [];
839
+ }
840
+ const all = this.#store.getAll();
841
+ const actualSize = Math.min(size, all.length);
842
+ // For large samples (≥50% of total), use Fisher-Yates shuffle
843
+ if (actualSize > all.length * SAMPLE_SIZE_THRESHOLD_RATIO) {
844
+ const shuffled = [...all];
845
+ for (let i = shuffled.length - 1; i > 0; i--) {
846
+ const j = Math.floor(Math.random() * (i + 1));
847
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
848
+ }
849
+ return shuffled.slice(0, actualSize);
850
+ }
851
+ // For small samples, use Set-based collision avoidance
852
+ const result = [];
853
+ const indices = new Set();
854
+ while (result.length < actualSize) {
855
+ const index = Math.floor(Math.random() * all.length);
856
+ if (!indices.has(index)) {
857
+ indices.add(index);
858
+ result.push(all[index]);
859
+ }
860
+ }
861
+ return result;
862
+ }
863
+ /**
864
+ * Return all prototype IDs from the current snapshot.
865
+ * Never performs HTTP requests.
866
+ */
867
+ async getPrototypeIdsFromSnapshot() {
868
+ return this.#store.getPrototypeIds();
869
+ }
870
+ /**
871
+ * Return all prototypes from the current snapshot.
872
+ * Never performs HTTP requests.
873
+ */
874
+ async getAllFromSnapshot() {
875
+ return this.#store.getAll();
876
+ }
877
+ /**
878
+ * Analyze prototypes to extract ID range (minimum and maximum).
879
+ *
880
+ * Uses a for-loop implementation for better performance with large datasets (5,000+ items).
881
+ * Single-pass algorithm with minimal memory allocations.
882
+ *
883
+ * @param prototypes - Array of prototypes to analyze
884
+ * @returns Object containing min and max IDs, or null values if array is empty
885
+ */
886
+ analyzePrototypesWithForLoop(prototypes) {
887
+ if (prototypes.length === 0) {
888
+ return { min: null, max: null };
889
+ }
890
+ let min = prototypes[0].id;
891
+ let max = prototypes[0].id;
892
+ for (let i = 1; i < prototypes.length; i++) {
893
+ const id = prototypes[i].id;
894
+ if (id < min)
895
+ min = id;
896
+ if (id > max)
897
+ max = id;
898
+ }
899
+ return { min, max };
900
+ }
901
+ /**
902
+ * Analyze prototypes from the current snapshot to extract ID range.
903
+ *
904
+ * Currently uses the for-loop implementation for optimal performance with typical dataset sizes.
905
+ * Never performs HTTP requests.
906
+ *
907
+ * @returns Object containing min and max IDs, or null values if snapshot is empty
908
+ */
909
+ async analyzePrototypes() {
910
+ const all = this.#store.getAll();
911
+ return this.analyzePrototypesWithForLoop(all);
912
+ }
913
+ /**
914
+ * Clean up event listeners and release resources.
915
+ *
916
+ * Removes all event listeners from the internal EventEmitter to prevent memory leaks.
917
+ * Safe to call even when events are disabled.
918
+ *
919
+ * @remarks
920
+ * Always call this method in cleanup paths:
921
+ * - Test cleanup (afterEach)
922
+ * - Component unmounting (React useEffect cleanup)
923
+ * - Before creating a new repository instance
924
+ */
925
+ dispose() {
926
+ this.events?.removeAllListeners();
927
+ }
928
+ }
929
+ //# sourceMappingURL=protopedia-in-memory-repository.js.map