@grest-ts/http 0.0.51 → 0.0.55
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/dist/src/_dedupCheck.js +1 -1
- package/dist/src/client/GGContextKeySynchronizer.d.ts.map +1 -1
- package/dist/src/client/GGContextKeySynchronizer.js +14 -1
- package/dist/src/client/GGContextKeySynchronizer.js.map +1 -1
- package/dist/src/client/GGHttpSchema.createClient.d.ts.map +1 -1
- package/dist/src/client/GGHttpSchema.createClient.js +3 -2
- package/dist/src/client/GGHttpSchema.createClient.js.map +1 -1
- package/dist/src/rpc/RpcResponse/GGRpcResponseBuilder.js.map +1 -1
- package/dist/src/schema/GGCookie.js +3 -2
- package/dist/src/schema/GGCookie.js.map +1 -1
- package/dist/src/schema/GGHttpPermissionsChecker.d.ts +2 -2
- package/dist/src/schema/GGHttpPermissionsChecker.d.ts.map +1 -1
- package/dist/src/schema/GGHttpPermissionsChecker.js +2 -0
- package/dist/src/schema/GGHttpPermissionsChecker.js.map +1 -1
- package/dist/src/schema/GGHttpSchema.d.ts +1 -1
- package/dist/src/schema/GGHttpSchema.d.ts.map +1 -1
- package/dist/src/schema/GGHttpSchema.js +1 -1
- package/dist/src/schema/GGHttpSchema.js.map +1 -1
- package/dist/src/server/GGHttpSchema.startServer.js +8 -4
- package/dist/src/server/GGHttpSchema.startServer.js.map +1 -1
- package/dist/src/server/GGHttpServer.js +2 -2
- package/dist/src/server/GGHttpServer.js.map +1 -1
- package/dist/src/server/GG_HTTP_REQUEST.d.ts +2 -2
- package/dist/testkit/clientHttp/GGHttpCall.d.ts.map +1 -1
- package/dist/testkit/clientHttp/GGHttpCall.js.map +1 -1
- package/dist/testkit/clientHttp/GGHttpSchema.callOn.d.ts +2 -2
- package/dist/testkit/clientHttp/GGHttpSchema.callOn.d.ts.map +1 -1
- package/dist/tsconfig.publish.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/_dedupCheck.ts +1 -1
- package/src/client/GGContextKeySynchronizer.ts +19 -1
- package/src/client/GGHttpSchema.createClient.ts +5 -3
- package/src/rpc/RpcResponse/GGRpcResponseBuilder.ts +2 -2
- package/src/schema/GGCookie.ts +3 -2
- package/src/schema/GGHttpPermissionsChecker.ts +3 -2
- package/src/schema/GGHttpSchema.ts +1 -1
- package/src/server/GGHttpSchema.startServer.ts +9 -6
- package/src/server/GGHttpServer.ts +2 -2
package/src/_dedupCheck.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
const __KEY = Symbol.for("grest-ts:dedup:@grest-ts/http");
|
|
13
13
|
const __slot = globalThis as Record<symbol, unknown>;
|
|
14
14
|
const __existing = __slot[__KEY] as { path: string; version: string } | undefined;
|
|
15
|
-
const __here = { path: import.meta.url, version: "0.0.
|
|
15
|
+
const __here = { path: import.meta.url, version: "0.0.55" };
|
|
16
16
|
// Same path + same version means the same physical package is being
|
|
17
17
|
// re-evaluated by a different ESM runner (e.g. vitest projects share one
|
|
18
18
|
// process but each project has its own module cache). That isn't a true
|
|
@@ -16,6 +16,24 @@ const GG_SYNC_CONTROLLERS = new GGContextKey<Map<string, ControllerEntry>>(
|
|
|
16
16
|
IsAny as unknown as GGSchema<Map<string, ControllerEntry>>
|
|
17
17
|
)
|
|
18
18
|
|
|
19
|
+
// Backstop so a stuck recover() can't hang every gated request forever. A recover() that
|
|
20
|
+
// never settles - e.g. a credential whose refresh endpoint is itself gated by the same wire,
|
|
21
|
+
// so the refresh call waits on the recover that is waiting on it (circular wait) - would
|
|
22
|
+
// otherwise block the outbound read with no request ever issued. Bounded well above a healthy
|
|
23
|
+
// recover (lock acquire 20s + request 15s) so only a truly stuck one trips; on timeout the
|
|
24
|
+
// waiter rejects and the caller fails/retries instead of freezing.
|
|
25
|
+
const RECOVER_TIMEOUT_MS = 45_000
|
|
26
|
+
|
|
27
|
+
function withRecoverTimeout(recover: Promise<void>, name: string): Promise<void> {
|
|
28
|
+
return new Promise<void>((resolve, reject) => {
|
|
29
|
+
const timer = setTimeout(
|
|
30
|
+
() => reject(new Error(`Credential recover for "${name}" timed out after ${RECOVER_TIMEOUT_MS}ms (stuck/circular refresh?)`)),
|
|
31
|
+
RECOVER_TIMEOUT_MS,
|
|
32
|
+
)
|
|
33
|
+
recover.then(resolve, reject).finally(() => clearTimeout(timer))
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
19
37
|
export class GGContextKeySynchronizer {
|
|
20
38
|
|
|
21
39
|
static provide(key: GGContextKey<any>, controller: GGKeyController): void {
|
|
@@ -30,7 +48,7 @@ export class GGContextKeySynchronizer {
|
|
|
30
48
|
static async waitFor(key: GGContextKey<any>): Promise<void> {
|
|
31
49
|
const entry = GG_SYNC_CONTROLLERS.get()?.get(key.name)
|
|
32
50
|
if (entry === undefined || !entry.controller.isStale()) return
|
|
33
|
-
entry.inflight ??= entry.controller.recover().finally(() => {
|
|
51
|
+
entry.inflight ??= withRecoverTimeout(entry.controller.recover(), key.name).finally(() => {
|
|
34
52
|
entry.inflight = undefined
|
|
35
53
|
})
|
|
36
54
|
await entry.inflight
|
|
@@ -67,6 +67,8 @@ export function createClient<TContract extends GGContractApiDefinition, TContext
|
|
|
67
67
|
config ??= {};
|
|
68
68
|
config.timeout ??= 15000;
|
|
69
69
|
|
|
70
|
+
const contract = httpSchema.contract;
|
|
71
|
+
|
|
70
72
|
// A custom transport implies "I take over the wire layer" — discovery is
|
|
71
73
|
// off the table (the transport knows how to find the target), and the
|
|
72
74
|
// default base URL becomes "" so the transport sees just the request path.
|
|
@@ -84,7 +86,7 @@ export function createClient<TContract extends GGContractApiDefinition, TContext
|
|
|
84
86
|
const transportImplementation: any = {}
|
|
85
87
|
for (const methodName of Object.keys(httpSchema.codec)) {
|
|
86
88
|
|
|
87
|
-
const contractFunction =
|
|
89
|
+
const contractFunction = contract.methods[methodName];
|
|
88
90
|
const noValidation = config?.noValidation === true;
|
|
89
91
|
|
|
90
92
|
const codec: GGHttpCodec = httpSchema.codec[methodName];
|
|
@@ -96,7 +98,7 @@ export function createClient<TContract extends GGContractApiDefinition, TContext
|
|
|
96
98
|
|
|
97
99
|
const implementation = async (data?: unknown): Promise<OK<unknown> | ANY_ERROR> => {
|
|
98
100
|
try {
|
|
99
|
-
let baseUrl: string = config.url;
|
|
101
|
+
let baseUrl: string | undefined = config.url;
|
|
100
102
|
if (baseUrl === undefined) {
|
|
101
103
|
try {
|
|
102
104
|
const {GG_DISCOVERY} = await import(/* @vite-ignore */ '@grest-ts/discovery');
|
|
@@ -149,7 +151,7 @@ export function createClient<TContract extends GGContractApiDefinition, TContext
|
|
|
149
151
|
|
|
150
152
|
}
|
|
151
153
|
// Per-client stub — doesn't belong in the callOn registry (server's impl does).
|
|
152
|
-
|
|
154
|
+
contract.implement(transportImplementation as GGContractImplementation<TContract>, {skipLocatorRegistration: true})
|
|
153
155
|
return transportImplementation;
|
|
154
156
|
}
|
|
155
157
|
|
|
@@ -47,7 +47,7 @@ export class GGRpcResponseBuilder {
|
|
|
47
47
|
"}";
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
private makeDataStr(schema: GGSchema<any
|
|
50
|
+
private makeDataStr(schema: GGSchema<any> | undefined, data: OK<any> | ANY_ERROR): string {
|
|
51
51
|
if (schema) {
|
|
52
52
|
GGContractExecutor.assertResponse(schema, data);
|
|
53
53
|
const dataStr = schema.unsafeStringify(data.data)
|
|
@@ -57,7 +57,7 @@ export class GGRpcResponseBuilder {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
private makeErrorCtx(ctx: GGErrorData, debugContext?: GGDebugData): string {
|
|
60
|
+
private makeErrorCtx(ctx: GGErrorData | undefined, debugContext?: GGDebugData): string {
|
|
61
61
|
if (ctx) {
|
|
62
62
|
let str = "";
|
|
63
63
|
str += ctx?.displayMessage ? '"displayMessage":' + JSON.stringify(ctx.displayMessage) + '' : "";
|
package/src/schema/GGCookie.ts
CHANGED
|
@@ -142,8 +142,9 @@ function _readCookie(rawCookieHeader: string | undefined, name: string): string
|
|
|
142
142
|
|
|
143
143
|
function _serializeSetCookie(name: string, value: string | undefined, options?: CookieOptions): string {
|
|
144
144
|
const o = {...SAFE_DEFAULTS, ...options}
|
|
145
|
-
const
|
|
146
|
-
const
|
|
145
|
+
const sameSiteValue = o.sameSite ?? "lax"
|
|
146
|
+
const secure = sameSiteValue === "none" ? true : o.secure
|
|
147
|
+
const sameSite = `${sameSiteValue.charAt(0).toUpperCase()}${sameSiteValue.slice(1)}`
|
|
147
148
|
const attrs = `; Path=${o.path}${o.domain ? `; Domain=${o.domain}` : ""}; SameSite=${sameSite}${secure ? "; Secure" : ""}${o.httpOnly ? "; HttpOnly" : ""}`
|
|
148
149
|
return value
|
|
149
150
|
? `${name}=${encodeURIComponent(value)}${o.maxAgeSec !== undefined ? `; Max-Age=${Math.trunc(o.maxAgeSec)}` : ""}${attrs}`
|
|
@@ -15,7 +15,7 @@ export class GGHttpPermissionsChecker {
|
|
|
15
15
|
Object.freeze(this.middlewaresWithPermissions)
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
public async assert(schema: string, method: string, required: GGPermission): Promise<ReadonlyArray<ReadonlyArray<string>>> {
|
|
18
|
+
public async assert(schema: string, method: string, required: GGPermission | undefined): Promise<ReadonlyArray<ReadonlyArray<string>>> {
|
|
19
19
|
const scopes: Array<readonly string[]> = [];
|
|
20
20
|
for (let i = 0; i < this.middlewaresWithPermissions.length; i++) {
|
|
21
21
|
scopes.push(await this.middlewaresWithPermissions[i].getGrantedPermissions())
|
|
@@ -24,7 +24,8 @@ export class GGHttpPermissionsChecker {
|
|
|
24
24
|
return Object.freeze(scopes) as readonly string[][]
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
public assertGrants(schema: string, method: string, granted: undefined | ReadonlyArray<ReadonlyArray<string>>, required: GGPermission): void {
|
|
27
|
+
public assertGrants(schema: string, method: string, granted: undefined | ReadonlyArray<ReadonlyArray<string>>, required: GGPermission | undefined): void {
|
|
28
|
+
if (required === undefined) return
|
|
28
29
|
if (!GGPermissionChecker.satisfies(required, granted)) {
|
|
29
30
|
throw new FORBIDDEN({
|
|
30
31
|
debugMessage: `${schema + (method ? "." + method : "")} requires ${GGPermissionChecker.describePermission(required)} - caller scopes did not satisfy`
|
|
@@ -10,7 +10,7 @@ export class GGHttpSchema<TContract extends GGContractApiDefinition, TContext> {
|
|
|
10
10
|
public readonly pathPrefix: string
|
|
11
11
|
public readonly apiMiddlewares: readonly GGTransportMiddleware[]
|
|
12
12
|
public readonly codec: Record<keyof TContract, GGHttpCodec>
|
|
13
|
-
public readonly contract: GGContractClass<TContract>
|
|
13
|
+
public readonly contract: GGContractClass<TContract>
|
|
14
14
|
|
|
15
15
|
constructor(
|
|
16
16
|
pathPrefix: string,
|
|
@@ -84,12 +84,14 @@ function setupRoutes<TContract extends GGContractApiDefinition>(
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
server.onStart(() => {
|
|
87
|
+
const port = server.port;
|
|
88
|
+
if (port === undefined) throw new Error("HTTP server port is not set; onStart ran before listen completed.");
|
|
87
89
|
GG_DISCOVERY.tryGet()?.registerRoutes([{
|
|
88
90
|
runtime: scope.serviceName,
|
|
89
91
|
api: httpSchema.name,
|
|
90
92
|
pathPrefix: pathPrefix,
|
|
91
93
|
protocol: "http",
|
|
92
|
-
port:
|
|
94
|
+
port: port
|
|
93
95
|
}]);
|
|
94
96
|
})
|
|
95
97
|
|
|
@@ -103,7 +105,8 @@ function setupRoutes<TContract extends GGContractApiDefinition>(
|
|
|
103
105
|
if (!codec) throw new Error(`Contract for "${httpSchema.name}.${methodName}" is missing wire format!`)
|
|
104
106
|
|
|
105
107
|
// Implementation function
|
|
106
|
-
const
|
|
108
|
+
const implMethod = implementation[methodName] as ((data?: unknown) => Promise<unknown>) | undefined
|
|
109
|
+
const implFn = implMethod?.bind(implementation)
|
|
107
110
|
if (!implFn) throw new Error(`Implementation for "${httpSchema.name}.${methodName}" is missing implementation method!`)
|
|
108
111
|
|
|
109
112
|
// Input schema
|
|
@@ -120,10 +123,10 @@ function setupRoutes<TContract extends GGContractApiDefinition>(
|
|
|
120
123
|
scope.ensureEntered();
|
|
121
124
|
return new GGContext("REQ", parentContext, true).run(async () => {
|
|
122
125
|
GG_TRACE.init();
|
|
123
|
-
GG_HTTP_REQUEST.set({port: server.port, method: req.method, path: req.url});
|
|
126
|
+
GG_HTTP_REQUEST.set({port: server.port ?? 0, method: req.method ?? "", path: req.url ?? ""});
|
|
124
127
|
GG_COOKIE_WRITES.set(cookieWriteNames);
|
|
125
128
|
const startTime = performance.now()
|
|
126
|
-
let rpcResult: ERROR<string, unknown> | OK<unknown>
|
|
129
|
+
let rpcResult: ERROR<string, unknown> | OK<unknown> | undefined
|
|
127
130
|
try {
|
|
128
131
|
const rawInput = await requestParser.readRequest(req)
|
|
129
132
|
await permissionsChecker.assert(httpSchema.name, methodName, contractFunctionSchema.permission)
|
|
@@ -150,7 +153,7 @@ function setupRoutes<TContract extends GGContractApiDefinition>(
|
|
|
150
153
|
}
|
|
151
154
|
|
|
152
155
|
function metrics(
|
|
153
|
-
resultType: string,
|
|
156
|
+
resultType: string | undefined,
|
|
154
157
|
apiName: string,
|
|
155
158
|
method: HttpMethod,
|
|
156
159
|
pathPrefix: string,
|
|
@@ -163,7 +166,7 @@ function metrics(
|
|
|
163
166
|
api: apiName,
|
|
164
167
|
method: pathSuffix,
|
|
165
168
|
path: path,
|
|
166
|
-
result: resultType
|
|
169
|
+
result: resultType ?? "unknown"
|
|
167
170
|
});
|
|
168
171
|
GGHttpMetrics.requestDuration.observe(performance.now() - startTime, {
|
|
169
172
|
api: apiName,
|
|
@@ -122,7 +122,7 @@ export class GGHttpServer {
|
|
|
122
122
|
res.writeHead(204);
|
|
123
123
|
res.end();
|
|
124
124
|
} else {
|
|
125
|
-
const resolved = this.router.find(req.method as HTTPMethod, req.url)
|
|
125
|
+
const resolved = this.router.find(req.method as HTTPMethod, req.url ?? "")
|
|
126
126
|
if (resolved) {
|
|
127
127
|
await (resolved.handler as unknown as GGHttpRequestCallback)(req, res);
|
|
128
128
|
} else {
|
|
@@ -131,7 +131,7 @@ export class GGHttpServer {
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
} catch (error) {
|
|
134
|
-
GGLog.error(this, error);
|
|
134
|
+
GGLog.error(this, error instanceof Error ? error : new Error(String(error)));
|
|
135
135
|
res.writeHead(500);
|
|
136
136
|
res.end();
|
|
137
137
|
} finally {
|