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.
- package/LICENSE +21 -0
- package/README.md +179 -0
- package/dist/builder.d.ts +158 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +255 -0
- package/dist/builder.js.map +1 -0
- package/dist/factory.d.ts +154 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +243 -0
- package/dist/factory.js.map +1 -0
- package/dist/fetcher/client/config.d.ts +140 -0
- package/dist/fetcher/client/config.d.ts.map +1 -0
- package/dist/fetcher/client/config.js +2 -0
- package/dist/fetcher/client/config.js.map +1 -0
- package/dist/fetcher/client/fetch-with-progress.d.ts +156 -0
- package/dist/fetcher/client/fetch-with-progress.d.ts.map +1 -0
- package/dist/fetcher/client/fetch-with-progress.js +313 -0
- package/dist/fetcher/client/fetch-with-progress.js.map +1 -0
- package/dist/fetcher/client/fetch-with-timeout.d.ts +6 -0
- package/dist/fetcher/client/fetch-with-timeout.d.ts.map +1 -0
- package/dist/fetcher/client/fetch-with-timeout.js +48 -0
- package/dist/fetcher/client/fetch-with-timeout.js.map +1 -0
- package/dist/fetcher/client/protopedia-api-custom-client.d.ts +141 -0
- package/dist/fetcher/client/protopedia-api-custom-client.d.ts.map +1 -0
- package/dist/fetcher/client/protopedia-api-custom-client.js +268 -0
- package/dist/fetcher/client/protopedia-api-custom-client.js.map +1 -0
- package/dist/fetcher/client/select-custom-fetch.d.ts +58 -0
- package/dist/fetcher/client/select-custom-fetch.d.ts.map +1 -0
- package/dist/fetcher/client/select-custom-fetch.js +58 -0
- package/dist/fetcher/client/select-custom-fetch.js.map +1 -0
- package/dist/fetcher/errors/fetcher-error.d.ts +10 -0
- package/dist/fetcher/errors/fetcher-error.d.ts.map +1 -0
- package/dist/fetcher/errors/fetcher-error.js +15 -0
- package/dist/fetcher/errors/fetcher-error.js.map +1 -0
- package/dist/fetcher/index.d.ts +73 -0
- package/dist/fetcher/index.d.ts.map +1 -0
- package/dist/fetcher/index.js +70 -0
- package/dist/fetcher/index.js.map +1 -0
- package/dist/fetcher/types/index.d.ts +9 -0
- package/dist/fetcher/types/index.d.ts.map +1 -0
- package/dist/fetcher/types/index.js +7 -0
- package/dist/fetcher/types/index.js.map +1 -0
- package/dist/fetcher/types/progress-event.types.d.ts +221 -0
- package/dist/fetcher/types/progress-event.types.d.ts.map +1 -0
- package/dist/fetcher/types/progress-event.types.js +10 -0
- package/dist/fetcher/types/progress-event.types.js.map +1 -0
- package/dist/fetcher/types/prototype-api.types.d.ts +106 -0
- package/dist/fetcher/types/prototype-api.types.d.ts.map +1 -0
- package/dist/fetcher/types/prototype-api.types.js +2 -0
- package/dist/fetcher/types/prototype-api.types.js.map +1 -0
- package/dist/fetcher/types/result.types.d.ts +75 -0
- package/dist/fetcher/types/result.types.d.ts.map +1 -0
- package/dist/fetcher/types/result.types.js +2 -0
- package/dist/fetcher/types/result.types.js.map +1 -0
- package/dist/fetcher/utils/create-client-fetch.d.ts +63 -0
- package/dist/fetcher/utils/create-client-fetch.d.ts.map +1 -0
- package/dist/fetcher/utils/create-client-fetch.js +89 -0
- package/dist/fetcher/utils/create-client-fetch.js.map +1 -0
- package/dist/fetcher/utils/create-fetch-with-stripped-headers.d.ts +6 -0
- package/dist/fetcher/utils/create-fetch-with-stripped-headers.d.ts.map +1 -0
- package/dist/fetcher/utils/create-fetch-with-stripped-headers.js +40 -0
- package/dist/fetcher/utils/create-fetch-with-stripped-headers.js.map +1 -0
- package/dist/fetcher/utils/errors/handler.d.ts +58 -0
- package/dist/fetcher/utils/errors/handler.d.ts.map +1 -0
- package/dist/fetcher/utils/errors/handler.js +243 -0
- package/dist/fetcher/utils/errors/handler.js.map +1 -0
- package/dist/fetcher/utils/errors/messages.d.ts +75 -0
- package/dist/fetcher/utils/errors/messages.d.ts.map +1 -0
- package/dist/fetcher/utils/errors/messages.js +88 -0
- package/dist/fetcher/utils/errors/messages.js.map +1 -0
- package/dist/fetcher/utils/index.d.ts +13 -0
- package/dist/fetcher/utils/index.d.ts.map +1 -0
- package/dist/fetcher/utils/index.js +12 -0
- package/dist/fetcher/utils/index.js.map +1 -0
- package/dist/fetcher/utils/log-timestamp-normalization-warnings.d.ts +10 -0
- package/dist/fetcher/utils/log-timestamp-normalization-warnings.d.ts.map +1 -0
- package/dist/fetcher/utils/log-timestamp-normalization-warnings.js +32 -0
- package/dist/fetcher/utils/log-timestamp-normalization-warnings.js.map +1 -0
- package/dist/fetcher/utils/normalize-protopedia-timestamp.d.ts +59 -0
- package/dist/fetcher/utils/normalize-protopedia-timestamp.d.ts.map +1 -0
- package/dist/fetcher/utils/normalize-protopedia-timestamp.js +81 -0
- package/dist/fetcher/utils/normalize-protopedia-timestamp.js.map +1 -0
- package/dist/fetcher/utils/normalize-prototype.d.ts +56 -0
- package/dist/fetcher/utils/normalize-prototype.d.ts.map +1 -0
- package/dist/fetcher/utils/normalize-prototype.js +113 -0
- package/dist/fetcher/utils/normalize-prototype.js.map +1 -0
- package/dist/fetcher/utils/sanitize-options.d.ts +14 -0
- package/dist/fetcher/utils/sanitize-options.d.ts.map +1 -0
- package/dist/fetcher/utils/sanitize-options.js +16 -0
- package/dist/fetcher/utils/sanitize-options.js.map +1 -0
- package/dist/fetcher/utils/string-parsers.d.ts +45 -0
- package/dist/fetcher/utils/string-parsers.d.ts.map +1 -0
- package/dist/fetcher/utils/string-parsers.js +53 -0
- package/dist/fetcher/utils/string-parsers.js.map +1 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +70 -0
- package/dist/index.js.map +1 -0
- package/dist/logger/console-logger.d.ts +74 -0
- package/dist/logger/console-logger.d.ts.map +1 -0
- package/dist/logger/console-logger.js +113 -0
- package/dist/logger/console-logger.js.map +1 -0
- package/dist/logger/factory.d.ts +88 -0
- package/dist/logger/factory.d.ts.map +1 -0
- package/dist/logger/factory.js +94 -0
- package/dist/logger/factory.js.map +1 -0
- package/dist/logger/index.d.ts +42 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/logger/index.js +41 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/logger/logger.types.d.ts +49 -0
- package/dist/logger/logger.types.d.ts.map +1 -0
- package/dist/logger/logger.types.js +2 -0
- package/dist/logger/logger.types.js.map +1 -0
- package/dist/repository/errors/validation-error.d.ts +24 -0
- package/dist/repository/errors/validation-error.d.ts.map +1 -0
- package/dist/repository/errors/validation-error.js +26 -0
- package/dist/repository/errors/validation-error.js.map +1 -0
- package/dist/repository/index.d.ts +122 -0
- package/dist/repository/index.d.ts.map +1 -0
- package/dist/repository/index.js +44 -0
- package/dist/repository/index.js.map +1 -0
- package/dist/repository/protopedia-in-memory-repository.d.ts +560 -0
- package/dist/repository/protopedia-in-memory-repository.d.ts.map +1 -0
- package/dist/repository/protopedia-in-memory-repository.js +929 -0
- package/dist/repository/protopedia-in-memory-repository.js.map +1 -0
- package/dist/repository/schemas/index.d.ts +9 -0
- package/dist/repository/schemas/index.d.ts.map +1 -0
- package/dist/repository/schemas/index.js +11 -0
- package/dist/repository/schemas/index.js.map +1 -0
- package/dist/repository/schemas/params.d.ts +44 -0
- package/dist/repository/schemas/params.d.ts.map +1 -0
- package/dist/repository/schemas/params.js +44 -0
- package/dist/repository/schemas/params.js.map +1 -0
- package/dist/repository/schemas/serializable-snapshot.d.ts +33 -0
- package/dist/repository/schemas/serializable-snapshot.d.ts.map +1 -0
- package/dist/repository/schemas/serializable-snapshot.js +45 -0
- package/dist/repository/schemas/serializable-snapshot.js.map +1 -0
- package/dist/repository/types/analysis.types.d.ts +89 -0
- package/dist/repository/types/analysis.types.d.ts.map +1 -0
- package/dist/repository/types/analysis.types.js +2 -0
- package/dist/repository/types/analysis.types.js.map +1 -0
- package/dist/repository/types/index.d.ts +12 -0
- package/dist/repository/types/index.d.ts.map +1 -0
- package/dist/repository/types/index.js +7 -0
- package/dist/repository/types/index.js.map +1 -0
- package/dist/repository/types/repository-events.types.d.ts +110 -0
- package/dist/repository/types/repository-events.types.d.ts.map +1 -0
- package/dist/repository/types/repository-events.types.js +2 -0
- package/dist/repository/types/repository-events.types.js.map +1 -0
- package/dist/repository/types/repository.types.d.ts +330 -0
- package/dist/repository/types/repository.types.d.ts.map +1 -0
- package/dist/repository/types/repository.types.js +2 -0
- package/dist/repository/types/repository.types.js.map +1 -0
- package/dist/repository/types/result.types.d.ts +55 -0
- package/dist/repository/types/result.types.d.ts.map +1 -0
- package/dist/repository/types/result.types.js +2 -0
- package/dist/repository/types/result.types.js.map +1 -0
- package/dist/repository/types/serialization.types.d.ts +61 -0
- package/dist/repository/types/serialization.types.d.ts.map +1 -0
- package/dist/repository/types/serialization.types.js +2 -0
- package/dist/repository/types/serialization.types.js.map +1 -0
- package/dist/repository/types/snapshot-operation.types.d.ts +140 -0
- package/dist/repository/types/snapshot-operation.types.d.ts.map +1 -0
- package/dist/repository/types/snapshot-operation.types.js +2 -0
- package/dist/repository/types/snapshot-operation.types.js.map +1 -0
- package/dist/repository/utils/convert-fetch-result.d.ts +46 -0
- package/dist/repository/utils/convert-fetch-result.d.ts.map +1 -0
- package/dist/repository/utils/convert-fetch-result.js +59 -0
- package/dist/repository/utils/convert-fetch-result.js.map +1 -0
- package/dist/repository/utils/convert-store-result.d.ts +36 -0
- package/dist/repository/utils/convert-store-result.d.ts.map +1 -0
- package/dist/repository/utils/convert-store-result.js +36 -0
- package/dist/repository/utils/convert-store-result.js.map +1 -0
- package/dist/repository/utils/emit-repository-event-safely.d.ts +5 -0
- package/dist/repository/utils/emit-repository-event-safely.d.ts.map +1 -0
- package/dist/repository/utils/emit-repository-event-safely.js +17 -0
- package/dist/repository/utils/emit-repository-event-safely.js.map +1 -0
- package/dist/repository/utils/index.d.ts +3 -0
- package/dist/repository/utils/index.d.ts.map +1 -0
- package/dist/repository/utils/index.js +3 -0
- package/dist/repository/utils/index.js.map +1 -0
- package/dist/repository/validation/index.d.ts +9 -0
- package/dist/repository/validation/index.d.ts.map +1 -0
- package/dist/repository/validation/index.js +10 -0
- package/dist/repository/validation/index.js.map +1 -0
- package/dist/repository/validation/params-validators.d.ts +46 -0
- package/dist/repository/validation/params-validators.d.ts.map +1 -0
- package/dist/repository/validation/params-validators.js +68 -0
- package/dist/repository/validation/params-validators.js.map +1 -0
- package/dist/repository/validation/serializable-snapshot.d.ts +47 -0
- package/dist/repository/validation/serializable-snapshot.d.ts.map +1 -0
- package/dist/repository/validation/serializable-snapshot.js +104 -0
- package/dist/repository/validation/serializable-snapshot.js.map +1 -0
- package/dist/schemas/index.d.ts +8 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +8 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/normalized-prototype.d.ts +56 -0
- package/dist/schemas/normalized-prototype.d.ts.map +1 -0
- package/dist/schemas/normalized-prototype.js +123 -0
- package/dist/schemas/normalized-prototype.js.map +1 -0
- package/dist/store/errors/store-error.d.ts +148 -0
- package/dist/store/errors/store-error.d.ts.map +1 -0
- package/dist/store/errors/store-error.js +156 -0
- package/dist/store/errors/store-error.js.map +1 -0
- package/dist/store/index.d.ts +84 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +83 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/store.d.ts +295 -0
- package/dist/store/store.d.ts.map +1 -0
- package/dist/store/store.js +411 -0
- package/dist/store/store.js.map +1 -0
- package/dist/store/types/index.d.ts +2 -0
- package/dist/store/types/index.d.ts.map +1 -0
- package/dist/store/types/index.js +2 -0
- package/dist/store/types/index.js.map +1 -0
- package/dist/store/types/result.types.d.ts +67 -0
- package/dist/store/types/result.types.d.ts.map +1 -0
- package/dist/store/types/result.types.js +2 -0
- package/dist/store/types/result.types.js.map +1 -0
- package/dist/types/codes.d.ts +44 -0
- package/dist/types/codes.d.ts.map +1 -0
- package/dist/types/codes.js +9 -0
- package/dist/types/codes.js.map +1 -0
- package/dist/types/index.d.ts +61 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +60 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/normalized-prototype.d.ts +95 -0
- package/dist/types/normalized-prototype.d.ts.map +1 -0
- package/dist/types/normalized-prototype.js +2 -0
- package/dist/types/normalized-prototype.js.map +1 -0
- package/dist/utils/converters/index.d.ts +15 -0
- package/dist/utils/converters/index.d.ts.map +1 -0
- package/dist/utils/converters/index.js +15 -0
- package/dist/utils/converters/index.js.map +1 -0
- package/dist/utils/converters/license-type.d.ts +23 -0
- package/dist/utils/converters/license-type.d.ts.map +1 -0
- package/dist/utils/converters/license-type.js +38 -0
- package/dist/utils/converters/license-type.js.map +1 -0
- package/dist/utils/converters/release-flag.d.ts +24 -0
- package/dist/utils/converters/release-flag.d.ts.map +1 -0
- package/dist/utils/converters/release-flag.js +40 -0
- package/dist/utils/converters/release-flag.js.map +1 -0
- package/dist/utils/converters/status.d.ts +23 -0
- package/dist/utils/converters/status.d.ts.map +1 -0
- package/dist/utils/converters/status.js +40 -0
- package/dist/utils/converters/status.js.map +1 -0
- package/dist/utils/converters/thanks-flag.d.ts +25 -0
- package/dist/utils/converters/thanks-flag.d.ts.map +1 -0
- package/dist/utils/converters/thanks-flag.js +41 -0
- package/dist/utils/converters/thanks-flag.js.map +1 -0
- package/dist/utils/deep-merge.d.ts +38 -0
- package/dist/utils/deep-merge.d.ts.map +1 -0
- package/dist/utils/deep-merge.js +85 -0
- package/dist/utils/deep-merge.js.map +1 -0
- package/dist/utils/index.d.ts +80 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +85 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger-utils.d.ts +100 -0
- package/dist/utils/logger-utils.d.ts.map +1 -0
- package/dist/utils/logger-utils.js +265 -0
- package/dist/utils/logger-utils.js.map +1 -0
- package/dist/utils/time/constants.d.ts +14 -0
- package/dist/utils/time/constants.d.ts.map +1 -0
- package/dist/utils/time/constants.js +14 -0
- package/dist/utils/time/constants.js.map +1 -0
- package/dist/utils/time/index.d.ts +28 -0
- package/dist/utils/time/index.d.ts.map +1 -0
- package/dist/utils/time/index.js +28 -0
- package/dist/utils/time/index.js.map +1 -0
- package/dist/utils/time/parser.d.ts +91 -0
- package/dist/utils/time/parser.d.ts.map +1 -0
- package/dist/utils/time/parser.js +143 -0
- package/dist/utils/time/parser.js.map +1 -0
- package/dist/utils/validation/index.d.ts +8 -0
- package/dist/utils/validation/index.d.ts.map +1 -0
- package/dist/utils/validation/index.js +7 -0
- package/dist/utils/validation/index.js.map +1 -0
- package/dist/utils/validation/normalized-prototype.d.ts +64 -0
- package/dist/utils/validation/normalized-prototype.d.ts.map +1 -0
- package/dist/utils/validation/normalized-prototype.js +97 -0
- package/dist/utils/validation/normalized-prototype.js.map +1 -0
- package/dist/utils/validation/types.d.ts +62 -0
- package/dist/utils/validation/types.d.ts.map +1 -0
- package/dist/utils/validation/types.js +8 -0
- package/dist/utils/validation/types.js.map +1 -0
- package/dist/version.d.ts +6 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +6 -0
- package/dist/version.js.map +1 -0
- 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
|