@honch/react-native-relay 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/retry.ts ADDED
@@ -0,0 +1,13 @@
1
+ const INITIAL_BACKOFF_MS = 1000;
2
+ const MAX_BACKOFF_MS = 300000;
3
+ const JITTER_RATIO = 0.25;
4
+
5
+ export function nextBackoffDelayMs(
6
+ attempt: number,
7
+ random: () => number = Math.random
8
+ ): number {
9
+ const safeAttempt = Math.max(0, attempt);
10
+ const base = Math.min(INITIAL_BACKOFF_MS * 2 ** safeAttempt, MAX_BACKOFF_MS);
11
+ const jitter = 1 - JITTER_RATIO + random() * JITTER_RATIO * 2;
12
+ return Math.min(Math.round(base * jitter), MAX_BACKOFF_MS);
13
+ }
@@ -0,0 +1,20 @@
1
+ export interface RelayUploadSchedulerNative {
2
+ scheduleUpload(delayMs: number): Promise<void>;
3
+ cancelUpload(): Promise<void>;
4
+ }
5
+
6
+ export type RelayUploadSchedulerOptions = {
7
+ native: RelayUploadSchedulerNative;
8
+ };
9
+
10
+ export function createRelayUploadScheduler(options: RelayUploadSchedulerOptions) {
11
+ return {
12
+ schedule(delayMs: number): Promise<void> {
13
+ return options.native.scheduleUpload(delayMs);
14
+ },
15
+
16
+ cancel(): Promise<void> {
17
+ return options.native.cancelUpload();
18
+ }
19
+ };
20
+ }
@@ -0,0 +1,206 @@
1
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+
4
+ import type { DurableRelayChunk, RelayDurableStore } from "../durableStore";
5
+ import type { RelayRetryState } from "../relayQueue";
6
+ import type { StoredRelayMessage } from "../relayQueue";
7
+
8
+ type JsonRelayStoreState = {
9
+ chunks: JsonRelayChunk[];
10
+ messages: JsonRelayMessage[];
11
+ };
12
+
13
+ type JsonRelayChunk = {
14
+ deviceId: string;
15
+ sourceType: number;
16
+ sequence: string;
17
+ offset: number;
18
+ frameBytes: number[];
19
+ payload: number[];
20
+ finalEnd?: number;
21
+ };
22
+
23
+ type JsonRelayMessage = {
24
+ deviceId: string;
25
+ sourceType: number;
26
+ sequence: string;
27
+ body: number[];
28
+ retryAttempt?: number;
29
+ nextAttemptAtMs?: number;
30
+ };
31
+
32
+ const EMPTY_STATE: JsonRelayStoreState = {
33
+ chunks: [],
34
+ messages: []
35
+ };
36
+
37
+ // Defaults match the MMKV store so retention is consistent across relay stores.
38
+ const DEFAULT_MAX_CHUNKS = 4096;
39
+ const DEFAULT_MAX_COMPLETE_MESSAGES = 1024;
40
+
41
+ export type JsonFileRelayStoreOptions = {
42
+ // Bound the on-disk store so it cannot grow without limit when uploads stall;
43
+ // when exceeded, the oldest entries are dropped (drop-oldest). No time TTL.
44
+ maxChunks?: number;
45
+ maxCompleteMessages?: number;
46
+ };
47
+
48
+ export function createJsonFileRelayStore(
49
+ filePath: string,
50
+ options: JsonFileRelayStoreOptions = {}
51
+ ): RelayDurableStore {
52
+ return new JsonFileRelayStore(
53
+ filePath,
54
+ options.maxChunks ?? DEFAULT_MAX_CHUNKS,
55
+ options.maxCompleteMessages ?? DEFAULT_MAX_COMPLETE_MESSAGES
56
+ );
57
+ }
58
+
59
+ class JsonFileRelayStore implements RelayDurableStore {
60
+ constructor(
61
+ private readonly filePath: string,
62
+ private readonly maxChunks: number,
63
+ private readonly maxCompleteMessages: number
64
+ ) {}
65
+
66
+ async putChunk(chunk: DurableRelayChunk): Promise<void> {
67
+ const state = await this.readState();
68
+ const serialized = serializeChunk(chunk);
69
+ const index = state.chunks.findIndex(
70
+ (entry) =>
71
+ entry.deviceId === chunk.deviceId &&
72
+ entry.sequence === chunk.sequence &&
73
+ entry.offset === chunk.offset
74
+ );
75
+ if (index >= 0) {
76
+ state.chunks[index] = serialized;
77
+ } else {
78
+ state.chunks.push(serialized);
79
+ while (state.chunks.length > this.maxChunks) {
80
+ state.chunks.shift();
81
+ }
82
+ }
83
+ await this.writeState(state);
84
+ }
85
+
86
+ async chunks(deviceId: string, sequence: string): Promise<DurableRelayChunk[]> {
87
+ const state = await this.readState();
88
+ return state.chunks
89
+ .filter((chunk) => chunk.deviceId === deviceId && chunk.sequence === sequence)
90
+ .map(deserializeChunk);
91
+ }
92
+
93
+ async putCompleteMessage(message: StoredRelayMessage): Promise<void> {
94
+ const state = await this.readState();
95
+ const serialized = serializeMessage(message);
96
+ const index = state.messages.findIndex(
97
+ (entry) => entry.deviceId === message.deviceId && entry.sequence === message.sequence
98
+ );
99
+ if (index >= 0) {
100
+ state.messages[index] = serialized;
101
+ } else {
102
+ state.messages.push(serialized);
103
+ while (state.messages.length > this.maxCompleteMessages) {
104
+ state.messages.shift();
105
+ }
106
+ }
107
+ await this.writeState(state);
108
+ }
109
+
110
+ async completeMessages(): Promise<StoredRelayMessage[]> {
111
+ const state = await this.readState();
112
+ return state.messages.map(deserializeMessage);
113
+ }
114
+
115
+ async markRetry(deviceId: string, sequence: string, retry: RelayRetryState): Promise<void> {
116
+ const state = await this.readState();
117
+ const message = state.messages.find(
118
+ (entry) => entry.deviceId === deviceId && entry.sequence === sequence
119
+ );
120
+ if (message !== undefined) {
121
+ message.retryAttempt = retry.attempt;
122
+ message.nextAttemptAtMs = retry.nextAttemptAtMs;
123
+ await this.writeState(state);
124
+ }
125
+ }
126
+
127
+ async deleteMessage(deviceId: string, sequence: string): Promise<void> {
128
+ const state = await this.readState();
129
+ state.messages = state.messages.filter(
130
+ (message) => message.deviceId !== deviceId || message.sequence !== sequence
131
+ );
132
+ state.chunks = state.chunks.filter(
133
+ (chunk) => chunk.deviceId !== deviceId || chunk.sequence !== sequence
134
+ );
135
+ await this.writeState(state);
136
+ }
137
+
138
+ private async readState(): Promise<JsonRelayStoreState> {
139
+ try {
140
+ const raw = await readFile(this.filePath, "utf8");
141
+ const parsed = JSON.parse(raw) as JsonRelayStoreState;
142
+ return {
143
+ chunks: parsed.chunks ?? [],
144
+ messages: parsed.messages ?? []
145
+ };
146
+ } catch (error) {
147
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
148
+ return { chunks: [], messages: [] };
149
+ }
150
+ throw error;
151
+ }
152
+ }
153
+
154
+ private async writeState(state: JsonRelayStoreState): Promise<void> {
155
+ await mkdir(dirname(this.filePath), { recursive: true });
156
+ const tempPath = `${this.filePath}.tmp`;
157
+ await writeFile(tempPath, JSON.stringify(state));
158
+ await rename(tempPath, this.filePath);
159
+ }
160
+ }
161
+
162
+ function serializeChunk(chunk: DurableRelayChunk): JsonRelayChunk {
163
+ return {
164
+ deviceId: chunk.deviceId,
165
+ sourceType: chunk.sourceType,
166
+ sequence: chunk.sequence,
167
+ offset: chunk.offset,
168
+ frameBytes: Array.from(chunk.frameBytes),
169
+ payload: Array.from(chunk.payload),
170
+ finalEnd: chunk.finalEnd
171
+ };
172
+ }
173
+
174
+ function deserializeChunk(chunk: JsonRelayChunk): DurableRelayChunk {
175
+ return {
176
+ deviceId: chunk.deviceId,
177
+ sourceType: chunk.sourceType,
178
+ sequence: chunk.sequence,
179
+ offset: chunk.offset,
180
+ frameBytes: new Uint8Array(chunk.frameBytes),
181
+ payload: new Uint8Array(chunk.payload),
182
+ finalEnd: chunk.finalEnd
183
+ };
184
+ }
185
+
186
+ function serializeMessage(message: StoredRelayMessage): JsonRelayMessage {
187
+ return {
188
+ deviceId: message.deviceId,
189
+ sourceType: message.sourceType,
190
+ sequence: message.sequence,
191
+ body: Array.from(message.body),
192
+ retryAttempt: message.retryAttempt,
193
+ nextAttemptAtMs: message.nextAttemptAtMs
194
+ };
195
+ }
196
+
197
+ function deserializeMessage(message: JsonRelayMessage): StoredRelayMessage {
198
+ return {
199
+ deviceId: message.deviceId,
200
+ sourceType: message.sourceType,
201
+ sequence: message.sequence,
202
+ body: new Uint8Array(message.body),
203
+ retryAttempt: message.retryAttempt,
204
+ nextAttemptAtMs: message.nextAttemptAtMs
205
+ };
206
+ }