@fabriktor/client 0.0.22 → 0.0.24

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.
@@ -1,5 +1,6 @@
1
1
  import { HTTPMethodDelete, HTTPMethodGet, HTTPMethodPatch, HTTPMethodPost } from "@fabriktor/schema";
2
2
  import type { OperationResult } from "@fabriktor/schema";
3
+ import type { IdempotencyStore } from "./idempotency.js";
3
4
  export type Method = typeof HTTPMethodGet | typeof HTTPMethodPost | typeof HTTPMethodPatch | typeof HTTPMethodDelete;
4
5
  export type Op<T> = Omit<OperationResult, "value"> & {
5
6
  value?: T;
@@ -19,12 +20,14 @@ export interface HTTPDoer {
19
20
  }
20
21
  export type HTTPClientOptions = {
21
22
  fetcher: Fetcher;
23
+ idempotency: IdempotencyStore;
22
24
  };
23
25
  export declare class WebFetcher implements Fetcher {
24
26
  fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
25
27
  }
26
28
  export declare class HTTPClient implements HTTPDoer {
27
29
  private fetcher;
30
+ private idempotency;
28
31
  constructor(opts: HTTPClientOptions);
29
32
  do<T, R>(opts: HTTPOptions<T>): Promise<R>;
30
33
  }
@@ -5,6 +5,7 @@
5
5
  // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
6
  import { MimeJSON, } from "@fabriktor/schema";
7
7
  import { errFromHTTP, errHTTPRequestFailed, errHTTPResponseInvalid, } from "./err.js";
8
+ import { applyIdempotencyHeader, DefaultIdempotencyStore, } from "./idempotency.js";
8
9
  const headerAuthorization = "authorization";
9
10
  const headerContentType = "content-type";
10
11
  const prefixBearer = "Bearer";
@@ -18,21 +19,33 @@ function noContent(res) {
18
19
  }
19
20
  export class HTTPClient {
20
21
  fetcher;
22
+ idempotency;
21
23
  constructor(opts) {
22
24
  this.fetcher = opts.fetcher;
25
+ this.idempotency = opts.idempotency;
23
26
  }
24
27
  async do(opts) {
28
+ const h = headers(opts);
29
+ const idempotencyToken = applyIdempotencyHeader({
30
+ headers: h,
31
+ store: this.idempotency,
32
+ method: opts.method,
33
+ path: opts.path,
34
+ });
25
35
  let res;
26
36
  try {
27
37
  res = await this.fetcher.fetch(url(opts.base_url, opts.path), {
28
38
  method: opts.method,
29
- headers: headers(opts),
39
+ headers: h,
30
40
  body: body(opts.body),
31
41
  });
32
42
  }
33
43
  catch (err) {
34
44
  throw errHTTPRequestFailed(err);
35
45
  }
46
+ if (res.ok && idempotencyToken !== undefined) {
47
+ this.idempotency.reset(opts.method, opts.path);
48
+ }
36
49
  if (!res.ok) {
37
50
  return await fail(res);
38
51
  }
@@ -50,6 +63,7 @@ export class HTTPClient {
50
63
  export function newHTTPClient(opts) {
51
64
  return new HTTPClient({
52
65
  fetcher: opts?.fetcher ?? new WebFetcher(),
66
+ idempotency: opts?.idempotency ?? new DefaultIdempotencyStore(),
53
67
  });
54
68
  }
55
69
  function url(base, path) {
@@ -0,0 +1,17 @@
1
+ export declare const idempotentMethods: Set<string>;
2
+ export type IdempotencyStore = {
3
+ get(method: string, path: string): string;
4
+ reset(method: string, path: string): void;
5
+ };
6
+ export declare class DefaultIdempotencyStore implements IdempotencyStore {
7
+ private tokens;
8
+ get(method: string, path: string): string;
9
+ reset(method: string, path: string): void;
10
+ }
11
+ export declare function shouldUseIdempotency(method: string): boolean;
12
+ export declare function applyIdempotencyHeader(opts: {
13
+ headers: Headers;
14
+ store: IdempotencyStore;
15
+ method: string;
16
+ path: string;
17
+ }): string | undefined;
@@ -0,0 +1,40 @@
1
+ // Copyright (C) Fabriktor, Inc. 2025-present.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+ // not use this file except in compliance with the License. You may obtain
5
+ // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ import { HeaderIdempotency, HTTPMethodPost, HTTPMethodPatch, } from "@fabriktor/schema";
7
+ export const idempotentMethods = new Set([HTTPMethodPost, HTTPMethodPatch]);
8
+ function newToken() {
9
+ return crypto.randomUUID();
10
+ }
11
+ function key(method, path) {
12
+ return `${method.toUpperCase()} ${path}`;
13
+ }
14
+ export class DefaultIdempotencyStore {
15
+ tokens = new Map();
16
+ get(method, path) {
17
+ const k = key(method, path);
18
+ const current = this.tokens.get(k);
19
+ if (current) {
20
+ return current;
21
+ }
22
+ const next = newToken();
23
+ this.tokens.set(k, next);
24
+ return next;
25
+ }
26
+ reset(method, path) {
27
+ this.tokens.delete(key(method, path));
28
+ }
29
+ }
30
+ export function shouldUseIdempotency(method) {
31
+ return idempotentMethods.has(method.toUpperCase());
32
+ }
33
+ export function applyIdempotencyHeader(opts) {
34
+ if (!shouldUseIdempotency(opts.method)) {
35
+ return undefined;
36
+ }
37
+ const token = opts.store.get(opts.method, opts.path);
38
+ opts.headers.set(HeaderIdempotency, token);
39
+ return token;
40
+ }
@@ -3,6 +3,7 @@ export * from "./core/col.js";
3
3
  export * from "./core/crud.js";
4
4
  export * from "./core/err.js";
5
5
  export * from "./core/http.js";
6
+ export * from "./core/idempotency.js";
6
7
  export * from "./core/path.js";
7
8
  export * from "./preferences/preferences.js";
8
9
  export * from "./users/tmp_phones.js";
package/dist/src/index.js CHANGED
@@ -3,6 +3,7 @@ export * from "./core/col.js";
3
3
  export * from "./core/crud.js";
4
4
  export * from "./core/err.js";
5
5
  export * from "./core/http.js";
6
+ export * from "./core/idempotency.js";
6
7
  export * from "./core/path.js";
7
8
  export * from "./preferences/preferences.js";
8
9
  export * from "./users/tmp_phones.js";
@@ -1,4 +1,4 @@
1
- import type { InUser, OperationResult, PLUsersContactList, PLUsersForgotPassword, PLUsersResolveUsername, PLUsersSignInLink, UpsertResult, User } from "@fabriktor/schema";
1
+ import type { InUser, OperationResult, PLUsersContactList, PLUsersForgotPassword, PLUsersResolveUsername, PLUsersVerifyEmail, PLUsersSignInLink, UpsertResult, User } from "@fabriktor/schema";
2
2
  import type { TokenProvider } from "../core/auth.js";
3
3
  import type { HTTPDoer, Op } from "../core/http.js";
4
4
  export type BootstrapOptions = {
@@ -7,6 +7,7 @@ export type BootstrapOptions = {
7
7
  export type ResolveUsernameOptions = PLUsersResolveUsername;
8
8
  export type ForgotPasswordOptions = PLUsersForgotPassword;
9
9
  export type SendSignInLinkOptions = PLUsersSignInLink;
10
+ export type SendEmailVerificationOptions = PLUsersVerifyEmail;
10
11
  export type MatchOptions = PLUsersContactList;
11
12
  export type SearchOptions = {
12
13
  q: string;
@@ -18,7 +19,7 @@ export type GetByUIDOptions = {
18
19
  export interface UsersOperator {
19
20
  bootstrap(opts: BootstrapOptions): Promise<Op<UpsertResult>>;
20
21
  sync(): Promise<OperationResult>;
21
- sendEmailVerification(): Promise<void>;
22
+ sendEmailVerification(opts: SendEmailVerificationOptions): Promise<void>;
22
23
  forgotPassword(opts: ForgotPasswordOptions): Promise<void>;
23
24
  sendSignInLink(opts: SendSignInLinkOptions): Promise<void>;
24
25
  resolveUsername(opts: ResolveUsernameOptions): Promise<Op<string>>;
@@ -38,7 +39,7 @@ export declare class UsersClient implements UsersOperator {
38
39
  constructor(opts: UsersClientOptions);
39
40
  bootstrap(opts: BootstrapOptions): Promise<Op<UpsertResult>>;
40
41
  sync(): Promise<OperationResult>;
41
- sendEmailVerification(): Promise<void>;
42
+ sendEmailVerification(opts: SendEmailVerificationOptions): Promise<void>;
42
43
  forgotPassword(opts: ForgotPasswordOptions): Promise<void>;
43
44
  sendSignInLink(opts: SendSignInLinkOptions): Promise<void>;
44
45
  resolveUsername(opts: ResolveUsernameOptions): Promise<Op<string>>;
@@ -34,13 +34,16 @@ export class UsersClient {
34
34
  token,
35
35
  });
36
36
  }
37
- async sendEmailVerification() {
37
+ async sendEmailVerification(opts) {
38
38
  const token = await requiredToken(this.get_token);
39
39
  return await this.http.do({
40
40
  base_url: this.base_url,
41
41
  path: userPath(PathUsersSendEmailVerification),
42
42
  method: HTTPMethodPost,
43
43
  token,
44
+ body: {
45
+ language: opts.language,
46
+ },
44
47
  });
45
48
  }
46
49
  async forgotPassword(opts) {
@@ -63,34 +66,15 @@ export class UsersClient {
63
66
  },
64
67
  });
65
68
  }
66
- // public async resolveUsername(
67
- // opts: ResolveUsernameOptions,
68
- // ): Promise<Op<string>> {
69
- // return await this.http.do<PLUsersResolveUsername, Op<string>>({
70
- // base_url: this.base_url,
71
- // path: userPath(PathUsersResolveUsername),
72
- // method: HTTPMethodPost,
73
- // body: {
74
- // username: opts.username,
75
- // },
76
- // });
77
- // }
78
69
  async resolveUsername(opts) {
79
- const body = {
80
- username: opts.username,
81
- };
82
- console.log("[client/users.resolveUsername] request", {
83
- path: userPath(PathUsersResolveUsername),
84
- body,
85
- });
86
- const out = await this.http.do({
70
+ return await this.http.do({
87
71
  base_url: this.base_url,
88
72
  path: userPath(PathUsersResolveUsername),
89
73
  method: HTTPMethodPost,
90
- body,
74
+ body: {
75
+ username: opts.username,
76
+ },
91
77
  });
92
- console.log("[client/users.resolveUsername] response", out);
93
- return out;
94
78
  }
95
79
  async match(opts) {
96
80
  const token = await requiredToken(this.get_token);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabriktor/client",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
4
  "description": "![Fabriktor Logo](./img/fabriktor-character.gif)",
5
5
  "type": "module",
6
6
  "exports": {
@@ -17,6 +17,6 @@
17
17
  "typescript": "^6.0.3"
18
18
  },
19
19
  "dependencies": {
20
- "@fabriktor/schema": "^0.0.15"
20
+ "@fabriktor/schema": "^0.0.18"
21
21
  }
22
22
  }