@nsky/sync 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/README.md +280 -0
- package/dist/core/index.cjs +277 -0
- package/dist/core/index.d.cts +2 -0
- package/dist/core/index.d.mts +2 -0
- package/dist/core/index.mjs +272 -0
- package/dist/core/index.mjs.map +1 -0
- package/dist/http/index.cjs +184 -0
- package/dist/http/index.d.cts +37 -0
- package/dist/http/index.d.cts.map +1 -0
- package/dist/http/index.d.mts +37 -0
- package/dist/http/index.d.mts.map +1 -0
- package/dist/http/index.mjs +184 -0
- package/dist/http/index.mjs.map +1 -0
- package/dist/index.cjs +23 -0
- package/dist/index.d.cts +56 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +56 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +5 -0
- package/dist/index2.d.cts +168 -0
- package/dist/index2.d.cts.map +1 -0
- package/dist/index2.d.mts +168 -0
- package/dist/index2.d.mts.map +1 -0
- package/dist/index3.d.cts +5 -0
- package/dist/index3.d.mts +5 -0
- package/dist/sql/index.cjs +149 -0
- package/dist/sql/index.d.cts +57 -0
- package/dist/sql/index.d.cts.map +1 -0
- package/dist/sql/index.d.mts +57 -0
- package/dist/sql/index.d.mts.map +1 -0
- package/dist/sql/index.mjs +126 -0
- package/dist/sql/index.mjs.map +1 -0
- package/dist/utils/index.cjs +148 -0
- package/dist/utils/index.d.cts +2 -0
- package/dist/utils/index.d.mts +2 -0
- package/dist/utils/index.mjs +143 -0
- package/dist/utils/index.mjs.map +1 -0
- package/package.json +90 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/utils/hlc.ts
|
|
3
|
+
var HLCClock = class {
|
|
4
|
+
#nodeId;
|
|
5
|
+
#now;
|
|
6
|
+
#lastWallTime = 0;
|
|
7
|
+
#counter = 0;
|
|
8
|
+
constructor(nodeId, now = Date.now) {
|
|
9
|
+
if (nodeId.length === 0) throw new Error("HLC node id is required.");
|
|
10
|
+
this.#nodeId = nodeId;
|
|
11
|
+
this.#now = now;
|
|
12
|
+
}
|
|
13
|
+
now() {
|
|
14
|
+
const wallTime = this.#now();
|
|
15
|
+
if (wallTime > this.#lastWallTime) {
|
|
16
|
+
this.#lastWallTime = wallTime;
|
|
17
|
+
this.#counter = 0;
|
|
18
|
+
} else this.#counter += 1;
|
|
19
|
+
return formatHLC({
|
|
20
|
+
wallTime: this.#lastWallTime,
|
|
21
|
+
counter: this.#counter,
|
|
22
|
+
nodeId: this.#nodeId
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
merge(remoteTimestamp) {
|
|
26
|
+
const remote = parseHLC(remoteTimestamp);
|
|
27
|
+
const wallTime = this.#now();
|
|
28
|
+
const maxWallTime = Math.max(wallTime, this.#lastWallTime, remote.wallTime);
|
|
29
|
+
if (maxWallTime === this.#lastWallTime && maxWallTime === remote.wallTime) this.#counter = Math.max(this.#counter, remote.counter) + 1;
|
|
30
|
+
else if (maxWallTime === this.#lastWallTime) this.#counter += 1;
|
|
31
|
+
else if (maxWallTime === remote.wallTime) this.#counter = remote.counter + 1;
|
|
32
|
+
else this.#counter = 0;
|
|
33
|
+
this.#lastWallTime = maxWallTime;
|
|
34
|
+
return formatHLC({
|
|
35
|
+
wallTime: this.#lastWallTime,
|
|
36
|
+
counter: this.#counter,
|
|
37
|
+
nodeId: this.#nodeId
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
function compareHLC(left, right) {
|
|
42
|
+
const a = parseHLC(left);
|
|
43
|
+
const b = parseHLC(right);
|
|
44
|
+
if (a.wallTime !== b.wallTime) return a.wallTime - b.wallTime;
|
|
45
|
+
if (a.counter !== b.counter) return a.counter - b.counter;
|
|
46
|
+
return a.nodeId.localeCompare(b.nodeId);
|
|
47
|
+
}
|
|
48
|
+
function parseHLC(timestamp) {
|
|
49
|
+
const match = /^(?<wallTime>\d{13})-(?<counter>\d{4})-(?<nodeId>.+)$/.exec(timestamp);
|
|
50
|
+
const wallTime = match?.groups?.wallTime;
|
|
51
|
+
const counter = match?.groups?.counter;
|
|
52
|
+
const nodeId = match?.groups?.nodeId;
|
|
53
|
+
if (wallTime === void 0 || counter === void 0 || nodeId === void 0) throw new Error(`Invalid HLC timestamp: ${timestamp}`);
|
|
54
|
+
return {
|
|
55
|
+
wallTime: Number(wallTime),
|
|
56
|
+
counter: Number(counter),
|
|
57
|
+
nodeId
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function formatHLC(timestamp) {
|
|
61
|
+
return `${timestamp.wallTime.toString().padStart(13, "0")}-${timestamp.counter.toString().padStart(4, "0")}-${timestamp.nodeId}`;
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/utils/id.ts
|
|
65
|
+
function createDeviceId() {
|
|
66
|
+
return `device-${createUuid()}`;
|
|
67
|
+
}
|
|
68
|
+
function createChangeId() {
|
|
69
|
+
return `change-${createUuid()}`;
|
|
70
|
+
}
|
|
71
|
+
function createUuid() {
|
|
72
|
+
if (globalThis.crypto?.randomUUID !== void 0) return globalThis.crypto.randomUUID();
|
|
73
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replaceAll(/[xy]/g, (token) => {
|
|
74
|
+
const value = Math.trunc(Math.random() * 16);
|
|
75
|
+
return (token === "x" ? value : value & 3 | 8).toString(16);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/utils/retry.ts
|
|
80
|
+
var ExponentialBackoffPolicy = class {
|
|
81
|
+
#maxAttempts;
|
|
82
|
+
#baseDelayMs;
|
|
83
|
+
#maxDelayMs;
|
|
84
|
+
#retryableCodes;
|
|
85
|
+
constructor(options = {}) {
|
|
86
|
+
this.#maxAttempts = options.maxAttempts ?? 3;
|
|
87
|
+
this.#baseDelayMs = options.baseDelayMs ?? 1e3;
|
|
88
|
+
this.#maxDelayMs = options.maxDelayMs ?? 3e4;
|
|
89
|
+
this.#retryableCodes = options.retryableCodes ?? /* @__PURE__ */ new Set([
|
|
90
|
+
"NETWORK_UNAVAILABLE",
|
|
91
|
+
"TRANSPORT_ERROR",
|
|
92
|
+
"SERVER_ERROR",
|
|
93
|
+
"UNKNOWN"
|
|
94
|
+
]);
|
|
95
|
+
}
|
|
96
|
+
decide(context) {
|
|
97
|
+
if (!this.#retryableCodes.has(context.lastError.code)) return { action: "abort" };
|
|
98
|
+
if (context.attempt >= this.#maxAttempts) return { action: "abort" };
|
|
99
|
+
if (context.attempt === 0) return {
|
|
100
|
+
action: "retry",
|
|
101
|
+
delayMs: 0
|
|
102
|
+
};
|
|
103
|
+
return {
|
|
104
|
+
action: "retry",
|
|
105
|
+
delayMs: Math.min(this.#baseDelayMs * 2 ** (context.attempt - 1), this.#maxDelayMs)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
async function retryOperation(operation, options) {
|
|
110
|
+
let attempt = 0;
|
|
111
|
+
let firstFailedAt = 0;
|
|
112
|
+
while (true) try {
|
|
113
|
+
return await operation();
|
|
114
|
+
} catch (error) {
|
|
115
|
+
const normalized = options.normalizeError(error);
|
|
116
|
+
firstFailedAt = firstFailedAt === 0 ? normalized.occurredAt : firstFailedAt;
|
|
117
|
+
const decision = options.policy.decide({
|
|
118
|
+
attempt,
|
|
119
|
+
firstFailedAt,
|
|
120
|
+
lastError: normalized
|
|
121
|
+
});
|
|
122
|
+
if (decision.action === "abort") throw error;
|
|
123
|
+
attempt += 1;
|
|
124
|
+
await (options.sleep ?? defaultSleep)(decision.delayMs);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function defaultSleep(delayMs) {
|
|
128
|
+
if (delayMs <= 0) return Promise.resolve();
|
|
129
|
+
return new Promise((resolve) => {
|
|
130
|
+
setTimeout(resolve, delayMs);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/utils/sleep.ts
|
|
135
|
+
function sleep(delayMs) {
|
|
136
|
+
if (delayMs <= 0) return Promise.resolve();
|
|
137
|
+
return new Promise((resolve) => {
|
|
138
|
+
setTimeout(resolve, delayMs);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
//#endregion
|
|
142
|
+
exports.ExponentialBackoffPolicy = ExponentialBackoffPolicy;
|
|
143
|
+
exports.HLCClock = HLCClock;
|
|
144
|
+
exports.compareHLC = compareHLC;
|
|
145
|
+
exports.createChangeId = createChangeId;
|
|
146
|
+
exports.createDeviceId = createDeviceId;
|
|
147
|
+
exports.retryOperation = retryOperation;
|
|
148
|
+
exports.sleep = sleep;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as RetryDecision, c as retryOperation, d as HLCClock, f as HLCTimestamp, i as RetryContext, l as createChangeId, n as ExponentialBackoffOptions, o as RetryOperationOptions, p as compareHLC, r as ExponentialBackoffPolicy, s as RetryPolicy, t as sleep, u as createDeviceId } from "../index.cjs";
|
|
2
|
+
export { ExponentialBackoffOptions, ExponentialBackoffPolicy, HLCClock, HLCTimestamp, RetryContext, RetryDecision, RetryOperationOptions, RetryPolicy, compareHLC, createChangeId, createDeviceId, retryOperation, sleep };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as RetryDecision, c as retryOperation, d as HLCClock, f as HLCTimestamp, i as RetryContext, l as createChangeId, n as ExponentialBackoffOptions, o as RetryOperationOptions, p as compareHLC, r as ExponentialBackoffPolicy, s as RetryPolicy, t as sleep, u as createDeviceId } from "../index.mjs";
|
|
2
|
+
export { ExponentialBackoffOptions, ExponentialBackoffPolicy, HLCClock, HLCTimestamp, RetryContext, RetryDecision, RetryOperationOptions, RetryPolicy, compareHLC, createChangeId, createDeviceId, retryOperation, sleep };
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
//#region src/utils/hlc.ts
|
|
2
|
+
var HLCClock = class {
|
|
3
|
+
#nodeId;
|
|
4
|
+
#now;
|
|
5
|
+
#lastWallTime = 0;
|
|
6
|
+
#counter = 0;
|
|
7
|
+
constructor(nodeId, now = Date.now) {
|
|
8
|
+
if (nodeId.length === 0) throw new Error("HLC node id is required.");
|
|
9
|
+
this.#nodeId = nodeId;
|
|
10
|
+
this.#now = now;
|
|
11
|
+
}
|
|
12
|
+
now() {
|
|
13
|
+
const wallTime = this.#now();
|
|
14
|
+
if (wallTime > this.#lastWallTime) {
|
|
15
|
+
this.#lastWallTime = wallTime;
|
|
16
|
+
this.#counter = 0;
|
|
17
|
+
} else this.#counter += 1;
|
|
18
|
+
return formatHLC({
|
|
19
|
+
wallTime: this.#lastWallTime,
|
|
20
|
+
counter: this.#counter,
|
|
21
|
+
nodeId: this.#nodeId
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
merge(remoteTimestamp) {
|
|
25
|
+
const remote = parseHLC(remoteTimestamp);
|
|
26
|
+
const wallTime = this.#now();
|
|
27
|
+
const maxWallTime = Math.max(wallTime, this.#lastWallTime, remote.wallTime);
|
|
28
|
+
if (maxWallTime === this.#lastWallTime && maxWallTime === remote.wallTime) this.#counter = Math.max(this.#counter, remote.counter) + 1;
|
|
29
|
+
else if (maxWallTime === this.#lastWallTime) this.#counter += 1;
|
|
30
|
+
else if (maxWallTime === remote.wallTime) this.#counter = remote.counter + 1;
|
|
31
|
+
else this.#counter = 0;
|
|
32
|
+
this.#lastWallTime = maxWallTime;
|
|
33
|
+
return formatHLC({
|
|
34
|
+
wallTime: this.#lastWallTime,
|
|
35
|
+
counter: this.#counter,
|
|
36
|
+
nodeId: this.#nodeId
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
function compareHLC(left, right) {
|
|
41
|
+
const a = parseHLC(left);
|
|
42
|
+
const b = parseHLC(right);
|
|
43
|
+
if (a.wallTime !== b.wallTime) return a.wallTime - b.wallTime;
|
|
44
|
+
if (a.counter !== b.counter) return a.counter - b.counter;
|
|
45
|
+
return a.nodeId.localeCompare(b.nodeId);
|
|
46
|
+
}
|
|
47
|
+
function parseHLC(timestamp) {
|
|
48
|
+
const match = /^(?<wallTime>\d{13})-(?<counter>\d{4})-(?<nodeId>.+)$/.exec(timestamp);
|
|
49
|
+
const wallTime = match?.groups?.wallTime;
|
|
50
|
+
const counter = match?.groups?.counter;
|
|
51
|
+
const nodeId = match?.groups?.nodeId;
|
|
52
|
+
if (wallTime === void 0 || counter === void 0 || nodeId === void 0) throw new Error(`Invalid HLC timestamp: ${timestamp}`);
|
|
53
|
+
return {
|
|
54
|
+
wallTime: Number(wallTime),
|
|
55
|
+
counter: Number(counter),
|
|
56
|
+
nodeId
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function formatHLC(timestamp) {
|
|
60
|
+
return `${timestamp.wallTime.toString().padStart(13, "0")}-${timestamp.counter.toString().padStart(4, "0")}-${timestamp.nodeId}`;
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/utils/id.ts
|
|
64
|
+
function createDeviceId() {
|
|
65
|
+
return `device-${createUuid()}`;
|
|
66
|
+
}
|
|
67
|
+
function createChangeId() {
|
|
68
|
+
return `change-${createUuid()}`;
|
|
69
|
+
}
|
|
70
|
+
function createUuid() {
|
|
71
|
+
if (globalThis.crypto?.randomUUID !== void 0) return globalThis.crypto.randomUUID();
|
|
72
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replaceAll(/[xy]/g, (token) => {
|
|
73
|
+
const value = Math.trunc(Math.random() * 16);
|
|
74
|
+
return (token === "x" ? value : value & 3 | 8).toString(16);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/utils/retry.ts
|
|
79
|
+
var ExponentialBackoffPolicy = class {
|
|
80
|
+
#maxAttempts;
|
|
81
|
+
#baseDelayMs;
|
|
82
|
+
#maxDelayMs;
|
|
83
|
+
#retryableCodes;
|
|
84
|
+
constructor(options = {}) {
|
|
85
|
+
this.#maxAttempts = options.maxAttempts ?? 3;
|
|
86
|
+
this.#baseDelayMs = options.baseDelayMs ?? 1e3;
|
|
87
|
+
this.#maxDelayMs = options.maxDelayMs ?? 3e4;
|
|
88
|
+
this.#retryableCodes = options.retryableCodes ?? /* @__PURE__ */ new Set([
|
|
89
|
+
"NETWORK_UNAVAILABLE",
|
|
90
|
+
"TRANSPORT_ERROR",
|
|
91
|
+
"SERVER_ERROR",
|
|
92
|
+
"UNKNOWN"
|
|
93
|
+
]);
|
|
94
|
+
}
|
|
95
|
+
decide(context) {
|
|
96
|
+
if (!this.#retryableCodes.has(context.lastError.code)) return { action: "abort" };
|
|
97
|
+
if (context.attempt >= this.#maxAttempts) return { action: "abort" };
|
|
98
|
+
if (context.attempt === 0) return {
|
|
99
|
+
action: "retry",
|
|
100
|
+
delayMs: 0
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
action: "retry",
|
|
104
|
+
delayMs: Math.min(this.#baseDelayMs * 2 ** (context.attempt - 1), this.#maxDelayMs)
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
async function retryOperation(operation, options) {
|
|
109
|
+
let attempt = 0;
|
|
110
|
+
let firstFailedAt = 0;
|
|
111
|
+
while (true) try {
|
|
112
|
+
return await operation();
|
|
113
|
+
} catch (error) {
|
|
114
|
+
const normalized = options.normalizeError(error);
|
|
115
|
+
firstFailedAt = firstFailedAt === 0 ? normalized.occurredAt : firstFailedAt;
|
|
116
|
+
const decision = options.policy.decide({
|
|
117
|
+
attempt,
|
|
118
|
+
firstFailedAt,
|
|
119
|
+
lastError: normalized
|
|
120
|
+
});
|
|
121
|
+
if (decision.action === "abort") throw error;
|
|
122
|
+
attempt += 1;
|
|
123
|
+
await (options.sleep ?? defaultSleep)(decision.delayMs);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function defaultSleep(delayMs) {
|
|
127
|
+
if (delayMs <= 0) return Promise.resolve();
|
|
128
|
+
return new Promise((resolve) => {
|
|
129
|
+
setTimeout(resolve, delayMs);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/utils/sleep.ts
|
|
134
|
+
function sleep(delayMs) {
|
|
135
|
+
if (delayMs <= 0) return Promise.resolve();
|
|
136
|
+
return new Promise((resolve) => {
|
|
137
|
+
setTimeout(resolve, delayMs);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
//#endregion
|
|
141
|
+
export { ExponentialBackoffPolicy, HLCClock, compareHLC, createChangeId, createDeviceId, retryOperation, sleep };
|
|
142
|
+
|
|
143
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["#nodeId","#now","#lastWallTime","#counter","#maxAttempts","#baseDelayMs","#maxDelayMs","#retryableCodes"],"sources":["../../src/utils/hlc.ts","../../src/utils/id.ts","../../src/utils/retry.ts","../../src/utils/sleep.ts"],"sourcesContent":["export type HLCTimestamp = string;\n\ninterface ParsedHLC {\n wallTime: number;\n counter: number;\n nodeId: string;\n}\n\nexport class HLCClock {\n readonly #nodeId: string;\n readonly #now: () => number;\n #lastWallTime = 0;\n #counter = 0;\n\n constructor(nodeId: string, now: () => number = Date.now) {\n if (nodeId.length === 0) {\n throw new Error(\"HLC node id is required.\");\n }\n\n this.#nodeId = nodeId;\n this.#now = now;\n }\n\n now(): HLCTimestamp {\n const wallTime = this.#now();\n\n if (wallTime > this.#lastWallTime) {\n this.#lastWallTime = wallTime;\n this.#counter = 0;\n } else {\n this.#counter += 1;\n }\n\n return formatHLC({\n wallTime: this.#lastWallTime,\n counter: this.#counter,\n nodeId: this.#nodeId,\n });\n }\n\n merge(remoteTimestamp: HLCTimestamp): HLCTimestamp {\n const remote = parseHLC(remoteTimestamp);\n const wallTime = this.#now();\n const maxWallTime = Math.max(wallTime, this.#lastWallTime, remote.wallTime);\n\n if (maxWallTime === this.#lastWallTime && maxWallTime === remote.wallTime) {\n this.#counter = Math.max(this.#counter, remote.counter) + 1;\n } else if (maxWallTime === this.#lastWallTime) {\n this.#counter += 1;\n } else if (maxWallTime === remote.wallTime) {\n this.#counter = remote.counter + 1;\n } else {\n this.#counter = 0;\n }\n\n this.#lastWallTime = maxWallTime;\n\n return formatHLC({\n wallTime: this.#lastWallTime,\n counter: this.#counter,\n nodeId: this.#nodeId,\n });\n }\n}\n\nexport function compareHLC(left: HLCTimestamp, right: HLCTimestamp): number {\n const a = parseHLC(left);\n const b = parseHLC(right);\n\n if (a.wallTime !== b.wallTime) {\n return a.wallTime - b.wallTime;\n }\n\n if (a.counter !== b.counter) {\n return a.counter - b.counter;\n }\n\n return a.nodeId.localeCompare(b.nodeId);\n}\n\nfunction parseHLC(timestamp: HLCTimestamp): ParsedHLC {\n const match = /^(?<wallTime>\\d{13})-(?<counter>\\d{4})-(?<nodeId>.+)$/.exec(timestamp);\n const wallTime = match?.groups?.wallTime;\n const counter = match?.groups?.counter;\n const nodeId = match?.groups?.nodeId;\n\n if (wallTime === undefined || counter === undefined || nodeId === undefined) {\n throw new Error(`Invalid HLC timestamp: ${timestamp}`);\n }\n\n return {\n wallTime: Number(wallTime),\n counter: Number(counter),\n nodeId,\n };\n}\n\nfunction formatHLC(timestamp: ParsedHLC): HLCTimestamp {\n return `${timestamp.wallTime.toString().padStart(13, \"0\")}-${timestamp.counter\n .toString()\n .padStart(4, \"0\")}-${timestamp.nodeId}`;\n}\n","export function createDeviceId(): string {\n return `device-${createUuid()}`;\n}\n\nexport function createChangeId(): string {\n return `change-${createUuid()}`;\n}\n\nfunction createUuid(): string {\n if (globalThis.crypto?.randomUUID !== undefined) {\n return globalThis.crypto.randomUUID();\n }\n\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replaceAll(/[xy]/g, (token) => {\n const value = Math.trunc(Math.random() * 16);\n const nibble = token === \"x\" ? value : (value & 0x3) | 0x8;\n return nibble.toString(16);\n });\n}\n","export interface RetryContext {\n readonly attempt: number;\n readonly lastError: {\n readonly code: string;\n readonly message: string;\n readonly occurredAt: number;\n };\n readonly firstFailedAt: number;\n}\n\nexport type RetryDecision =\n | { readonly action: \"retry\"; readonly delayMs: number }\n | { readonly action: \"abort\" };\n\nexport interface RetryPolicy {\n decide(context: RetryContext): RetryDecision;\n}\n\nexport interface RetryOperationOptions {\n readonly policy: RetryPolicy;\n readonly normalizeError: (error: unknown) => RetryContext[\"lastError\"];\n readonly sleep?: (delayMs: number) => Promise<void>;\n}\n\nexport interface ExponentialBackoffOptions {\n readonly maxAttempts?: number;\n readonly baseDelayMs?: number;\n readonly maxDelayMs?: number;\n readonly retryableCodes?: ReadonlySet<string>;\n}\n\nexport class ExponentialBackoffPolicy implements RetryPolicy {\n readonly #maxAttempts: number;\n readonly #baseDelayMs: number;\n readonly #maxDelayMs: number;\n readonly #retryableCodes: ReadonlySet<string>;\n\n constructor(options: ExponentialBackoffOptions = {}) {\n this.#maxAttempts = options.maxAttempts ?? 3;\n this.#baseDelayMs = options.baseDelayMs ?? 1_000;\n this.#maxDelayMs = options.maxDelayMs ?? 30_000;\n this.#retryableCodes =\n options.retryableCodes ??\n new Set([\"NETWORK_UNAVAILABLE\", \"TRANSPORT_ERROR\", \"SERVER_ERROR\", \"UNKNOWN\"]);\n }\n\n decide(context: RetryContext): RetryDecision {\n if (!this.#retryableCodes.has(context.lastError.code)) {\n return { action: \"abort\" };\n }\n\n if (context.attempt >= this.#maxAttempts) {\n return { action: \"abort\" };\n }\n\n if (context.attempt === 0) {\n return { action: \"retry\", delayMs: 0 };\n }\n\n const delayMs = Math.min(this.#baseDelayMs * 2 ** (context.attempt - 1), this.#maxDelayMs);\n return { action: \"retry\", delayMs };\n }\n}\n\nexport async function retryOperation<T>(\n operation: () => Promise<T>,\n options: RetryOperationOptions,\n): Promise<T> {\n let attempt = 0;\n let firstFailedAt = 0;\n\n while (true) {\n try {\n // Retry attempts are intentionally serial; each decision depends on the previous failure.\n // oxlint-disable-next-line no-await-in-loop\n return await operation();\n } catch (error) {\n const normalized = options.normalizeError(error);\n firstFailedAt = firstFailedAt === 0 ? normalized.occurredAt : firstFailedAt;\n const decision = options.policy.decide({\n attempt,\n firstFailedAt,\n lastError: normalized,\n });\n\n if (decision.action === \"abort\") {\n throw error;\n }\n\n attempt += 1;\n // oxlint-disable-next-line no-await-in-loop\n await (options.sleep ?? defaultSleep)(decision.delayMs);\n }\n }\n}\n\nfunction defaultSleep(delayMs: number): Promise<void> {\n if (delayMs <= 0) {\n return Promise.resolve();\n }\n\n return new Promise((resolve) => {\n setTimeout(resolve, delayMs);\n });\n}\n","export function sleep(delayMs: number): Promise<void> {\n if (delayMs <= 0) {\n return Promise.resolve();\n }\n\n return new Promise((resolve) => {\n setTimeout(resolve, delayMs);\n });\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nsky/sync",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Headless local-first sync engine and adapters for @nsky packages.",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist",
|
|
7
|
+
"README.md"
|
|
8
|
+
],
|
|
9
|
+
"type": "module",
|
|
10
|
+
"sideEffects": false,
|
|
11
|
+
"main": "./dist/index.mjs",
|
|
12
|
+
"module": "./dist/index.mjs",
|
|
13
|
+
"types": "./dist/index.d.mts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"import": {
|
|
17
|
+
"types": "./dist/index.d.mts",
|
|
18
|
+
"default": "./dist/index.mjs"
|
|
19
|
+
},
|
|
20
|
+
"require": {
|
|
21
|
+
"types": "./dist/index.d.cts",
|
|
22
|
+
"default": "./dist/index.cjs"
|
|
23
|
+
},
|
|
24
|
+
"default": "./dist/index.mjs"
|
|
25
|
+
},
|
|
26
|
+
"./core": {
|
|
27
|
+
"import": {
|
|
28
|
+
"types": "./dist/core/index.d.mts",
|
|
29
|
+
"default": "./dist/core/index.mjs"
|
|
30
|
+
},
|
|
31
|
+
"require": {
|
|
32
|
+
"types": "./dist/core/index.d.cts",
|
|
33
|
+
"default": "./dist/core/index.cjs"
|
|
34
|
+
},
|
|
35
|
+
"default": "./dist/core/index.mjs"
|
|
36
|
+
},
|
|
37
|
+
"./utils": {
|
|
38
|
+
"import": {
|
|
39
|
+
"types": "./dist/utils/index.d.mts",
|
|
40
|
+
"default": "./dist/utils/index.mjs"
|
|
41
|
+
},
|
|
42
|
+
"require": {
|
|
43
|
+
"types": "./dist/utils/index.d.cts",
|
|
44
|
+
"default": "./dist/utils/index.cjs"
|
|
45
|
+
},
|
|
46
|
+
"default": "./dist/utils/index.mjs"
|
|
47
|
+
},
|
|
48
|
+
"./sql": {
|
|
49
|
+
"import": {
|
|
50
|
+
"types": "./dist/sql/index.d.mts",
|
|
51
|
+
"default": "./dist/sql/index.mjs"
|
|
52
|
+
},
|
|
53
|
+
"require": {
|
|
54
|
+
"types": "./dist/sql/index.d.cts",
|
|
55
|
+
"default": "./dist/sql/index.cjs"
|
|
56
|
+
},
|
|
57
|
+
"default": "./dist/sql/index.mjs"
|
|
58
|
+
},
|
|
59
|
+
"./http": {
|
|
60
|
+
"import": {
|
|
61
|
+
"types": "./dist/http/index.d.mts",
|
|
62
|
+
"default": "./dist/http/index.mjs"
|
|
63
|
+
},
|
|
64
|
+
"require": {
|
|
65
|
+
"types": "./dist/http/index.d.cts",
|
|
66
|
+
"default": "./dist/http/index.cjs"
|
|
67
|
+
},
|
|
68
|
+
"default": "./dist/http/index.mjs"
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"publishConfig": {
|
|
72
|
+
"access": "public"
|
|
73
|
+
},
|
|
74
|
+
"dependencies": {
|
|
75
|
+
"dexie": "^4.2.2"
|
|
76
|
+
},
|
|
77
|
+
"devDependencies": {
|
|
78
|
+
"fake-indexeddb": "^6.2.5"
|
|
79
|
+
},
|
|
80
|
+
"engines": {
|
|
81
|
+
"node": "^22.18.0 || ^24.0.0"
|
|
82
|
+
},
|
|
83
|
+
"scripts": {
|
|
84
|
+
"build": "pnpm exec tsdown",
|
|
85
|
+
"dev": "pnpm exec tsdown --watch",
|
|
86
|
+
"publint": "publint",
|
|
87
|
+
"test": "vitest run",
|
|
88
|
+
"typecheck": "tsc --noEmit"
|
|
89
|
+
}
|
|
90
|
+
}
|