@buenojs/bueno 0.8.3 → 0.8.5
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 +136 -16
- package/dist/cli/{index.js → bin.js} +3036 -1421
- package/dist/container/index.js +250 -0
- package/dist/context/index.js +219 -0
- package/dist/database/index.js +493 -0
- package/dist/frontend/index.js +7697 -0
- package/dist/health/index.js +364 -0
- package/dist/i18n/index.js +345 -0
- package/dist/index.js +11043 -6482
- package/dist/jobs/index.js +819 -0
- package/dist/lock/index.js +367 -0
- package/dist/logger/index.js +281 -0
- package/dist/metrics/index.js +289 -0
- package/dist/middleware/index.js +77 -0
- package/dist/migrations/index.js +571 -0
- package/dist/modules/index.js +3346 -0
- package/dist/notification/index.js +484 -0
- package/dist/observability/index.js +331 -0
- package/dist/openapi/index.js +776 -0
- package/dist/orm/index.js +1356 -0
- package/dist/router/index.js +886 -0
- package/dist/rpc/index.js +691 -0
- package/dist/schema/index.js +400 -0
- package/dist/telemetry/index.js +595 -0
- package/dist/template/index.js +640 -0
- package/dist/templates/index.js +640 -0
- package/dist/testing/index.js +1111 -0
- package/dist/types/index.js +60 -0
- package/package.json +121 -27
- package/src/cache/index.ts +2 -1
- package/src/cli/bin.ts +2 -2
- package/src/cli/commands/build.ts +183 -165
- package/src/cli/commands/dev.ts +96 -89
- package/src/cli/commands/generate.ts +142 -111
- package/src/cli/commands/help.ts +20 -16
- package/src/cli/commands/index.ts +3 -6
- package/src/cli/commands/migration.ts +124 -105
- package/src/cli/commands/new.ts +392 -438
- package/src/cli/commands/start.ts +81 -79
- package/src/cli/core/args.ts +68 -50
- package/src/cli/core/console.ts +89 -95
- package/src/cli/core/index.ts +4 -4
- package/src/cli/core/prompt.ts +65 -62
- package/src/cli/core/spinner.ts +23 -20
- package/src/cli/index.ts +46 -38
- package/src/cli/templates/database/index.ts +61 -0
- package/src/cli/templates/database/mysql.ts +14 -0
- package/src/cli/templates/database/none.ts +16 -0
- package/src/cli/templates/database/postgresql.ts +14 -0
- package/src/cli/templates/database/sqlite.ts +14 -0
- package/src/cli/templates/deploy.ts +29 -26
- package/src/cli/templates/docker.ts +41 -30
- package/src/cli/templates/frontend/index.ts +63 -0
- package/src/cli/templates/frontend/none.ts +17 -0
- package/src/cli/templates/frontend/react.ts +140 -0
- package/src/cli/templates/frontend/solid.ts +134 -0
- package/src/cli/templates/frontend/svelte.ts +131 -0
- package/src/cli/templates/frontend/vue.ts +130 -0
- package/src/cli/templates/generators/index.ts +339 -0
- package/src/cli/templates/generators/types.ts +56 -0
- package/src/cli/templates/index.ts +35 -2
- package/src/cli/templates/project/api.ts +81 -0
- package/src/cli/templates/project/default.ts +140 -0
- package/src/cli/templates/project/fullstack.ts +111 -0
- package/src/cli/templates/project/index.ts +95 -0
- package/src/cli/templates/project/minimal.ts +45 -0
- package/src/cli/templates/project/types.ts +94 -0
- package/src/cli/templates/project/website.ts +263 -0
- package/src/cli/utils/fs.ts +55 -41
- package/src/cli/utils/index.ts +3 -2
- package/src/cli/utils/strings.ts +47 -33
- package/src/cli/utils/version.ts +47 -0
- package/src/config/env-validation.ts +100 -0
- package/src/config/env.ts +169 -41
- package/src/config/index.ts +28 -20
- package/src/config/loader.ts +25 -16
- package/src/config/merge.ts +21 -10
- package/src/config/types.ts +545 -25
- package/src/config/validation.ts +215 -7
- package/src/container/forward-ref.ts +22 -22
- package/src/container/index.ts +34 -12
- package/src/context/index.ts +11 -1
- package/src/database/index.ts +7 -190
- package/src/database/orm/builder.ts +457 -0
- package/src/database/orm/casts/index.ts +130 -0
- package/src/database/orm/casts/types.ts +25 -0
- package/src/database/orm/compiler.ts +304 -0
- package/src/database/orm/hooks/index.ts +114 -0
- package/src/database/orm/index.ts +61 -0
- package/src/database/orm/model-registry.ts +59 -0
- package/src/database/orm/model.ts +821 -0
- package/src/database/orm/relationships/base.ts +146 -0
- package/src/database/orm/relationships/belongs-to-many.ts +179 -0
- package/src/database/orm/relationships/belongs-to.ts +56 -0
- package/src/database/orm/relationships/has-many.ts +45 -0
- package/src/database/orm/relationships/has-one.ts +41 -0
- package/src/database/orm/relationships/index.ts +11 -0
- package/src/database/orm/scopes/index.ts +55 -0
- package/src/events/__tests__/event-system.test.ts +235 -0
- package/src/events/config.ts +238 -0
- package/src/events/example-usage.ts +185 -0
- package/src/events/index.ts +278 -0
- package/src/events/manager.ts +385 -0
- package/src/events/registry.ts +182 -0
- package/src/events/types.ts +124 -0
- package/src/frontend/api-routes.ts +65 -23
- package/src/frontend/bundler.ts +76 -34
- package/src/frontend/console-client.ts +2 -2
- package/src/frontend/console-stream.ts +94 -38
- package/src/frontend/dev-server.ts +94 -46
- package/src/frontend/file-router.ts +61 -19
- package/src/frontend/frameworks/index.ts +37 -10
- package/src/frontend/frameworks/react.ts +10 -8
- package/src/frontend/frameworks/solid.ts +11 -9
- package/src/frontend/frameworks/svelte.ts +15 -9
- package/src/frontend/frameworks/vue.ts +13 -11
- package/src/frontend/hmr-client.ts +12 -10
- package/src/frontend/hmr.ts +146 -103
- package/src/frontend/index.ts +14 -5
- package/src/frontend/islands.ts +41 -22
- package/src/frontend/isr.ts +59 -37
- package/src/frontend/layout.ts +36 -21
- package/src/frontend/ssr/react.ts +74 -27
- package/src/frontend/ssr/solid.ts +54 -20
- package/src/frontend/ssr/svelte.ts +48 -14
- package/src/frontend/ssr/vue.ts +50 -18
- package/src/frontend/ssr.ts +83 -39
- package/src/frontend/types.ts +91 -56
- package/src/health/index.ts +21 -9
- package/src/i18n/engine.ts +305 -0
- package/src/i18n/index.ts +38 -0
- package/src/i18n/loader.ts +218 -0
- package/src/i18n/middleware.ts +164 -0
- package/src/i18n/negotiator.ts +162 -0
- package/src/i18n/types.ts +158 -0
- package/src/index.ts +179 -27
- package/src/jobs/drivers/memory.ts +315 -0
- package/src/jobs/drivers/redis.ts +459 -0
- package/src/jobs/index.ts +30 -0
- package/src/jobs/queue.ts +281 -0
- package/src/jobs/types.ts +295 -0
- package/src/jobs/worker.ts +380 -0
- package/src/logger/index.ts +1 -3
- package/src/logger/transports/index.ts +62 -22
- package/src/metrics/index.ts +25 -16
- package/src/migrations/index.ts +9 -0
- package/src/modules/filters.ts +13 -17
- package/src/modules/guards.ts +49 -26
- package/src/modules/index.ts +409 -298
- package/src/modules/interceptors.ts +58 -20
- package/src/modules/lazy.ts +11 -19
- package/src/modules/lifecycle.ts +15 -7
- package/src/modules/metadata.ts +15 -5
- package/src/modules/pipes.ts +94 -72
- package/src/notification/channels/base.ts +68 -0
- package/src/notification/channels/email.ts +105 -0
- package/src/notification/channels/push.ts +104 -0
- package/src/notification/channels/sms.ts +105 -0
- package/src/notification/channels/whatsapp.ts +104 -0
- package/src/notification/index.ts +48 -0
- package/src/notification/service.ts +354 -0
- package/src/notification/types.ts +344 -0
- package/src/observability/__tests__/observability.test.ts +483 -0
- package/src/observability/breadcrumbs.ts +114 -0
- package/src/observability/index.ts +136 -0
- package/src/observability/interceptor.ts +85 -0
- package/src/observability/service.ts +303 -0
- package/src/observability/trace.ts +37 -0
- package/src/observability/types.ts +196 -0
- package/src/openapi/__tests__/decorators.test.ts +335 -0
- package/src/openapi/__tests__/document-builder.test.ts +285 -0
- package/src/openapi/__tests__/route-scanner.test.ts +334 -0
- package/src/openapi/__tests__/schema-generator.test.ts +275 -0
- package/src/openapi/decorators.ts +328 -0
- package/src/openapi/document-builder.ts +274 -0
- package/src/openapi/index.ts +112 -0
- package/src/openapi/metadata.ts +112 -0
- package/src/openapi/route-scanner.ts +289 -0
- package/src/openapi/schema-generator.ts +256 -0
- package/src/openapi/swagger-module.ts +166 -0
- package/src/openapi/types.ts +398 -0
- package/src/orm/index.ts +10 -0
- package/src/rpc/index.ts +3 -1
- package/src/schema/index.ts +9 -0
- package/src/security/index.ts +15 -6
- package/src/ssg/index.ts +9 -8
- package/src/telemetry/index.ts +76 -22
- package/src/template/index.ts +7 -0
- package/src/templates/engine.ts +224 -0
- package/src/templates/index.ts +9 -0
- package/src/templates/loader.ts +331 -0
- package/src/templates/renderers/markdown.ts +212 -0
- package/src/templates/renderers/simple.ts +269 -0
- package/src/templates/types.ts +154 -0
- package/src/testing/index.ts +100 -27
- package/src/types/optional-deps.d.ts +347 -187
- package/src/validation/index.ts +92 -2
- package/src/validation/schemas.ts +536 -0
- package/tests/integration/fullstack.test.ts +4 -4
- package/tests/unit/database.test.ts +2 -72
- package/tests/unit/env-validation.test.ts +166 -0
- package/tests/unit/events.test.ts +910 -0
- package/tests/unit/i18n.test.ts +455 -0
- package/tests/unit/jobs.test.ts +493 -0
- package/tests/unit/notification.test.ts +988 -0
- package/tests/unit/observability.test.ts +453 -0
- package/tests/unit/orm/builder.test.ts +323 -0
- package/tests/unit/orm/casts.test.ts +179 -0
- package/tests/unit/orm/compiler.test.ts +220 -0
- package/tests/unit/orm/eager-loading.test.ts +285 -0
- package/tests/unit/orm/hooks.test.ts +191 -0
- package/tests/unit/orm/model.test.ts +373 -0
- package/tests/unit/orm/relationships.test.ts +303 -0
- package/tests/unit/orm/scopes.test.ts +74 -0
- package/tests/unit/templates-simple.test.ts +53 -0
- package/tests/unit/templates.test.ts +454 -0
- package/tests/unit/validation.test.ts +18 -24
- package/tsconfig.json +11 -3
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __export = (target, all) => {
|
|
4
|
+
for (var name in all)
|
|
5
|
+
__defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true,
|
|
8
|
+
configurable: true,
|
|
9
|
+
set: (newValue) => all[name] = () => newValue
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
var __legacyDecorateClassTS = function(decorators, target, key, desc) {
|
|
13
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
14
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
|
|
15
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
16
|
+
else
|
|
17
|
+
for (var i = decorators.length - 1;i >= 0; i--)
|
|
18
|
+
if (d = decorators[i])
|
|
19
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
20
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
21
|
+
};
|
|
22
|
+
var __legacyMetadataTS = (k, v) => {
|
|
23
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function")
|
|
24
|
+
return Reflect.metadata(k, v);
|
|
25
|
+
};
|
|
26
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
27
|
+
var __require = import.meta.require;
|
|
28
|
+
|
|
29
|
+
// src/rpc/index.ts
|
|
30
|
+
class DeduplicationStore {
|
|
31
|
+
defaultTTL;
|
|
32
|
+
pending = new Map;
|
|
33
|
+
cache = new Map;
|
|
34
|
+
cleanupInterval;
|
|
35
|
+
constructor(defaultTTL = 5000) {
|
|
36
|
+
this.defaultTTL = defaultTTL;
|
|
37
|
+
this.cleanupInterval = setInterval(() => this.cleanup(), 1e4);
|
|
38
|
+
}
|
|
39
|
+
cleanup() {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
42
|
+
if (now - entry.timestamp > entry.ttl) {
|
|
43
|
+
this.cache.delete(key);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
for (const [key, entry] of this.pending.entries()) {
|
|
47
|
+
if (now - entry.timestamp > 60000) {
|
|
48
|
+
this.pending.delete(key);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
getPending(key, body) {
|
|
53
|
+
const pending = this.pending.get(key);
|
|
54
|
+
if (pending && (body === undefined || pending.body === body)) {
|
|
55
|
+
return pending;
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
setPending(key, promise, body) {
|
|
60
|
+
this.pending.set(key, { promise, timestamp: Date.now(), body });
|
|
61
|
+
}
|
|
62
|
+
removePending(key) {
|
|
63
|
+
this.pending.delete(key);
|
|
64
|
+
}
|
|
65
|
+
getCached(key, ttl) {
|
|
66
|
+
const cached = this.cache.get(key);
|
|
67
|
+
if (cached && Date.now() - cached.timestamp < ttl) {
|
|
68
|
+
return cached.response.clone();
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
setCached(key, response, ttl) {
|
|
73
|
+
this.cache.set(key, {
|
|
74
|
+
response: response.clone(),
|
|
75
|
+
timestamp: Date.now(),
|
|
76
|
+
ttl
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
getCachedData(key) {
|
|
80
|
+
return this.cache.get(key)?.data;
|
|
81
|
+
}
|
|
82
|
+
setCachedData(key, data, ttl) {
|
|
83
|
+
this.cache.set(key, {
|
|
84
|
+
response: new Response(JSON.stringify(data)),
|
|
85
|
+
timestamp: Date.now(),
|
|
86
|
+
ttl
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
invalidate(key) {
|
|
90
|
+
this.cache.delete(key);
|
|
91
|
+
}
|
|
92
|
+
clear() {
|
|
93
|
+
this.pending.clear();
|
|
94
|
+
this.cache.clear();
|
|
95
|
+
}
|
|
96
|
+
destroy() {
|
|
97
|
+
if (this.cleanupInterval) {
|
|
98
|
+
clearInterval(this.cleanupInterval);
|
|
99
|
+
}
|
|
100
|
+
this.clear();
|
|
101
|
+
}
|
|
102
|
+
getStats() {
|
|
103
|
+
return {
|
|
104
|
+
pending: this.pending.size,
|
|
105
|
+
cached: this.cache.size
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
class OptimisticStore {
|
|
111
|
+
pending = new Map;
|
|
112
|
+
idCounter = 0;
|
|
113
|
+
create(cacheKey, optimisticData, previousData, callbacks) {
|
|
114
|
+
const id = `optimistic-${++this.idCounter}`;
|
|
115
|
+
this.pending.set(id, {
|
|
116
|
+
id,
|
|
117
|
+
cacheKey,
|
|
118
|
+
optimisticData,
|
|
119
|
+
previousData,
|
|
120
|
+
timestamp: Date.now(),
|
|
121
|
+
status: "pending",
|
|
122
|
+
onRollback: callbacks?.onRollback,
|
|
123
|
+
onConfirm: callbacks?.onConfirm
|
|
124
|
+
});
|
|
125
|
+
return id;
|
|
126
|
+
}
|
|
127
|
+
confirm(id, serverData) {
|
|
128
|
+
const update = this.pending.get(id);
|
|
129
|
+
if (update) {
|
|
130
|
+
update.status = "confirmed";
|
|
131
|
+
if (update.onConfirm && serverData !== undefined) {
|
|
132
|
+
update.onConfirm(serverData);
|
|
133
|
+
}
|
|
134
|
+
this.pending.delete(id);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
rollback(id) {
|
|
138
|
+
const update = this.pending.get(id);
|
|
139
|
+
if (update) {
|
|
140
|
+
update.status = "rolled_back";
|
|
141
|
+
const previousData = update.previousData;
|
|
142
|
+
if (update.onRollback) {
|
|
143
|
+
update.onRollback(previousData);
|
|
144
|
+
}
|
|
145
|
+
this.pending.delete(id);
|
|
146
|
+
return previousData;
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
get(id) {
|
|
151
|
+
return this.pending.get(id);
|
|
152
|
+
}
|
|
153
|
+
getByCacheKey(cacheKey) {
|
|
154
|
+
for (const update of this.pending.values()) {
|
|
155
|
+
if (update.cacheKey === cacheKey) {
|
|
156
|
+
return update;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
hasPending(cacheKey) {
|
|
162
|
+
for (const update of this.pending.values()) {
|
|
163
|
+
if (update.cacheKey === cacheKey && update.status === "pending") {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
getOptimisticData(cacheKey) {
|
|
170
|
+
const update = this.getByCacheKey(cacheKey);
|
|
171
|
+
if (update && update.status === "pending") {
|
|
172
|
+
return update.optimisticData;
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
clear() {
|
|
177
|
+
this.pending.clear();
|
|
178
|
+
}
|
|
179
|
+
getStats() {
|
|
180
|
+
return { pending: this.pending.size };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function defaultKeyGenerator(method, url, body) {
|
|
184
|
+
const bodyHash = body ? JSON.stringify(body) : "";
|
|
185
|
+
return `${method}:${url}:${bodyHash}`;
|
|
186
|
+
}
|
|
187
|
+
var DEFAULT_RETRYABLE_STATUS_CODES = [408, 429, 500, 502, 503, 504];
|
|
188
|
+
var DEFAULT_RETRYABLE_ERRORS = [
|
|
189
|
+
"ECONNRESET",
|
|
190
|
+
"ETIMEDOUT",
|
|
191
|
+
"ENOTFOUND",
|
|
192
|
+
"EAI_AGAIN"
|
|
193
|
+
];
|
|
194
|
+
function defaultShouldRetry(response, error, attempt, config) {
|
|
195
|
+
const maxAttempts = config?.maxAttempts ?? 3;
|
|
196
|
+
const retryableStatusCodes = config?.retryableStatusCodes ?? DEFAULT_RETRYABLE_STATUS_CODES;
|
|
197
|
+
const retryableErrors = config?.retryableErrors ?? DEFAULT_RETRYABLE_ERRORS;
|
|
198
|
+
if (attempt >= maxAttempts) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
if (error) {
|
|
202
|
+
if (retryableErrors.length > 0) {
|
|
203
|
+
return retryableErrors.some((code) => error.message.includes(code) || error.name === code);
|
|
204
|
+
}
|
|
205
|
+
return DEFAULT_RETRYABLE_ERRORS.some((code) => error.message.includes(code) || error.name === code);
|
|
206
|
+
}
|
|
207
|
+
if (response) {
|
|
208
|
+
if (retryableStatusCodes.length > 0) {
|
|
209
|
+
return retryableStatusCodes.includes(response.status);
|
|
210
|
+
}
|
|
211
|
+
return DEFAULT_RETRYABLE_STATUS_CODES.includes(response.status);
|
|
212
|
+
}
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
function calculateDelay(attempt, config) {
|
|
216
|
+
const delay = config.initialDelay * config.backoffMultiplier ** (attempt - 1);
|
|
217
|
+
return Math.min(delay, config.maxDelay);
|
|
218
|
+
}
|
|
219
|
+
function sleep(ms) {
|
|
220
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
class RPCClient {
|
|
224
|
+
baseUrl;
|
|
225
|
+
defaultHeaders;
|
|
226
|
+
defaultTimeout;
|
|
227
|
+
requestInterceptors = [];
|
|
228
|
+
responseInterceptors = [];
|
|
229
|
+
errorInterceptors = [];
|
|
230
|
+
deduplicationConfig;
|
|
231
|
+
optimisticConfig;
|
|
232
|
+
retryConfig;
|
|
233
|
+
deduplicationStore;
|
|
234
|
+
optimisticStore;
|
|
235
|
+
constructor(options) {
|
|
236
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
237
|
+
this.defaultHeaders = {
|
|
238
|
+
"Content-Type": "application/json",
|
|
239
|
+
...options.headers
|
|
240
|
+
};
|
|
241
|
+
this.defaultTimeout = options.timeout ?? 30000;
|
|
242
|
+
if (options.interceptors?.request) {
|
|
243
|
+
this.requestInterceptors = Array.isArray(options.interceptors.request) ? options.interceptors.request : [options.interceptors.request];
|
|
244
|
+
}
|
|
245
|
+
if (options.interceptors?.response) {
|
|
246
|
+
this.responseInterceptors = Array.isArray(options.interceptors.response) ? options.interceptors.response : [options.interceptors.response];
|
|
247
|
+
}
|
|
248
|
+
if (options.interceptors?.error) {
|
|
249
|
+
this.errorInterceptors = Array.isArray(options.interceptors.error) ? options.interceptors.error : [options.interceptors.error];
|
|
250
|
+
}
|
|
251
|
+
this.deduplicationConfig = {
|
|
252
|
+
enabled: options.deduplication?.enabled ?? true,
|
|
253
|
+
ttl: options.deduplication?.ttl ?? 5000,
|
|
254
|
+
keyGenerator: options.deduplication?.keyGenerator ?? defaultKeyGenerator
|
|
255
|
+
};
|
|
256
|
+
this.optimisticConfig = {
|
|
257
|
+
enabled: options.optimisticUpdates?.enabled ?? true,
|
|
258
|
+
autoRollback: options.optimisticUpdates?.autoRollback ?? true,
|
|
259
|
+
onConflict: options.optimisticUpdates?.onConflict ?? "rollback"
|
|
260
|
+
};
|
|
261
|
+
this.retryConfig = {
|
|
262
|
+
enabled: options.retry?.enabled ?? true,
|
|
263
|
+
maxAttempts: options.retry?.maxAttempts ?? 3,
|
|
264
|
+
initialDelay: options.retry?.initialDelay ?? 1000,
|
|
265
|
+
maxDelay: options.retry?.maxDelay ?? 30000,
|
|
266
|
+
backoffMultiplier: options.retry?.backoffMultiplier ?? 2,
|
|
267
|
+
retryableStatusCodes: options.retry?.retryableStatusCodes ?? [
|
|
268
|
+
...DEFAULT_RETRYABLE_STATUS_CODES
|
|
269
|
+
],
|
|
270
|
+
retryableErrors: options.retry?.retryableErrors ?? [
|
|
271
|
+
...DEFAULT_RETRYABLE_ERRORS
|
|
272
|
+
],
|
|
273
|
+
onRetry: options.retry?.onRetry ?? (() => {}),
|
|
274
|
+
shouldRetry: options.retry?.shouldRetry ?? ((response, error, attempt) => defaultShouldRetry(response, error, attempt, undefined))
|
|
275
|
+
};
|
|
276
|
+
this.deduplicationStore = new DeduplicationStore(this.deduplicationConfig.ttl);
|
|
277
|
+
this.optimisticStore = new OptimisticStore;
|
|
278
|
+
}
|
|
279
|
+
async get(path, options) {
|
|
280
|
+
return this.request("GET", path, undefined, options);
|
|
281
|
+
}
|
|
282
|
+
async post(path, body, options) {
|
|
283
|
+
return this.request("POST", path, body, options);
|
|
284
|
+
}
|
|
285
|
+
async put(path, body, options) {
|
|
286
|
+
return this.request("PUT", path, body, options);
|
|
287
|
+
}
|
|
288
|
+
async patch(path, body, options) {
|
|
289
|
+
return this.request("PATCH", path, body, options);
|
|
290
|
+
}
|
|
291
|
+
async delete(path, options) {
|
|
292
|
+
return this.request("DELETE", path, undefined, options);
|
|
293
|
+
}
|
|
294
|
+
async head(path, options) {
|
|
295
|
+
return this.request("HEAD", path, undefined, options);
|
|
296
|
+
}
|
|
297
|
+
async options(path, options) {
|
|
298
|
+
return this.request("OPTIONS", path, undefined, options);
|
|
299
|
+
}
|
|
300
|
+
async request(method, path, body, options) {
|
|
301
|
+
let url = `${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
|
|
302
|
+
if (options?.query) {
|
|
303
|
+
const searchParams = new URLSearchParams(options.query);
|
|
304
|
+
url += `?${searchParams.toString()}`;
|
|
305
|
+
}
|
|
306
|
+
const skipDeduplication = options?.skipDeduplication || options?.retry?.skipRetry;
|
|
307
|
+
if (this.deduplicationConfig.enabled && method === "GET" && !skipDeduplication) {
|
|
308
|
+
const cacheKey = this.deduplicationConfig.keyGenerator(method, url);
|
|
309
|
+
const cached = this.deduplicationStore.getCached(cacheKey, this.deduplicationConfig.ttl);
|
|
310
|
+
if (cached) {
|
|
311
|
+
return cached;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (this.deduplicationConfig.enabled && !skipDeduplication) {
|
|
315
|
+
const bodyStr = body ? JSON.stringify(body) : undefined;
|
|
316
|
+
const dedupeKey = this.deduplicationConfig.keyGenerator(method, url, body);
|
|
317
|
+
const pending = this.deduplicationStore.getPending(dedupeKey, bodyStr);
|
|
318
|
+
if (pending) {
|
|
319
|
+
return pending.promise;
|
|
320
|
+
}
|
|
321
|
+
const requestPromise = this.executeWithRetry(method, url, body, options, dedupeKey);
|
|
322
|
+
this.deduplicationStore.setPending(dedupeKey, requestPromise, bodyStr);
|
|
323
|
+
try {
|
|
324
|
+
const response = await requestPromise;
|
|
325
|
+
return response;
|
|
326
|
+
} finally {
|
|
327
|
+
this.deduplicationStore.removePending(dedupeKey);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return this.executeWithRetry(method, url, body, options);
|
|
331
|
+
}
|
|
332
|
+
async executeWithRetry(method, url, body, options, cacheKey) {
|
|
333
|
+
const retryOptions = options?.retry;
|
|
334
|
+
const retryEnabled = retryOptions?.enabled ?? this.retryConfig.enabled;
|
|
335
|
+
const skipRetry = retryOptions?.skipRetry ?? false;
|
|
336
|
+
if (!retryEnabled || skipRetry) {
|
|
337
|
+
return this.executeRequest(method, url, body, options, cacheKey);
|
|
338
|
+
}
|
|
339
|
+
const maxAttempts = retryOptions?.maxAttempts ?? this.retryConfig.maxAttempts;
|
|
340
|
+
const initialDelay = retryOptions?.initialDelay ?? this.retryConfig.initialDelay;
|
|
341
|
+
const state = {
|
|
342
|
+
attempt: 0,
|
|
343
|
+
lastError: null,
|
|
344
|
+
totalDelay: 0
|
|
345
|
+
};
|
|
346
|
+
while (state.attempt < maxAttempts) {
|
|
347
|
+
state.attempt++;
|
|
348
|
+
try {
|
|
349
|
+
const response = await this.executeRequest(method, url, body, options, cacheKey);
|
|
350
|
+
if (response.ok || !this.shouldRetryResponse(response, null, state.attempt, maxAttempts)) {
|
|
351
|
+
return response;
|
|
352
|
+
}
|
|
353
|
+
if (state.attempt < maxAttempts) {
|
|
354
|
+
const delay = calculateDelay(state.attempt, {
|
|
355
|
+
...this.retryConfig,
|
|
356
|
+
initialDelay,
|
|
357
|
+
maxAttempts
|
|
358
|
+
});
|
|
359
|
+
this.retryConfig.onRetry(state.attempt, null, delay);
|
|
360
|
+
await sleep(delay);
|
|
361
|
+
state.totalDelay += delay;
|
|
362
|
+
} else {
|
|
363
|
+
return response;
|
|
364
|
+
}
|
|
365
|
+
} catch (error) {
|
|
366
|
+
state.lastError = error instanceof Error ? error : new Error(String(error));
|
|
367
|
+
if (state.attempt < maxAttempts && this.shouldRetryError(state.lastError, state.attempt, maxAttempts)) {
|
|
368
|
+
const delay = calculateDelay(state.attempt, {
|
|
369
|
+
...this.retryConfig,
|
|
370
|
+
initialDelay,
|
|
371
|
+
maxAttempts
|
|
372
|
+
});
|
|
373
|
+
this.retryConfig.onRetry(state.attempt, state.lastError, delay);
|
|
374
|
+
await sleep(delay);
|
|
375
|
+
state.totalDelay += delay;
|
|
376
|
+
} else {
|
|
377
|
+
throw state.lastError;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
throw state.lastError || new Error("Max retry attempts exceeded");
|
|
382
|
+
}
|
|
383
|
+
shouldRetryResponse(response, error, attempt, maxAttempts) {
|
|
384
|
+
if (attempt >= maxAttempts) {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
return this.retryConfig.shouldRetry(response, error, attempt);
|
|
388
|
+
}
|
|
389
|
+
shouldRetryError(error, attempt, maxAttempts) {
|
|
390
|
+
if (attempt >= maxAttempts) {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
return this.retryConfig.shouldRetry(null, error, attempt);
|
|
394
|
+
}
|
|
395
|
+
async executeRequest(method, url, body, options, cacheKey) {
|
|
396
|
+
let config = {
|
|
397
|
+
method,
|
|
398
|
+
headers: {
|
|
399
|
+
...this.defaultHeaders,
|
|
400
|
+
...options?.headers
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
if (body && !["GET", "HEAD", "OPTIONS"].includes(method)) {
|
|
404
|
+
config.body = JSON.stringify(body);
|
|
405
|
+
}
|
|
406
|
+
const context = { url, method, requestInit: config };
|
|
407
|
+
for (const interceptor of this.requestInterceptors) {
|
|
408
|
+
const interceptorContext = {
|
|
409
|
+
...config,
|
|
410
|
+
url,
|
|
411
|
+
method
|
|
412
|
+
};
|
|
413
|
+
const result = await interceptor(interceptorContext);
|
|
414
|
+
config = result;
|
|
415
|
+
context.requestInit = config;
|
|
416
|
+
}
|
|
417
|
+
const controller = new AbortController;
|
|
418
|
+
const timeout = options?.timeout ?? this.defaultTimeout;
|
|
419
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
420
|
+
config.signal = controller.signal;
|
|
421
|
+
try {
|
|
422
|
+
let response = await fetch(url, config);
|
|
423
|
+
if (cacheKey && method === "GET" && response.ok) {
|
|
424
|
+
this.deduplicationStore.setCached(cacheKey, response, this.deduplicationConfig.ttl);
|
|
425
|
+
}
|
|
426
|
+
for (const interceptor of this.responseInterceptors) {
|
|
427
|
+
response = await interceptor(response, context);
|
|
428
|
+
}
|
|
429
|
+
return response;
|
|
430
|
+
} catch (error) {
|
|
431
|
+
const processedError = error instanceof Error ? error : new Error(String(error));
|
|
432
|
+
for (const interceptor of this.errorInterceptors) {
|
|
433
|
+
const result = await interceptor(processedError, context);
|
|
434
|
+
if (result instanceof Response) {
|
|
435
|
+
return result;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (processedError.name === "AbortError") {
|
|
439
|
+
throw new Error(`Request timeout after ${timeout}ms`);
|
|
440
|
+
}
|
|
441
|
+
throw processedError;
|
|
442
|
+
} finally {
|
|
443
|
+
clearTimeout(timeoutId);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
async optimistic(method, path, options) {
|
|
447
|
+
if (!this.optimisticConfig.enabled) {
|
|
448
|
+
const response = await this.request(method, path, options?.body, options);
|
|
449
|
+
return { response };
|
|
450
|
+
}
|
|
451
|
+
const cacheKey = options?.cacheKey ?? path;
|
|
452
|
+
const previousData = this.deduplicationStore.getCachedData(cacheKey);
|
|
453
|
+
const optimisticData = options?.optimisticData;
|
|
454
|
+
let rollbackId;
|
|
455
|
+
if (optimisticData !== undefined) {
|
|
456
|
+
rollbackId = this.optimisticStore.create(cacheKey, optimisticData, previousData, { onRollback: options?.onRollback, onConfirm: options?.onConfirm });
|
|
457
|
+
this.deduplicationStore.setCachedData(cacheKey, optimisticData, this.deduplicationConfig.ttl);
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
const response = await this.request(method, path, options?.body, {
|
|
461
|
+
query: options?.query,
|
|
462
|
+
headers: options?.headers,
|
|
463
|
+
skipDeduplication: true
|
|
464
|
+
});
|
|
465
|
+
if (response.ok) {
|
|
466
|
+
if (rollbackId) {
|
|
467
|
+
const responseData = await response.clone().json().catch(() => {
|
|
468
|
+
return;
|
|
469
|
+
});
|
|
470
|
+
this.optimisticStore.confirm(rollbackId, responseData);
|
|
471
|
+
}
|
|
472
|
+
return { response, rollbackId };
|
|
473
|
+
}
|
|
474
|
+
if (rollbackId && this.optimisticConfig.autoRollback) {
|
|
475
|
+
this.rollback(rollbackId);
|
|
476
|
+
}
|
|
477
|
+
return { response, rollbackId };
|
|
478
|
+
} catch (error) {
|
|
479
|
+
if (rollbackId && this.optimisticConfig.autoRollback) {
|
|
480
|
+
this.rollback(rollbackId);
|
|
481
|
+
}
|
|
482
|
+
throw error;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
async optimisticPost(path, body, options) {
|
|
486
|
+
return this.optimistic("POST", path, { body, ...options });
|
|
487
|
+
}
|
|
488
|
+
async optimisticPut(path, body, options) {
|
|
489
|
+
return this.optimistic("PUT", path, { body, ...options });
|
|
490
|
+
}
|
|
491
|
+
async optimisticPatch(path, body, options) {
|
|
492
|
+
return this.optimistic("PATCH", path, { body, ...options });
|
|
493
|
+
}
|
|
494
|
+
async optimisticDelete(path, options) {
|
|
495
|
+
return this.optimistic("DELETE", path, options);
|
|
496
|
+
}
|
|
497
|
+
rollback(rollbackId) {
|
|
498
|
+
const previousData = this.optimisticStore.rollback(rollbackId);
|
|
499
|
+
if (previousData !== undefined) {
|
|
500
|
+
const update = this.optimisticStore.get(rollbackId);
|
|
501
|
+
if (update) {
|
|
502
|
+
this.deduplicationStore.setCachedData(update.cacheKey, previousData, this.deduplicationConfig.ttl);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return previousData;
|
|
506
|
+
}
|
|
507
|
+
confirm(rollbackId, serverData) {
|
|
508
|
+
this.optimisticStore.confirm(rollbackId, serverData);
|
|
509
|
+
}
|
|
510
|
+
hasPendingOptimisticUpdate(cacheKey) {
|
|
511
|
+
return this.optimisticStore.hasPending(cacheKey);
|
|
512
|
+
}
|
|
513
|
+
getOptimisticData(cacheKey) {
|
|
514
|
+
return this.optimisticStore.getOptimisticData(cacheKey);
|
|
515
|
+
}
|
|
516
|
+
getPendingOptimisticCount() {
|
|
517
|
+
return this.optimisticStore.getStats().pending;
|
|
518
|
+
}
|
|
519
|
+
clearOptimisticUpdates() {
|
|
520
|
+
this.optimisticStore.clear();
|
|
521
|
+
}
|
|
522
|
+
async withRetry(method, path, body, retryOptions) {
|
|
523
|
+
return this.request(method, path, body, { retry: retryOptions });
|
|
524
|
+
}
|
|
525
|
+
isRetryEnabled() {
|
|
526
|
+
return this.retryConfig.enabled;
|
|
527
|
+
}
|
|
528
|
+
getMaxRetryAttempts() {
|
|
529
|
+
return this.retryConfig.maxAttempts;
|
|
530
|
+
}
|
|
531
|
+
getRetryConfig() {
|
|
532
|
+
return { ...this.retryConfig };
|
|
533
|
+
}
|
|
534
|
+
addRequestInterceptor(interceptor) {
|
|
535
|
+
this.requestInterceptors.push(interceptor);
|
|
536
|
+
}
|
|
537
|
+
addResponseInterceptor(interceptor) {
|
|
538
|
+
this.responseInterceptors.push(interceptor);
|
|
539
|
+
}
|
|
540
|
+
addErrorInterceptor(interceptor) {
|
|
541
|
+
this.errorInterceptors.push(interceptor);
|
|
542
|
+
}
|
|
543
|
+
removeRequestInterceptor(interceptor) {
|
|
544
|
+
const index = this.requestInterceptors.indexOf(interceptor);
|
|
545
|
+
if (index > -1) {
|
|
546
|
+
this.requestInterceptors.splice(index, 1);
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
removeResponseInterceptor(interceptor) {
|
|
552
|
+
const index = this.responseInterceptors.indexOf(interceptor);
|
|
553
|
+
if (index > -1) {
|
|
554
|
+
this.responseInterceptors.splice(index, 1);
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
removeErrorInterceptor(interceptor) {
|
|
560
|
+
const index = this.errorInterceptors.indexOf(interceptor);
|
|
561
|
+
if (index > -1) {
|
|
562
|
+
this.errorInterceptors.splice(index, 1);
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
clearInterceptors() {
|
|
568
|
+
this.requestInterceptors = [];
|
|
569
|
+
this.responseInterceptors = [];
|
|
570
|
+
this.errorInterceptors = [];
|
|
571
|
+
}
|
|
572
|
+
getInterceptorStats() {
|
|
573
|
+
return {
|
|
574
|
+
request: this.requestInterceptors.length,
|
|
575
|
+
response: this.responseInterceptors.length,
|
|
576
|
+
error: this.errorInterceptors.length
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
withInterceptors(interceptors) {
|
|
580
|
+
return new RPCClient({
|
|
581
|
+
baseUrl: this.baseUrl,
|
|
582
|
+
headers: this.defaultHeaders,
|
|
583
|
+
timeout: this.defaultTimeout,
|
|
584
|
+
deduplication: this.deduplicationConfig,
|
|
585
|
+
optimisticUpdates: this.optimisticConfig,
|
|
586
|
+
retry: this.retryConfig,
|
|
587
|
+
interceptors
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
withBaseUrl(baseUrl) {
|
|
591
|
+
const interceptors = {};
|
|
592
|
+
if (this.requestInterceptors.length > 0)
|
|
593
|
+
interceptors.request = this.requestInterceptors;
|
|
594
|
+
if (this.responseInterceptors.length > 0)
|
|
595
|
+
interceptors.response = this.responseInterceptors;
|
|
596
|
+
if (this.errorInterceptors.length > 0)
|
|
597
|
+
interceptors.error = this.errorInterceptors;
|
|
598
|
+
return new RPCClient({
|
|
599
|
+
baseUrl,
|
|
600
|
+
headers: this.defaultHeaders,
|
|
601
|
+
timeout: this.defaultTimeout,
|
|
602
|
+
deduplication: this.deduplicationConfig,
|
|
603
|
+
optimisticUpdates: this.optimisticConfig,
|
|
604
|
+
retry: this.retryConfig,
|
|
605
|
+
interceptors
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
withHeaders(headers) {
|
|
609
|
+
const interceptors = {};
|
|
610
|
+
if (this.requestInterceptors.length > 0)
|
|
611
|
+
interceptors.request = this.requestInterceptors;
|
|
612
|
+
if (this.responseInterceptors.length > 0)
|
|
613
|
+
interceptors.response = this.responseInterceptors;
|
|
614
|
+
if (this.errorInterceptors.length > 0)
|
|
615
|
+
interceptors.error = this.errorInterceptors;
|
|
616
|
+
return new RPCClient({
|
|
617
|
+
baseUrl: this.baseUrl,
|
|
618
|
+
headers: { ...this.defaultHeaders, ...headers },
|
|
619
|
+
timeout: this.defaultTimeout,
|
|
620
|
+
deduplication: this.deduplicationConfig,
|
|
621
|
+
optimisticUpdates: this.optimisticConfig,
|
|
622
|
+
retry: this.retryConfig,
|
|
623
|
+
interceptors
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
clearCache() {
|
|
627
|
+
this.deduplicationStore.clear();
|
|
628
|
+
}
|
|
629
|
+
clearAllCaches() {
|
|
630
|
+
this.deduplicationStore.clear();
|
|
631
|
+
this.optimisticStore.clear();
|
|
632
|
+
}
|
|
633
|
+
invalidateCache(cacheKey) {
|
|
634
|
+
this.deduplicationStore.invalidate(cacheKey);
|
|
635
|
+
}
|
|
636
|
+
getDeduplicationStats() {
|
|
637
|
+
return this.deduplicationStore.getStats();
|
|
638
|
+
}
|
|
639
|
+
isDeduplicationEnabled() {
|
|
640
|
+
return this.deduplicationConfig.enabled;
|
|
641
|
+
}
|
|
642
|
+
getDeduplicationTTL() {
|
|
643
|
+
return this.deduplicationConfig.ttl;
|
|
644
|
+
}
|
|
645
|
+
isOptimisticUpdatesEnabled() {
|
|
646
|
+
return this.optimisticConfig.enabled;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
function createRPClient(options) {
|
|
650
|
+
return new RPCClient(options);
|
|
651
|
+
}
|
|
652
|
+
function bc(options) {
|
|
653
|
+
return createRPClient(options);
|
|
654
|
+
}
|
|
655
|
+
function extractRouteTypes(router) {
|
|
656
|
+
const routes = router.getRoutes();
|
|
657
|
+
return routes.map((r) => ({
|
|
658
|
+
method: r.method,
|
|
659
|
+
path: r.pattern
|
|
660
|
+
}));
|
|
661
|
+
}
|
|
662
|
+
async function parseJSON(response) {
|
|
663
|
+
return response.json();
|
|
664
|
+
}
|
|
665
|
+
async function parseText(response) {
|
|
666
|
+
return response.text();
|
|
667
|
+
}
|
|
668
|
+
function isOK(response) {
|
|
669
|
+
return response.ok;
|
|
670
|
+
}
|
|
671
|
+
function isStatus(response, status) {
|
|
672
|
+
return response.status === status;
|
|
673
|
+
}
|
|
674
|
+
async function throwIfNotOK(response) {
|
|
675
|
+
if (!response.ok) {
|
|
676
|
+
const error = await response.text();
|
|
677
|
+
throw new Error(`HTTP ${response.status}: ${error}`);
|
|
678
|
+
}
|
|
679
|
+
return response;
|
|
680
|
+
}
|
|
681
|
+
export {
|
|
682
|
+
throwIfNotOK,
|
|
683
|
+
parseText,
|
|
684
|
+
parseJSON,
|
|
685
|
+
isStatus,
|
|
686
|
+
isOK,
|
|
687
|
+
extractRouteTypes,
|
|
688
|
+
createRPClient,
|
|
689
|
+
bc,
|
|
690
|
+
RPCClient
|
|
691
|
+
};
|