@uploadista/client-core 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +5 -0
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/dist/auth/auth-http-client.d.ts +50 -0
- package/dist/auth/auth-http-client.d.ts.map +1 -0
- package/dist/auth/auth-http-client.js +110 -0
- package/dist/auth/direct-auth.d.ts +38 -0
- package/dist/auth/direct-auth.d.ts.map +1 -0
- package/dist/auth/direct-auth.js +95 -0
- package/dist/auth/index.d.ts +6 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +5 -0
- package/dist/auth/no-auth.d.ts +26 -0
- package/dist/auth/no-auth.d.ts.map +1 -0
- package/dist/auth/no-auth.js +33 -0
- package/dist/auth/saas-auth.d.ts +80 -0
- package/dist/auth/saas-auth.d.ts.map +1 -0
- package/dist/auth/saas-auth.js +167 -0
- package/dist/auth/types.d.ts +101 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +8 -0
- package/dist/chunk-buffer.d.ts +209 -0
- package/dist/chunk-buffer.d.ts.map +1 -0
- package/dist/chunk-buffer.js +236 -0
- package/dist/client/create-uploadista-client.d.ts +369 -0
- package/dist/client/create-uploadista-client.d.ts.map +1 -0
- package/dist/client/create-uploadista-client.js +518 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +3 -0
- package/dist/client/uploadista-api.d.ts +284 -0
- package/dist/client/uploadista-api.d.ts.map +1 -0
- package/dist/client/uploadista-api.js +444 -0
- package/dist/client/uploadista-websocket-manager.d.ts +110 -0
- package/dist/client/uploadista-websocket-manager.d.ts.map +1 -0
- package/dist/client/uploadista-websocket-manager.js +207 -0
- package/dist/error.d.ts +106 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +69 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/logger.d.ts +70 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +59 -0
- package/dist/mock-data-store.d.ts +30 -0
- package/dist/mock-data-store.d.ts.map +1 -0
- package/dist/mock-data-store.js +88 -0
- package/dist/network-monitor.d.ts +262 -0
- package/dist/network-monitor.d.ts.map +1 -0
- package/dist/network-monitor.js +291 -0
- package/dist/services/abort-controller-service.d.ts +19 -0
- package/dist/services/abort-controller-service.d.ts.map +1 -0
- package/dist/services/abort-controller-service.js +4 -0
- package/dist/services/checksum-service.d.ts +4 -0
- package/dist/services/checksum-service.d.ts.map +1 -0
- package/dist/services/checksum-service.js +1 -0
- package/dist/services/file-reader-service.d.ts +38 -0
- package/dist/services/file-reader-service.d.ts.map +1 -0
- package/dist/services/file-reader-service.js +4 -0
- package/dist/services/fingerprint-service.d.ts +4 -0
- package/dist/services/fingerprint-service.d.ts.map +1 -0
- package/dist/services/fingerprint-service.js +1 -0
- package/dist/services/http-client.d.ts +182 -0
- package/dist/services/http-client.d.ts.map +1 -0
- package/dist/services/http-client.js +1 -0
- package/dist/services/id-generation-service.d.ts +10 -0
- package/dist/services/id-generation-service.d.ts.map +1 -0
- package/dist/services/id-generation-service.js +1 -0
- package/dist/services/index.d.ts +11 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +10 -0
- package/dist/services/platform-service.d.ts +48 -0
- package/dist/services/platform-service.d.ts.map +1 -0
- package/dist/services/platform-service.js +10 -0
- package/dist/services/service-container.d.ts +25 -0
- package/dist/services/service-container.d.ts.map +1 -0
- package/dist/services/service-container.js +1 -0
- package/dist/services/storage-service.d.ts +26 -0
- package/dist/services/storage-service.d.ts.map +1 -0
- package/dist/services/storage-service.js +1 -0
- package/dist/services/websocket-service.d.ts +36 -0
- package/dist/services/websocket-service.d.ts.map +1 -0
- package/dist/services/websocket-service.js +4 -0
- package/dist/smart-chunker.d.ts +72 -0
- package/dist/smart-chunker.d.ts.map +1 -0
- package/dist/smart-chunker.js +317 -0
- package/dist/storage/client-storage.d.ts +148 -0
- package/dist/storage/client-storage.d.ts.map +1 -0
- package/dist/storage/client-storage.js +62 -0
- package/dist/storage/in-memory-storage-service.d.ts +7 -0
- package/dist/storage/in-memory-storage-service.d.ts.map +1 -0
- package/dist/storage/in-memory-storage-service.js +24 -0
- package/dist/storage/index.d.ts +3 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +2 -0
- package/dist/types/buffered-chunk.d.ts +6 -0
- package/dist/types/buffered-chunk.d.ts.map +1 -0
- package/dist/types/buffered-chunk.js +1 -0
- package/dist/types/chunk-metrics.d.ts +12 -0
- package/dist/types/chunk-metrics.d.ts.map +1 -0
- package/dist/types/chunk-metrics.js +1 -0
- package/dist/types/flow-result.d.ts +11 -0
- package/dist/types/flow-result.d.ts.map +1 -0
- package/dist/types/flow-result.js +1 -0
- package/dist/types/flow-upload-config.d.ts +54 -0
- package/dist/types/flow-upload-config.d.ts.map +1 -0
- package/dist/types/flow-upload-config.js +1 -0
- package/dist/types/flow-upload-item.d.ts +16 -0
- package/dist/types/flow-upload-item.d.ts.map +1 -0
- package/dist/types/flow-upload-item.js +1 -0
- package/dist/types/flow-upload-options.d.ts +41 -0
- package/dist/types/flow-upload-options.d.ts.map +1 -0
- package/dist/types/flow-upload-options.js +1 -0
- package/dist/types/index.d.ts +14 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +13 -0
- package/dist/types/multi-flow-upload-options.d.ts +33 -0
- package/dist/types/multi-flow-upload-options.d.ts.map +1 -0
- package/dist/types/multi-flow-upload-options.js +1 -0
- package/dist/types/multi-flow-upload-state.d.ts +9 -0
- package/dist/types/multi-flow-upload-state.d.ts.map +1 -0
- package/dist/types/multi-flow-upload-state.js +1 -0
- package/dist/types/performance-insights.d.ts +11 -0
- package/dist/types/performance-insights.d.ts.map +1 -0
- package/dist/types/performance-insights.js +1 -0
- package/dist/types/previous-upload.d.ts +20 -0
- package/dist/types/previous-upload.d.ts.map +1 -0
- package/dist/types/previous-upload.js +9 -0
- package/dist/types/upload-options.d.ts +40 -0
- package/dist/types/upload-options.d.ts.map +1 -0
- package/dist/types/upload-options.js +1 -0
- package/dist/types/upload-response.d.ts +6 -0
- package/dist/types/upload-response.d.ts.map +1 -0
- package/dist/types/upload-response.js +1 -0
- package/dist/types/upload-result.d.ts +57 -0
- package/dist/types/upload-result.d.ts.map +1 -0
- package/dist/types/upload-result.js +1 -0
- package/dist/types/upload-session-metrics.d.ts +16 -0
- package/dist/types/upload-session-metrics.d.ts.map +1 -0
- package/dist/types/upload-session-metrics.js +1 -0
- package/dist/upload/chunk-upload.d.ts +40 -0
- package/dist/upload/chunk-upload.d.ts.map +1 -0
- package/dist/upload/chunk-upload.js +82 -0
- package/dist/upload/flow-upload.d.ts +48 -0
- package/dist/upload/flow-upload.d.ts.map +1 -0
- package/dist/upload/flow-upload.js +240 -0
- package/dist/upload/index.d.ts +3 -0
- package/dist/upload/index.d.ts.map +1 -0
- package/dist/upload/index.js +2 -0
- package/dist/upload/parallel-upload.d.ts +65 -0
- package/dist/upload/parallel-upload.d.ts.map +1 -0
- package/dist/upload/parallel-upload.js +231 -0
- package/dist/upload/single-upload.d.ts +118 -0
- package/dist/upload/single-upload.d.ts.map +1 -0
- package/dist/upload/single-upload.js +332 -0
- package/dist/upload/upload-manager.d.ts +30 -0
- package/dist/upload/upload-manager.d.ts.map +1 -0
- package/dist/upload/upload-manager.js +57 -0
- package/dist/upload/upload-metrics.d.ts +37 -0
- package/dist/upload/upload-metrics.d.ts.map +1 -0
- package/dist/upload/upload-metrics.js +236 -0
- package/dist/upload/upload-storage.d.ts +32 -0
- package/dist/upload/upload-storage.d.ts.map +1 -0
- package/dist/upload/upload-storage.js +46 -0
- package/dist/upload/upload-strategy.d.ts +66 -0
- package/dist/upload/upload-strategy.d.ts.map +1 -0
- package/dist/upload/upload-strategy.js +171 -0
- package/dist/upload/upload-utils.d.ts +26 -0
- package/dist/upload/upload-utils.d.ts.map +1 -0
- package/dist/upload/upload-utils.js +80 -0
- package/package.json +29 -0
- package/src/__tests__/smart-chunking.test.ts +399 -0
- package/src/auth/__tests__/auth-http-client.test.ts +327 -0
- package/src/auth/__tests__/direct-auth.test.ts +135 -0
- package/src/auth/__tests__/no-auth.test.ts +40 -0
- package/src/auth/__tests__/saas-auth.test.ts +337 -0
- package/src/auth/auth-http-client.ts +150 -0
- package/src/auth/direct-auth.ts +121 -0
- package/src/auth/index.ts +5 -0
- package/src/auth/no-auth.ts +39 -0
- package/src/auth/saas-auth.ts +218 -0
- package/src/auth/types.ts +105 -0
- package/src/chunk-buffer.ts +287 -0
- package/src/client/create-uploadista-client.ts +901 -0
- package/src/client/index.ts +3 -0
- package/src/client/uploadista-api.ts +857 -0
- package/src/client/uploadista-websocket-manager.ts +275 -0
- package/src/error.ts +149 -0
- package/src/index.ts +13 -0
- package/src/logger.ts +104 -0
- package/src/mock-data-store.ts +97 -0
- package/src/network-monitor.ts +445 -0
- package/src/services/abort-controller-service.ts +21 -0
- package/src/services/checksum-service.ts +3 -0
- package/src/services/file-reader-service.ts +44 -0
- package/src/services/fingerprint-service.ts +6 -0
- package/src/services/http-client.ts +229 -0
- package/src/services/id-generation-service.ts +9 -0
- package/src/services/index.ts +10 -0
- package/src/services/platform-service.ts +65 -0
- package/src/services/service-container.ts +24 -0
- package/src/services/storage-service.ts +29 -0
- package/src/services/websocket-service.ts +33 -0
- package/src/smart-chunker.ts +451 -0
- package/src/storage/client-storage.ts +186 -0
- package/src/storage/in-memory-storage-service.ts +33 -0
- package/src/storage/index.ts +2 -0
- package/src/types/buffered-chunk.ts +5 -0
- package/src/types/chunk-metrics.ts +11 -0
- package/src/types/flow-result.ts +14 -0
- package/src/types/flow-upload-config.ts +56 -0
- package/src/types/flow-upload-item.ts +16 -0
- package/src/types/flow-upload-options.ts +56 -0
- package/src/types/index.ts +13 -0
- package/src/types/multi-flow-upload-options.ts +39 -0
- package/src/types/multi-flow-upload-state.ts +9 -0
- package/src/types/performance-insights.ts +7 -0
- package/src/types/previous-upload.ts +22 -0
- package/src/types/upload-options.ts +56 -0
- package/src/types/upload-response.ts +6 -0
- package/src/types/upload-result.ts +60 -0
- package/src/types/upload-session-metrics.ts +15 -0
- package/src/upload/chunk-upload.ts +151 -0
- package/src/upload/flow-upload.ts +367 -0
- package/src/upload/index.ts +2 -0
- package/src/upload/parallel-upload.ts +387 -0
- package/src/upload/single-upload.ts +554 -0
- package/src/upload/upload-manager.ts +106 -0
- package/src/upload/upload-metrics.ts +340 -0
- package/src/upload/upload-storage.ts +87 -0
- package/src/upload/upload-strategy.ts +296 -0
- package/src/upload/upload-utils.ts +114 -0
- package/tsconfig.json +23 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import type { NetworkCondition, NetworkMonitor } from "./network-monitor";
|
|
2
|
+
import type { ConnectionMetrics } from "./services/http-client";
|
|
3
|
+
|
|
4
|
+
export interface ChunkingStrategy {
|
|
5
|
+
name: string;
|
|
6
|
+
minChunkSize: number;
|
|
7
|
+
maxChunkSize: number;
|
|
8
|
+
initialChunkSize: number;
|
|
9
|
+
adaptationRate: number; // how quickly to adapt (0-1)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DatastoreConstraints {
|
|
13
|
+
minChunkSize: number;
|
|
14
|
+
maxChunkSize: number;
|
|
15
|
+
optimalChunkSize: number;
|
|
16
|
+
requiresOrderedChunks?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SmartChunkerConfig {
|
|
20
|
+
enabled?: boolean;
|
|
21
|
+
fallbackChunkSize?: number;
|
|
22
|
+
minChunkSize?: number;
|
|
23
|
+
maxChunkSize?: number;
|
|
24
|
+
initialChunkSize?: number;
|
|
25
|
+
targetUtilization?: number; // target bandwidth utilization (0-1)
|
|
26
|
+
adaptationRate?: number;
|
|
27
|
+
conservativeMode?: boolean;
|
|
28
|
+
connectionPoolingAware?: boolean; // enable connection pooling optimizations
|
|
29
|
+
datastoreConstraints?: DatastoreConstraints;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ChunkSizeDecision {
|
|
33
|
+
size: number;
|
|
34
|
+
strategy: string;
|
|
35
|
+
reason: string;
|
|
36
|
+
networkCondition: NetworkCondition;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const DEFAULT_STRATEGIES: Record<string, ChunkingStrategy> = {
|
|
40
|
+
conservative: {
|
|
41
|
+
name: "conservative",
|
|
42
|
+
minChunkSize: 64 * 1024, // 64 KB
|
|
43
|
+
maxChunkSize: 2 * 1024 * 1024, // 2 MB
|
|
44
|
+
initialChunkSize: 256 * 1024, // 256 KB
|
|
45
|
+
adaptationRate: 0.1,
|
|
46
|
+
},
|
|
47
|
+
balanced: {
|
|
48
|
+
name: "balanced",
|
|
49
|
+
minChunkSize: 128 * 1024, // 128 KB
|
|
50
|
+
maxChunkSize: 8 * 1024 * 1024, // 8 MB
|
|
51
|
+
initialChunkSize: 512 * 1024, // 512 KB
|
|
52
|
+
adaptationRate: 0.2,
|
|
53
|
+
},
|
|
54
|
+
aggressive: {
|
|
55
|
+
name: "aggressive",
|
|
56
|
+
minChunkSize: 256 * 1024, // 256 KB
|
|
57
|
+
maxChunkSize: 32 * 1024 * 1024, // 32 MB
|
|
58
|
+
initialChunkSize: 1024 * 1024, // 1 MB
|
|
59
|
+
adaptationRate: 0.3,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const S3_OPTIMIZED_STRATEGIES: Record<string, ChunkingStrategy> = {
|
|
64
|
+
conservative: {
|
|
65
|
+
name: "s3-conservative",
|
|
66
|
+
minChunkSize: 5 * 1024 * 1024, // 5MB - S3 minimum
|
|
67
|
+
maxChunkSize: 64 * 1024 * 1024, // 64MB
|
|
68
|
+
initialChunkSize: 8 * 1024 * 1024, // 8MB
|
|
69
|
+
adaptationRate: 0.1,
|
|
70
|
+
},
|
|
71
|
+
balanced: {
|
|
72
|
+
name: "s3-balanced",
|
|
73
|
+
minChunkSize: 5 * 1024 * 1024, // 5MB - S3 minimum
|
|
74
|
+
maxChunkSize: 128 * 1024 * 1024, // 128MB
|
|
75
|
+
initialChunkSize: 16 * 1024 * 1024, // 16MB
|
|
76
|
+
adaptationRate: 0.2,
|
|
77
|
+
},
|
|
78
|
+
aggressive: {
|
|
79
|
+
name: "s3-aggressive",
|
|
80
|
+
minChunkSize: 5 * 1024 * 1024, // 5MB - S3 minimum
|
|
81
|
+
maxChunkSize: 256 * 1024 * 1024, // 256MB
|
|
82
|
+
initialChunkSize: 32 * 1024 * 1024, // 32MB
|
|
83
|
+
adaptationRate: 0.3,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export class SmartChunker {
|
|
88
|
+
private config: Required<Omit<SmartChunkerConfig, "datastoreConstraints">> & {
|
|
89
|
+
datastoreConstraints?: DatastoreConstraints;
|
|
90
|
+
};
|
|
91
|
+
private networkMonitor: NetworkMonitor;
|
|
92
|
+
private currentChunkSize: number;
|
|
93
|
+
private lastDecision: ChunkSizeDecision | null = null;
|
|
94
|
+
private consecutiveFailures = 0;
|
|
95
|
+
private consecutiveSuccesses = 0;
|
|
96
|
+
private connectionMetrics: ConnectionMetrics | null = null;
|
|
97
|
+
|
|
98
|
+
constructor(networkMonitor: NetworkMonitor, config: SmartChunkerConfig = {}) {
|
|
99
|
+
this.networkMonitor = networkMonitor;
|
|
100
|
+
this.config = {
|
|
101
|
+
enabled: config.enabled ?? true,
|
|
102
|
+
fallbackChunkSize: config.fallbackChunkSize ?? 1024 * 1024, // 1 MB
|
|
103
|
+
minChunkSize: config.minChunkSize ?? 64 * 1024, // 64 KB
|
|
104
|
+
maxChunkSize: config.maxChunkSize ?? 32 * 1024 * 1024, // 32 MB
|
|
105
|
+
initialChunkSize: config.initialChunkSize ?? 512 * 1024, // 512 KB
|
|
106
|
+
targetUtilization: config.targetUtilization ?? 0.85, // 85%
|
|
107
|
+
adaptationRate: config.adaptationRate ?? 0.2,
|
|
108
|
+
conservativeMode: config.conservativeMode ?? false,
|
|
109
|
+
connectionPoolingAware: config.connectionPoolingAware ?? true, // Enable by default
|
|
110
|
+
datastoreConstraints: config.datastoreConstraints,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
this.currentChunkSize = this.getEffectiveInitialChunkSize();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private getEffectiveInitialChunkSize(): number {
|
|
117
|
+
if (this.config.datastoreConstraints) {
|
|
118
|
+
return Math.max(
|
|
119
|
+
this.config.initialChunkSize,
|
|
120
|
+
this.config.datastoreConstraints.optimalChunkSize,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return this.config.initialChunkSize;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private applyDatastoreConstraints(size: number): number {
|
|
127
|
+
if (this.config.datastoreConstraints) {
|
|
128
|
+
return Math.max(
|
|
129
|
+
this.config.datastoreConstraints.minChunkSize,
|
|
130
|
+
Math.min(this.config.datastoreConstraints.maxChunkSize, size),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return size;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
getNextChunkSize(remainingBytes?: number): ChunkSizeDecision {
|
|
137
|
+
if (!this.config.enabled) {
|
|
138
|
+
return {
|
|
139
|
+
size: this.config.fallbackChunkSize,
|
|
140
|
+
strategy: "fixed",
|
|
141
|
+
reason: "Smart chunking disabled",
|
|
142
|
+
networkCondition: { type: "unknown", confidence: 0 },
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const networkCondition = this.networkMonitor.getNetworkCondition();
|
|
147
|
+
|
|
148
|
+
let newSize = this.currentChunkSize;
|
|
149
|
+
let strategy = "adaptive";
|
|
150
|
+
let reason = "";
|
|
151
|
+
|
|
152
|
+
// If we don't have enough data, use initial strategy
|
|
153
|
+
if (networkCondition.type === "unknown") {
|
|
154
|
+
newSize = this.config.initialChunkSize;
|
|
155
|
+
strategy = "initial";
|
|
156
|
+
reason = "Insufficient network data";
|
|
157
|
+
} else {
|
|
158
|
+
const chunkingStrategy = this.selectStrategy(networkCondition);
|
|
159
|
+
newSize = this.calculateOptimalChunkSize(
|
|
160
|
+
networkCondition,
|
|
161
|
+
chunkingStrategy,
|
|
162
|
+
);
|
|
163
|
+
strategy = chunkingStrategy.name;
|
|
164
|
+
reason = `Network condition: ${networkCondition.type} (confidence: ${Math.round(networkCondition.confidence * 100)}%)`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Apply remaining bytes limit
|
|
168
|
+
if (remainingBytes && remainingBytes < newSize) {
|
|
169
|
+
newSize = remainingBytes;
|
|
170
|
+
reason += `, limited by remaining bytes (${remainingBytes})`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Apply datastore constraints first
|
|
174
|
+
newSize = this.applyDatastoreConstraints(newSize);
|
|
175
|
+
|
|
176
|
+
// Ensure bounds
|
|
177
|
+
newSize = Math.max(
|
|
178
|
+
this.config.minChunkSize,
|
|
179
|
+
Math.min(this.config.maxChunkSize, newSize),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
this.currentChunkSize = newSize;
|
|
183
|
+
this.lastDecision = {
|
|
184
|
+
size: newSize,
|
|
185
|
+
strategy,
|
|
186
|
+
reason,
|
|
187
|
+
networkCondition,
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
return this.lastDecision;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
recordChunkResult(size: number, duration: number, success: boolean): void {
|
|
194
|
+
// Record the result in network monitor
|
|
195
|
+
this.networkMonitor.recordUpload(size, duration, success);
|
|
196
|
+
|
|
197
|
+
// Update our internal state
|
|
198
|
+
if (success) {
|
|
199
|
+
this.consecutiveSuccesses++;
|
|
200
|
+
this.consecutiveFailures = 0;
|
|
201
|
+
} else {
|
|
202
|
+
this.consecutiveFailures++;
|
|
203
|
+
this.consecutiveSuccesses = 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Adjust chunk size based on recent performance
|
|
207
|
+
this.adaptChunkSize(success, duration, size);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
getCurrentChunkSize(): number {
|
|
211
|
+
return this.currentChunkSize;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
getLastDecision(): ChunkSizeDecision | null {
|
|
215
|
+
return this.lastDecision;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
reset(): void {
|
|
219
|
+
this.currentChunkSize = this.config.initialChunkSize;
|
|
220
|
+
this.consecutiveFailures = 0;
|
|
221
|
+
this.consecutiveSuccesses = 0;
|
|
222
|
+
this.lastDecision = null;
|
|
223
|
+
this.connectionMetrics = null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Update connection metrics for connection pooling aware optimizations
|
|
228
|
+
*/
|
|
229
|
+
updateConnectionMetrics(metrics: ConnectionMetrics): void {
|
|
230
|
+
this.connectionMetrics = metrics;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get insights about connection pooling impact on chunking
|
|
235
|
+
*/
|
|
236
|
+
getConnectionPoolingInsights(): {
|
|
237
|
+
isOptimized: boolean;
|
|
238
|
+
reuseRate: number;
|
|
239
|
+
recommendedMinChunkSize: number;
|
|
240
|
+
connectionOverhead: number;
|
|
241
|
+
} {
|
|
242
|
+
if (!this.connectionMetrics || !this.config.connectionPoolingAware) {
|
|
243
|
+
return {
|
|
244
|
+
isOptimized: false,
|
|
245
|
+
reuseRate: 0,
|
|
246
|
+
recommendedMinChunkSize: this.config.minChunkSize,
|
|
247
|
+
connectionOverhead: 0,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const reuseRate = this.connectionMetrics.reuseRate;
|
|
252
|
+
const avgConnectionTime = this.connectionMetrics.averageConnectionTime;
|
|
253
|
+
|
|
254
|
+
// With good connection reuse, we can afford smaller chunks
|
|
255
|
+
const connectionOverhead = (1 - reuseRate) * avgConnectionTime;
|
|
256
|
+
const recommendedMinChunkSize = Math.max(
|
|
257
|
+
this.config.minChunkSize,
|
|
258
|
+
Math.floor(connectionOverhead * 10000), // 10KB per ms of overhead
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
isOptimized: reuseRate > 0.7,
|
|
263
|
+
reuseRate,
|
|
264
|
+
recommendedMinChunkSize,
|
|
265
|
+
connectionOverhead,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private selectStrategy(networkCondition: NetworkCondition): ChunkingStrategy {
|
|
270
|
+
const fallbackStrategy: ChunkingStrategy = {
|
|
271
|
+
name: "fallback",
|
|
272
|
+
minChunkSize: 128 * 1024,
|
|
273
|
+
maxChunkSize: 4 * 1024 * 1024,
|
|
274
|
+
initialChunkSize: 512 * 1024,
|
|
275
|
+
adaptationRate: 0.2,
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// Use S3-optimized strategies if datastore constraints indicate S3 (5MB minimum)
|
|
279
|
+
const isS3Like =
|
|
280
|
+
this.config.datastoreConstraints?.minChunkSize === 5 * 1024 * 1024;
|
|
281
|
+
const strategiesSource = isS3Like
|
|
282
|
+
? S3_OPTIMIZED_STRATEGIES
|
|
283
|
+
: DEFAULT_STRATEGIES;
|
|
284
|
+
|
|
285
|
+
if (this.config.conservativeMode) {
|
|
286
|
+
return strategiesSource.conservative ?? fallbackStrategy;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Enhanced strategy selection with connection pooling awareness
|
|
290
|
+
let baseStrategy: ChunkingStrategy;
|
|
291
|
+
|
|
292
|
+
switch (networkCondition.type) {
|
|
293
|
+
case "fast":
|
|
294
|
+
baseStrategy =
|
|
295
|
+
networkCondition.confidence > 0.7
|
|
296
|
+
? (strategiesSource.aggressive ?? fallbackStrategy)
|
|
297
|
+
: (strategiesSource.balanced ?? fallbackStrategy);
|
|
298
|
+
break;
|
|
299
|
+
case "slow":
|
|
300
|
+
baseStrategy = strategiesSource.conservative ?? fallbackStrategy;
|
|
301
|
+
break;
|
|
302
|
+
case "unstable":
|
|
303
|
+
baseStrategy = strategiesSource.conservative ?? fallbackStrategy;
|
|
304
|
+
break;
|
|
305
|
+
default:
|
|
306
|
+
baseStrategy = strategiesSource.balanced ?? fallbackStrategy;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Apply connection pooling optimizations
|
|
310
|
+
if (this.config.connectionPoolingAware && this.connectionMetrics) {
|
|
311
|
+
return this.optimizeStrategyForConnectionPooling(baseStrategy);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return baseStrategy;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Optimize chunking strategy based on connection pooling performance
|
|
319
|
+
*/
|
|
320
|
+
private optimizeStrategyForConnectionPooling(
|
|
321
|
+
strategy: ChunkingStrategy,
|
|
322
|
+
): ChunkingStrategy {
|
|
323
|
+
if (!this.connectionMetrics) return strategy;
|
|
324
|
+
|
|
325
|
+
const insights = this.getConnectionPoolingInsights();
|
|
326
|
+
const reuseRate = insights.reuseRate;
|
|
327
|
+
|
|
328
|
+
// High connection reuse allows for more aggressive chunking
|
|
329
|
+
if (reuseRate > 0.8) {
|
|
330
|
+
return {
|
|
331
|
+
...strategy,
|
|
332
|
+
name: `${strategy.name}-pooled-aggressive`,
|
|
333
|
+
minChunkSize: Math.max(strategy.minChunkSize * 0.5, 32 * 1024), // Smaller min chunks
|
|
334
|
+
adaptationRate: Math.min(strategy.adaptationRate * 1.3, 0.5), // Faster adaptation
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Good connection reuse allows moderate optimization
|
|
339
|
+
if (reuseRate > 0.5) {
|
|
340
|
+
return {
|
|
341
|
+
...strategy,
|
|
342
|
+
name: `${strategy.name}-pooled-moderate`,
|
|
343
|
+
minChunkSize: Math.max(strategy.minChunkSize * 0.75, 64 * 1024),
|
|
344
|
+
adaptationRate: Math.min(strategy.adaptationRate * 1.1, 0.4),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Poor connection reuse requires conservative approach
|
|
349
|
+
return {
|
|
350
|
+
...strategy,
|
|
351
|
+
name: `${strategy.name}-pooled-conservative`,
|
|
352
|
+
minChunkSize: Math.max(
|
|
353
|
+
strategy.minChunkSize * 1.5,
|
|
354
|
+
insights.recommendedMinChunkSize,
|
|
355
|
+
),
|
|
356
|
+
adaptationRate: strategy.adaptationRate * 0.8,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private calculateOptimalChunkSize(
|
|
361
|
+
networkCondition: NetworkCondition,
|
|
362
|
+
strategy: ChunkingStrategy,
|
|
363
|
+
): number {
|
|
364
|
+
let targetSize = this.currentChunkSize;
|
|
365
|
+
|
|
366
|
+
// Base calculation on current throughput
|
|
367
|
+
const optimalThroughput = this.networkMonitor.getOptimalThroughput();
|
|
368
|
+
|
|
369
|
+
if (optimalThroughput > 0) {
|
|
370
|
+
// Calculate target chunk duration (aim for 2-5 seconds per chunk)
|
|
371
|
+
const targetDuration = this.getTargetChunkDuration(networkCondition);
|
|
372
|
+
const theoreticalSize =
|
|
373
|
+
optimalThroughput * targetDuration * this.config.targetUtilization;
|
|
374
|
+
|
|
375
|
+
// Blend current size with theoretical optimal size
|
|
376
|
+
const blendFactor = strategy.adaptationRate;
|
|
377
|
+
targetSize =
|
|
378
|
+
this.currentChunkSize * (1 - blendFactor) +
|
|
379
|
+
theoreticalSize * blendFactor;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Apply strategy constraints
|
|
383
|
+
targetSize = Math.max(
|
|
384
|
+
strategy.minChunkSize,
|
|
385
|
+
Math.min(strategy.maxChunkSize, targetSize),
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
// Apply failure-based adjustments
|
|
389
|
+
if (this.consecutiveFailures > 0) {
|
|
390
|
+
// Reduce size on failures
|
|
391
|
+
const reductionFactor = Math.min(0.5, this.consecutiveFailures * 0.2);
|
|
392
|
+
targetSize *= 1 - reductionFactor;
|
|
393
|
+
} else if (this.consecutiveSuccesses > 2) {
|
|
394
|
+
// Gradually increase size on consistent success
|
|
395
|
+
const increaseFactor = Math.min(0.3, this.consecutiveSuccesses * 0.05);
|
|
396
|
+
targetSize *= 1 + increaseFactor;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return Math.round(targetSize);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private getTargetChunkDuration(networkCondition: NetworkCondition): number {
|
|
403
|
+
switch (networkCondition.type) {
|
|
404
|
+
case "fast":
|
|
405
|
+
return 3; // 3 seconds for fast connections
|
|
406
|
+
case "slow":
|
|
407
|
+
return 5; // 5 seconds for slow connections to reduce overhead
|
|
408
|
+
case "unstable":
|
|
409
|
+
return 2; // 2 seconds for unstable connections for quick recovery
|
|
410
|
+
default:
|
|
411
|
+
return 3; // Default to 3 seconds
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private adaptChunkSize(
|
|
416
|
+
success: boolean,
|
|
417
|
+
duration: number,
|
|
418
|
+
size: number,
|
|
419
|
+
): void {
|
|
420
|
+
if (!success) {
|
|
421
|
+
// On failure, be more conservative
|
|
422
|
+
this.currentChunkSize = Math.max(
|
|
423
|
+
this.config.minChunkSize,
|
|
424
|
+
this.currentChunkSize * 0.8,
|
|
425
|
+
);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// On success, check if we should adjust based on performance
|
|
430
|
+
const throughput = size / (duration / 1000); // bytes per second
|
|
431
|
+
const metrics = this.networkMonitor.getCurrentMetrics();
|
|
432
|
+
|
|
433
|
+
if (metrics.averageSpeed > 0) {
|
|
434
|
+
const utilizationRatio = throughput / metrics.averageSpeed;
|
|
435
|
+
|
|
436
|
+
if (utilizationRatio < this.config.targetUtilization * 0.8) {
|
|
437
|
+
// We're not utilizing bandwidth well, try larger chunks
|
|
438
|
+
this.currentChunkSize = Math.min(
|
|
439
|
+
this.config.maxChunkSize,
|
|
440
|
+
this.currentChunkSize * 1.1,
|
|
441
|
+
);
|
|
442
|
+
} else if (utilizationRatio > this.config.targetUtilization * 1.2) {
|
|
443
|
+
// We might be overloading, try smaller chunks
|
|
444
|
+
this.currentChunkSize = Math.max(
|
|
445
|
+
this.config.minChunkSize,
|
|
446
|
+
this.currentChunkSize * 0.95,
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { IdGenerationService } from "../services/id-generation-service";
|
|
2
|
+
import type { StorageService } from "../services/storage-service";
|
|
3
|
+
import {
|
|
4
|
+
type PreviousUpload,
|
|
5
|
+
previousUploadSchema,
|
|
6
|
+
} from "../types/previous-upload";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Client-side storage interface for managing upload resumption data.
|
|
10
|
+
*
|
|
11
|
+
* Provides methods to store, retrieve, and manage previous upload information,
|
|
12
|
+
* enabling the client to resume interrupted uploads from where they left off.
|
|
13
|
+
* This is essential for implementing reliable upload resumption across sessions.
|
|
14
|
+
*
|
|
15
|
+
* Storage keys are namespaced with "uploadista::" prefix and organized by
|
|
16
|
+
* file fingerprint to allow quick lookup of resumable uploads.
|
|
17
|
+
*
|
|
18
|
+
* @example Finding resumable uploads
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const storage = createClientStorage(localStorage);
|
|
21
|
+
*
|
|
22
|
+
* // Find all previous uploads
|
|
23
|
+
* const allUploads = await storage.findAllUploads();
|
|
24
|
+
*
|
|
25
|
+
* // Find uploads for a specific file
|
|
26
|
+
* const fingerprint = await computeFingerprint(file);
|
|
27
|
+
* const matches = await storage.findUploadsByFingerprint(fingerprint);
|
|
28
|
+
*
|
|
29
|
+
* if (matches.length > 0) {
|
|
30
|
+
* // Resume from the most recent upload
|
|
31
|
+
* const uploadId = matches[0].uploadId;
|
|
32
|
+
* await resumeUpload(uploadId);
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export type ClientStorage = {
|
|
37
|
+
/**
|
|
38
|
+
* Retrieves all stored upload records from client storage.
|
|
39
|
+
*
|
|
40
|
+
* Useful for debugging or displaying a list of resumable uploads to the user.
|
|
41
|
+
*
|
|
42
|
+
* @returns Array of all previous upload records
|
|
43
|
+
*/
|
|
44
|
+
findAllUploads: () => Promise<PreviousUpload[]>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Finds previous upload records matching a specific file fingerprint.
|
|
48
|
+
*
|
|
49
|
+
* This is the primary method for discovering resumable uploads.
|
|
50
|
+
* Returns uploads sorted by most recent first.
|
|
51
|
+
*
|
|
52
|
+
* @param fingerprint - The file fingerprint to search for
|
|
53
|
+
* @returns Array of matching upload records, or empty array if none found
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* const fingerprint = await computeFingerprint(file);
|
|
58
|
+
* const previous = await storage.findUploadsByFingerprint(fingerprint);
|
|
59
|
+
*
|
|
60
|
+
* if (previous.length > 0) {
|
|
61
|
+
* console.log(`Found ${previous.length} resumable uploads`);
|
|
62
|
+
* console.log(`Last upload was ${previous[0].offset} bytes`);
|
|
63
|
+
* }
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
findUploadsByFingerprint: (fingerprint: string) => Promise<PreviousUpload[]>;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Removes an upload record from client storage.
|
|
70
|
+
*
|
|
71
|
+
* Called after an upload completes successfully or is explicitly cancelled
|
|
72
|
+
* to clean up storage and prevent stale resumption attempts.
|
|
73
|
+
*
|
|
74
|
+
* @param clientStorageKey - The storage key returned by addUpload
|
|
75
|
+
*
|
|
76
|
+
* @example Cleanup after successful upload
|
|
77
|
+
* ```typescript
|
|
78
|
+
* await uploadFile(file);
|
|
79
|
+
* await storage.removeUpload(storageKey);
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
removeUpload: (clientStorageKey: string) => Promise<void>;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Stores an upload record in client storage for future resumption.
|
|
86
|
+
*
|
|
87
|
+
* Creates a namespaced storage key that includes the file fingerprint,
|
|
88
|
+
* making it easy to find resumable uploads later.
|
|
89
|
+
*
|
|
90
|
+
* @param fingerprint - File fingerprint for organizing uploads
|
|
91
|
+
* @param upload - Upload metadata to store (uploadId, offset, etc.)
|
|
92
|
+
* @param options - Options object containing ID generation service
|
|
93
|
+
* @returns The storage key that can be used to remove this upload later, or undefined if storage failed
|
|
94
|
+
*
|
|
95
|
+
* @example Storing upload progress
|
|
96
|
+
* ```typescript
|
|
97
|
+
* const fingerprint = await computeFingerprint(file);
|
|
98
|
+
* const key = await storage.addUpload(
|
|
99
|
+
* fingerprint,
|
|
100
|
+
* { uploadId: 'abc123', offset: 1024000 },
|
|
101
|
+
* { generateId: idService }
|
|
102
|
+
* );
|
|
103
|
+
*
|
|
104
|
+
* // Later, remove when complete
|
|
105
|
+
* if (key) await storage.removeUpload(key);
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
addUpload: (
|
|
109
|
+
fingerprint: string,
|
|
110
|
+
upload: PreviousUpload,
|
|
111
|
+
{ generateId }: { generateId: IdGenerationService },
|
|
112
|
+
) => Promise<string | undefined>;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Creates a ClientStorage instance using the provided storage service.
|
|
117
|
+
*
|
|
118
|
+
* This factory function wraps a platform-specific StorageService (e.g., localStorage,
|
|
119
|
+
* AsyncStorage) with the ClientStorage interface, providing a consistent API
|
|
120
|
+
* for upload resumption across different platforms.
|
|
121
|
+
*
|
|
122
|
+
* @param storageService - Platform-specific storage implementation
|
|
123
|
+
* @returns ClientStorage instance for managing upload records
|
|
124
|
+
*
|
|
125
|
+
* @example Browser with localStorage
|
|
126
|
+
* ```typescript
|
|
127
|
+
* const storage = createClientStorage({
|
|
128
|
+
* find: async (prefix) => {
|
|
129
|
+
* const items: Record<string, string> = {};
|
|
130
|
+
* for (let i = 0; i < localStorage.length; i++) {
|
|
131
|
+
* const key = localStorage.key(i);
|
|
132
|
+
* if (key?.startsWith(prefix)) {
|
|
133
|
+
* items[key] = localStorage.getItem(key) || '';
|
|
134
|
+
* }
|
|
135
|
+
* }
|
|
136
|
+
* return items;
|
|
137
|
+
* },
|
|
138
|
+
* setItem: async (key, value) => localStorage.setItem(key, value),
|
|
139
|
+
* removeItem: async (key) => localStorage.removeItem(key),
|
|
140
|
+
* });
|
|
141
|
+
* ```
|
|
142
|
+
*
|
|
143
|
+
* @example React Native with AsyncStorage
|
|
144
|
+
* ```typescript
|
|
145
|
+
* const storage = createClientStorage({
|
|
146
|
+
* find: async (prefix) => {
|
|
147
|
+
* const keys = await AsyncStorage.getAllKeys();
|
|
148
|
+
* const matching = keys.filter(k => k.startsWith(prefix));
|
|
149
|
+
* const pairs = await AsyncStorage.multiGet(matching);
|
|
150
|
+
* return Object.fromEntries(pairs);
|
|
151
|
+
* },
|
|
152
|
+
* setItem: async (key, value) => AsyncStorage.setItem(key, value),
|
|
153
|
+
* removeItem: async (key) => AsyncStorage.removeItem(key),
|
|
154
|
+
* });
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
export function createClientStorage(
|
|
158
|
+
storageService: StorageService,
|
|
159
|
+
): ClientStorage {
|
|
160
|
+
return {
|
|
161
|
+
findAllUploads: async () => {
|
|
162
|
+
const items = await storageService.find("uploadista::");
|
|
163
|
+
return Object.values(items).map((item) =>
|
|
164
|
+
previousUploadSchema.parse(JSON.parse(item)),
|
|
165
|
+
);
|
|
166
|
+
},
|
|
167
|
+
findUploadsByFingerprint: async (fingerprint: string) => {
|
|
168
|
+
const items = await storageService.find(`uploadista::${fingerprint}`);
|
|
169
|
+
return Object.values(items).map((item) =>
|
|
170
|
+
previousUploadSchema.parse(JSON.parse(item)),
|
|
171
|
+
);
|
|
172
|
+
},
|
|
173
|
+
removeUpload: (clientStorageKey: string) =>
|
|
174
|
+
storageService.removeItem(clientStorageKey),
|
|
175
|
+
addUpload: async (
|
|
176
|
+
fingerprint: string,
|
|
177
|
+
upload: PreviousUpload,
|
|
178
|
+
{ generateId }: { generateId: IdGenerationService },
|
|
179
|
+
) => {
|
|
180
|
+
const key = generateId.generate();
|
|
181
|
+
const clientStorageKey = `uploadista::${fingerprint}::${key}`;
|
|
182
|
+
await storageService.setItem(clientStorageKey, JSON.stringify(upload));
|
|
183
|
+
return clientStorageKey;
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { StorageService } from "../services/storage-service";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* In-memory fallback storage service for Expo
|
|
5
|
+
* Used when AsyncStorage is not available or for testing
|
|
6
|
+
*/
|
|
7
|
+
export function createInMemoryStorageService(): StorageService {
|
|
8
|
+
const storage = new Map<string, string>();
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
async getItem(key: string): Promise<string | null> {
|
|
12
|
+
return storage.get(key) ?? null;
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
async setItem(key: string, value: string): Promise<void> {
|
|
16
|
+
storage.set(key, value);
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
async removeItem(key: string): Promise<void> {
|
|
20
|
+
storage.delete(key);
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
async findAll(): Promise<Record<string, string>> {
|
|
24
|
+
return Object.fromEntries(storage.entries());
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
async find(prefix: string): Promise<Record<string, string>> {
|
|
28
|
+
return Object.fromEntries(
|
|
29
|
+
Array.from(storage.entries()).filter(([key]) => key.startsWith(prefix)),
|
|
30
|
+
);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|