@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.
Files changed (38) hide show
  1. package/dist/src/_dedupCheck.js +1 -1
  2. package/dist/src/client/GGContextKeySynchronizer.d.ts.map +1 -1
  3. package/dist/src/client/GGContextKeySynchronizer.js +14 -1
  4. package/dist/src/client/GGContextKeySynchronizer.js.map +1 -1
  5. package/dist/src/client/GGHttpSchema.createClient.d.ts.map +1 -1
  6. package/dist/src/client/GGHttpSchema.createClient.js +3 -2
  7. package/dist/src/client/GGHttpSchema.createClient.js.map +1 -1
  8. package/dist/src/rpc/RpcResponse/GGRpcResponseBuilder.js.map +1 -1
  9. package/dist/src/schema/GGCookie.js +3 -2
  10. package/dist/src/schema/GGCookie.js.map +1 -1
  11. package/dist/src/schema/GGHttpPermissionsChecker.d.ts +2 -2
  12. package/dist/src/schema/GGHttpPermissionsChecker.d.ts.map +1 -1
  13. package/dist/src/schema/GGHttpPermissionsChecker.js +2 -0
  14. package/dist/src/schema/GGHttpPermissionsChecker.js.map +1 -1
  15. package/dist/src/schema/GGHttpSchema.d.ts +1 -1
  16. package/dist/src/schema/GGHttpSchema.d.ts.map +1 -1
  17. package/dist/src/schema/GGHttpSchema.js +1 -1
  18. package/dist/src/schema/GGHttpSchema.js.map +1 -1
  19. package/dist/src/server/GGHttpSchema.startServer.js +8 -4
  20. package/dist/src/server/GGHttpSchema.startServer.js.map +1 -1
  21. package/dist/src/server/GGHttpServer.js +2 -2
  22. package/dist/src/server/GGHttpServer.js.map +1 -1
  23. package/dist/src/server/GG_HTTP_REQUEST.d.ts +2 -2
  24. package/dist/testkit/clientHttp/GGHttpCall.d.ts.map +1 -1
  25. package/dist/testkit/clientHttp/GGHttpCall.js.map +1 -1
  26. package/dist/testkit/clientHttp/GGHttpSchema.callOn.d.ts +2 -2
  27. package/dist/testkit/clientHttp/GGHttpSchema.callOn.d.ts.map +1 -1
  28. package/dist/tsconfig.publish.tsbuildinfo +1 -1
  29. package/package.json +10 -10
  30. package/src/_dedupCheck.ts +1 -1
  31. package/src/client/GGContextKeySynchronizer.ts +19 -1
  32. package/src/client/GGHttpSchema.createClient.ts +5 -3
  33. package/src/rpc/RpcResponse/GGRpcResponseBuilder.ts +2 -2
  34. package/src/schema/GGCookie.ts +3 -2
  35. package/src/schema/GGHttpPermissionsChecker.ts +3 -2
  36. package/src/schema/GGHttpSchema.ts +1 -1
  37. package/src/server/GGHttpSchema.startServer.ts +9 -6
  38. package/src/server/GGHttpServer.ts +2 -2
@@ -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.51" };
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 = httpSchema.contract.methods[methodName];
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
- httpSchema.contract.implement(transportImplementation as GGContractImplementation<TContract>, {skipLocatorRegistration: true})
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>, data: OK<any> | ANY_ERROR): string {
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) + '' : "";
@@ -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 secure = o.sameSite === "none" ? true : o.secure
146
- const sameSite = `${o.sameSite.charAt(0).toUpperCase()}${o.sameSite.slice(1)}`
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> | null = null
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: server.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 implFn = implementation[methodName]?.bind(implementation)
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 {