@tearleads/client-sdk 0.1.69 → 0.1.71

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 (88) hide show
  1. package/internal/api-client/ApiClient.d.ts +73 -3
  2. package/internal/api-client/apiRequestMetrics.d.ts +18 -4
  3. package/internal/api-client/apiRequestMetrics.js +55 -5
  4. package/internal/api-client/apiRequestMetrics.js.map +1 -1
  5. package/internal/api-client/durationSampleWindow.d.ts +26 -0
  6. package/internal/api-client/durationSampleWindow.js +42 -0
  7. package/internal/api-client/durationSampleWindow.js.map +1 -0
  8. package/internal/api-client/measureApiRequest.js +9 -4
  9. package/internal/api-client/measureApiRequest.js.map +1 -1
  10. package/internal/api-client/serverTiming.d.ts +12 -0
  11. package/internal/api-client/serverTiming.js +49 -0
  12. package/internal/api-client/serverTiming.js.map +1 -0
  13. package/internal/validators/documentCreateSeedRefinements.js +9 -0
  14. package/internal/validators/documentCreateSeedRefinements.js.map +1 -0
  15. package/internal/validators/operation/auth.d.ts +120 -0
  16. package/internal/validators/operation/containerMutations.d.ts +355 -0
  17. package/internal/validators/operation/containerMutations.js +7 -1
  18. package/internal/validators/operation/containerMutations.js.map +1 -1
  19. package/internal/validators/operation/documentMutations.d.ts +356 -0
  20. package/internal/validators/operation/documentMutations.js +6 -1
  21. package/internal/validators/operation/documentMutations.js.map +1 -1
  22. package/internal/validators/operation/documentSync.d.ts +9 -9
  23. package/internal/validators/operation/organizations.d.ts +120 -0
  24. package/internal/validators/request/containerMetadata.d.ts +313 -0
  25. package/internal/validators/request/containerMetadata.js +20 -3
  26. package/internal/validators/request/containerMetadata.js.map +1 -1
  27. package/internal/validators/request/document.d.ts +571 -0
  28. package/internal/validators/request/document.js +19 -1
  29. package/internal/validators/request/document.js.map +1 -1
  30. package/internal/validators/request/documentCreateSeed.js +35 -0
  31. package/internal/validators/request/documentCreateSeed.js.map +1 -0
  32. package/internal/validators/request/organizationProvisioning.js +9 -22
  33. package/internal/validators/request/organizationProvisioning.js.map +1 -1
  34. package/internal/validators/response/containerMetadata.d.ts +20 -0
  35. package/internal/validators/response/documentMutation.d.ts +43 -0
  36. package/internal/validators/response/documentMutation.js +12 -1
  37. package/internal/validators/response/documentMutation.js.map +1 -1
  38. package/internal/validators/response/documentSyncSchema.d.ts +13 -9
  39. package/internal/validators/response/documentSyncSchema.js +23 -18
  40. package/internal/validators/response/documentSyncSchema.js.map +1 -1
  41. package/internal/validators/response/index.d.ts +1 -1
  42. package/internal/validators/response/index.js.map +1 -1
  43. package/internal/validators/response/organizationProvisioning.d.ts +350 -0
  44. package/internal/validators/response/registration.d.ts +120 -0
  45. package/package.json +1 -1
  46. package/stores/container-contents/containerState/containerStatePersistence.js +2 -0
  47. package/stores/container-contents/containerState/containerStatePersistence.js.map +1 -1
  48. package/stores/container-contents/remoteEvents/remoteEventSync.js +1 -1
  49. package/stores/container-contents/remoteEvents/remoteEventSync.js.map +1 -1
  50. package/stores/container-contents/state.js +2 -0
  51. package/stores/container-contents/state.js.map +1 -1
  52. package/stores/container-contents/syncAgent/syncAgentTypes.d.ts +5 -0
  53. package/stores/container-contents/syncAgent/syncAgentTypes.js.map +1 -1
  54. package/stores/documents/documentStore/sync/createdDocumentPersistence.js +71 -0
  55. package/stores/documents/documentStore/sync/createdDocumentPersistence.js.map +1 -0
  56. package/stores/documents/documentStore/sync/index.js +13 -2
  57. package/stores/documents/documentStore/sync/index.js.map +1 -1
  58. package/stores/documents/documentStore/sync/syncRemoteSignals.js +21 -3
  59. package/stores/documents/documentStore/sync/syncRemoteSignals.js.map +1 -1
  60. package/stores/documents/documentStore/sync/syncSeededCreate.js +50 -0
  61. package/stores/documents/documentStore/sync/syncSeededCreate.js.map +1 -0
  62. package/stores/documents/documentStore/sync/syncShared.js +16 -8
  63. package/stores/documents/documentStore/sync/syncShared.js.map +1 -1
  64. package/workflows/container-contents/container-state/create/createIntentSeed.js +60 -0
  65. package/workflows/container-contents/container-state/create/createIntentSeed.js.map +1 -0
  66. package/workflows/container-contents/container-state/create/createIntentSync.js +18 -19
  67. package/workflows/container-contents/container-state/create/createIntentSync.js.map +1 -1
  68. package/workflows/container-contents/container-state/create/createWithMetadata.js +43 -6
  69. package/workflows/container-contents/container-state/create/createWithMetadata.js.map +1 -1
  70. package/workflows/container-contents/container-state/create/createWithMetadataPlan.js +5 -1
  71. package/workflows/container-contents/container-state/create/createWithMetadataPlan.js.map +1 -1
  72. package/workflows/container-contents/container-state/remote.js +15 -0
  73. package/workflows/container-contents/container-state/remote.js.map +1 -1
  74. package/workflows/container-contents/container-state/types.js.map +1 -1
  75. package/workflows/container-contents/metadata/metadataEvents.d.ts +7 -1
  76. package/workflows/container-contents/metadata/metadataEvents.js +8 -2
  77. package/workflows/container-contents/metadata/metadataEvents.js.map +1 -1
  78. package/workflows/container-contents/remoteHydration/types.d.ts +3 -0
  79. package/workflows/container-contents/remoteHydration/types.js.map +1 -1
  80. package/workflows/documents/create/index.d.ts +7 -0
  81. package/workflows/documents/create/index.js +40 -2
  82. package/workflows/documents/create/index.js.map +1 -1
  83. package/workflows/documents/index.d.ts +1 -0
  84. package/workflows/documents/index.js +1 -0
  85. package/workflows/documents/index.js.map +1 -1
  86. package/workflows/documents/sync/initialSync.d.ts +39 -0
  87. package/workflows/documents/sync/initialSync.js +39 -6
  88. package/workflows/documents/sync/initialSync.js.map +1 -1
@@ -173,6 +173,11 @@ export declare class ApiClient {
173
173
  targets: Record<string, unknown>[];
174
174
  };
175
175
  id: string;
176
+ initialSync?: {
177
+ commitLsn: string | null;
178
+ commitLsnMode: "tracked" | "untracked";
179
+ acceptedOutgoingUpdateIds: string[];
180
+ } | undefined;
176
181
  };
177
182
  };
178
183
  organizationMetadataContainerId: string;
@@ -214,6 +219,11 @@ export declare class ApiClient {
214
219
  targets: Record<string, unknown>[];
215
220
  };
216
221
  id: string;
222
+ initialSync?: {
223
+ commitLsn: string | null;
224
+ commitLsnMode: "tracked" | "untracked";
225
+ acceptedOutgoingUpdateIds: string[];
226
+ } | undefined;
217
227
  };
218
228
  rootMetadataDocumentId: string;
219
229
  systemContainers: {
@@ -312,6 +322,11 @@ export declare class ApiClient {
312
322
  targets: Record<string, unknown>[];
313
323
  };
314
324
  id: string;
325
+ initialSync?: {
326
+ commitLsn: string | null;
327
+ commitLsnMode: "tracked" | "untracked";
328
+ acceptedOutgoingUpdateIds: string[];
329
+ } | undefined;
315
330
  };
316
331
  }[];
317
332
  userId: string;
@@ -350,6 +365,11 @@ export declare class ApiClient {
350
365
  targets: Record<string, unknown>[];
351
366
  };
352
367
  id: string;
368
+ initialSync?: {
369
+ commitLsn: string | null;
370
+ commitLsnMode: "tracked" | "untracked";
371
+ acceptedOutgoingUpdateIds: string[];
372
+ } | undefined;
353
373
  } | undefined;
354
374
  organizationProfileDocumentId?: string | undefined;
355
375
  rosterProfileContainer?: {
@@ -448,6 +468,11 @@ export declare class ApiClient {
448
468
  targets: Record<string, unknown>[];
449
469
  };
450
470
  id: string;
471
+ initialSync?: {
472
+ commitLsn: string | null;
473
+ commitLsnMode: "tracked" | "untracked";
474
+ acceptedOutgoingUpdateIds: string[];
475
+ } | undefined;
451
476
  };
452
477
  } | undefined;
453
478
  rosterProfileContainerId?: string | undefined;
@@ -486,6 +511,11 @@ export declare class ApiClient {
486
511
  targets: Record<string, unknown>[];
487
512
  };
488
513
  id: string;
514
+ initialSync?: {
515
+ commitLsn: string | null;
516
+ commitLsnMode: "tracked" | "untracked";
517
+ acceptedOutgoingUpdateIds: string[];
518
+ } | undefined;
489
519
  } | undefined;
490
520
  rosterProfileDocumentId?: string | undefined;
491
521
  } | null>;
@@ -603,6 +633,11 @@ export declare class ApiClient {
603
633
  targets: Record<string, unknown>[];
604
634
  };
605
635
  id: string;
636
+ initialSync?: {
637
+ commitLsn: string | null;
638
+ commitLsnMode: "tracked" | "untracked";
639
+ acceptedOutgoingUpdateIds: string[];
640
+ } | undefined;
606
641
  };
607
642
  };
608
643
  organizationMetadataContainerId: string;
@@ -644,6 +679,11 @@ export declare class ApiClient {
644
679
  targets: Record<string, unknown>[];
645
680
  };
646
681
  id: string;
682
+ initialSync?: {
683
+ commitLsn: string | null;
684
+ commitLsnMode: "tracked" | "untracked";
685
+ acceptedOutgoingUpdateIds: string[];
686
+ } | undefined;
647
687
  };
648
688
  rootMetadataDocumentId: string;
649
689
  systemContainers: {
@@ -742,6 +782,11 @@ export declare class ApiClient {
742
782
  targets: Record<string, unknown>[];
743
783
  };
744
784
  id: string;
785
+ initialSync?: {
786
+ commitLsn: string | null;
787
+ commitLsnMode: "tracked" | "untracked";
788
+ acceptedOutgoingUpdateIds: string[];
789
+ } | undefined;
745
790
  };
746
791
  }[];
747
792
  userId: string;
@@ -780,6 +825,11 @@ export declare class ApiClient {
780
825
  targets: Record<string, unknown>[];
781
826
  };
782
827
  id: string;
828
+ initialSync?: {
829
+ commitLsn: string | null;
830
+ commitLsnMode: "tracked" | "untracked";
831
+ acceptedOutgoingUpdateIds: string[];
832
+ } | undefined;
783
833
  } | undefined;
784
834
  organizationProfileDocumentId?: string | undefined;
785
835
  rosterProfileContainer?: {
@@ -878,6 +928,11 @@ export declare class ApiClient {
878
928
  targets: Record<string, unknown>[];
879
929
  };
880
930
  id: string;
931
+ initialSync?: {
932
+ commitLsn: string | null;
933
+ commitLsnMode: "tracked" | "untracked";
934
+ acceptedOutgoingUpdateIds: string[];
935
+ } | undefined;
881
936
  };
882
937
  } | undefined;
883
938
  rosterProfileContainerId?: string | undefined;
@@ -916,6 +971,11 @@ export declare class ApiClient {
916
971
  targets: Record<string, unknown>[];
917
972
  };
918
973
  id: string;
974
+ initialSync?: {
975
+ commitLsn: string | null;
976
+ commitLsnMode: "tracked" | "untracked";
977
+ acceptedOutgoingUpdateIds: string[];
978
+ } | undefined;
919
979
  } | undefined;
920
980
  rosterProfileDocumentId?: string | undefined;
921
981
  } | null>;
@@ -2608,6 +2668,11 @@ export declare class ApiClient {
2608
2668
  targets: Record<string, unknown>[];
2609
2669
  };
2610
2670
  id: string;
2671
+ initialSync?: {
2672
+ commitLsn: string | null;
2673
+ commitLsnMode: "tracked" | "untracked";
2674
+ acceptedOutgoingUpdateIds: string[];
2675
+ } | undefined;
2611
2676
  } | null>;
2612
2677
  createDocumentResult(input: DocumentCreateRequest, options?: RequestResultOptions): Promise<RequestResult<DocumentCreateResponse>>;
2613
2678
  getContainerWriterProjection(containerId: string): Promise<{
@@ -2900,6 +2965,11 @@ export declare class ApiClient {
2900
2965
  targets: Record<string, unknown>[];
2901
2966
  };
2902
2967
  id: string;
2968
+ initialSync?: {
2969
+ commitLsn: string | null;
2970
+ commitLsnMode: "tracked" | "untracked";
2971
+ acceptedOutgoingUpdateIds: string[];
2972
+ } | undefined;
2903
2973
  };
2904
2974
  } | null>;
2905
2975
  createContainerWithMetadataDocumentResult(input: ContainerCreateWithMetadataDocumentRequest, options?: RequestResultOptions): Promise<RequestResult<ContainerCreateWithMetadataDocumentResponse>>;
@@ -3970,9 +4040,6 @@ export declare class ApiClient {
3970
4040
  };
3971
4041
  } | null>;
3972
4042
  syncDocument(documentId: string, input: DocumentSyncRequest, options?: RequestResultOptions): Promise<{
3973
- acceptedOutgoingUpdateIds: string[];
3974
- commitLsn: string | null;
3975
- commitLsnMode: "tracked" | "untracked";
3976
4043
  contentKeyBundle: {
3977
4044
  contentKeyEpoch: number;
3978
4045
  documentId: string;
@@ -4035,6 +4102,9 @@ export declare class ApiClient {
4035
4102
  checkpointPayloadKind?: "full_history_snapshot" | undefined;
4036
4103
  sourceVersionVector?: string | undefined;
4037
4104
  }[];
4105
+ commitLsn: string | null;
4106
+ commitLsnMode: "tracked" | "untracked";
4107
+ acceptedOutgoingUpdateIds: string[];
4038
4108
  } | null>;
4039
4109
  syncDocumentResult(documentId: string, input: DocumentSyncRequest, options?: RequestResultOptions): Promise<RequestResult<DocumentSyncResponse>>;
4040
4110
  initiateMultipartBlobStage(input: InitiateMultipartBlobStageRequest): Promise<{
@@ -1,4 +1,6 @@
1
1
  import type { HttpOperation } from "../validators/operation/index.js";
2
+ import { type ApiRequestDurationSummary } from "./durationSampleWindow.js";
3
+ import type { ApiServerTiming } from "./serverTiming.js";
2
4
  export interface ApiRequestCounts {
3
5
  readonly total: number;
4
6
  readonly inFlight: number;
@@ -9,6 +11,12 @@ export interface ApiRequestCounts {
9
11
  export interface ApiRequestEndpointCounts extends ApiRequestCounts {
10
12
  readonly method: string;
11
13
  readonly path: string;
14
+ /** Dispatch until response headers, a connection failure, or cancellation. */
15
+ readonly duration: ApiRequestDurationSummary | null;
16
+ /** The API's Server-Timing `total`; null until a response reports it. */
17
+ readonly serverDuration: ApiRequestDurationSummary | null;
18
+ /** The API's Server-Timing `db`; null until a response reports it. */
19
+ readonly databaseDuration: ApiRequestDurationSummary | null;
12
20
  }
13
21
  export interface ApiRequestMetricsSnapshot extends ApiRequestCounts {
14
22
  readonly startedAt: string;
@@ -20,18 +28,24 @@ export interface ApiRequestInstrumentation {
20
28
  }
21
29
  type Outcome = "succeeded" | "failed" | "cancelled";
22
30
  /**
23
- * In-memory HTTP attempt counts, partitioned by identity at dispatch time.
24
- * Only operation templates are retained: no URLs, queries, bodies or tokens.
25
- * Hosts may reuse this store across client lifetimes until a page reload.
31
+ * In-memory HTTP attempt counts and durations, partitioned by identity at
32
+ * dispatch time. Only operation templates are retained: no URLs, queries,
33
+ * bodies or tokens. Hosts may reuse this store across client lifetimes until a
34
+ * page reload.
26
35
  */
27
36
  export declare class ApiRequestMetrics {
37
+ private readonly now;
28
38
  private readonly scopes;
39
+ private readonly durations;
29
40
  private readonly listeners;
30
41
  private readonly generations;
42
+ /** `now` must be monotonic; tests inject a fake clock. */
43
+ constructor(now?: () => number);
31
44
  getSnapshot: (identity: string | null) => ApiRequestMetricsSnapshot;
32
45
  subscribe: (listener: () => void) => (() => void);
33
46
  reset(identity: string | null): void;
34
- begin(identity: string | null, operation: HttpOperation): (outcome: Outcome) => void;
47
+ begin(identity: string | null, operation: HttpOperation): (outcome: Outcome, serverTiming?: ApiServerTiming | null) => void;
48
+ private recordDurations;
35
49
  private update;
36
50
  private notify;
37
51
  }
@@ -1,3 +1,4 @@
1
+ import { DurationSampleWindow, } from "./durationSampleWindow.js";
1
2
  const emptyCounts = {
2
3
  total: 0,
3
4
  inFlight: 0,
@@ -5,18 +6,33 @@ const emptyCounts = {
5
6
  failed: 0,
6
7
  cancelled: 0,
7
8
  };
9
+ const emptyDurations = {
10
+ duration: null,
11
+ serverDuration: null,
12
+ databaseDuration: null,
13
+ };
8
14
  function emptySnapshot() {
9
15
  return { ...emptyCounts, startedAt: new Date().toISOString(), endpoints: [] };
10
16
  }
17
+ function endpointKey(row) {
18
+ return `${row.method} ${row.path}`;
19
+ }
11
20
  /**
12
- * In-memory HTTP attempt counts, partitioned by identity at dispatch time.
13
- * Only operation templates are retained: no URLs, queries, bodies or tokens.
14
- * Hosts may reuse this store across client lifetimes until a page reload.
21
+ * In-memory HTTP attempt counts and durations, partitioned by identity at
22
+ * dispatch time. Only operation templates are retained: no URLs, queries,
23
+ * bodies or tokens. Hosts may reuse this store across client lifetimes until a
24
+ * page reload.
15
25
  */
16
26
  export class ApiRequestMetrics {
27
+ now;
17
28
  scopes = new Map();
29
+ durations = new Map();
18
30
  listeners = new Set();
19
31
  generations = new Map();
32
+ /** `now` must be monotonic; tests inject a fake clock. */
33
+ constructor(now = () => performance.now()) {
34
+ this.now = now;
35
+ }
20
36
  getSnapshot = (identity) => {
21
37
  let snapshot = this.scopes.get(identity);
22
38
  if (!snapshot) {
@@ -32,13 +48,16 @@ export class ApiRequestMetrics {
32
48
  reset(identity) {
33
49
  this.generations.set(identity, (this.generations.get(identity) ?? 0) + 1);
34
50
  this.scopes.set(identity, emptySnapshot());
51
+ this.durations.delete(identity);
35
52
  this.notify();
36
53
  }
37
54
  begin(identity, operation) {
38
55
  const generation = this.generations.get(identity) ?? 0;
56
+ const startedAt = this.now();
39
57
  const previous = this.getSnapshot(identity);
40
58
  const existing = previous.endpoints.find((row) => row.method === operation.method && row.path === operation.path);
41
59
  let row = {
60
+ ...emptyDurations,
42
61
  ...(existing ?? emptyCounts),
43
62
  method: operation.method,
44
63
  path: operation.path,
@@ -52,7 +71,7 @@ export class ApiRequestMetrics {
52
71
  // Reset replaces the scope's endpoints. Capture a generation independently
53
72
  // of snapshots, which are replaced for every concurrent request update.
54
73
  let completed = false;
55
- return (outcome) => {
74
+ return (outcome, serverTiming) => {
56
75
  if (completed)
57
76
  return;
58
77
  completed = true;
@@ -64,6 +83,7 @@ export class ApiRequestMetrics {
64
83
  return;
65
84
  row = {
66
85
  ...endpoint,
86
+ ...this.recordDurations(identity, endpoint, this.now() - startedAt, serverTiming),
67
87
  inFlight: endpoint.inFlight - 1,
68
88
  [outcome]: endpoint[outcome] + 1,
69
89
  };
@@ -73,12 +93,42 @@ export class ApiRequestMetrics {
73
93
  });
74
94
  };
75
95
  }
96
+ recordDurations(identity, endpoint, elapsedMs, serverTiming) {
97
+ let byEndpoint = this.durations.get(identity);
98
+ if (!byEndpoint) {
99
+ byEndpoint = new Map();
100
+ this.durations.set(identity, byEndpoint);
101
+ }
102
+ const key = endpointKey(endpoint);
103
+ let windows = byEndpoint.get(key);
104
+ if (!windows) {
105
+ windows = {
106
+ duration: new DurationSampleWindow(),
107
+ serverDuration: new DurationSampleWindow(),
108
+ databaseDuration: new DurationSampleWindow(),
109
+ };
110
+ byEndpoint.set(key, windows);
111
+ }
112
+ windows.duration.record(elapsedMs);
113
+ const serverMs = serverTiming?.totalMs;
114
+ if (typeof serverMs === "number")
115
+ windows.serverDuration.record(serverMs);
116
+ const databaseMs = serverTiming?.databaseMs;
117
+ if (typeof databaseMs === "number") {
118
+ windows.databaseDuration.record(databaseMs);
119
+ }
120
+ return {
121
+ duration: windows.duration.summarize(),
122
+ serverDuration: windows.serverDuration.summarize(),
123
+ databaseDuration: windows.databaseDuration.summarize(),
124
+ };
125
+ }
76
126
  update(identity, previous, existing, row, counts) {
77
127
  const endpoints = existing
78
128
  ? previous.endpoints.map((candidate) => candidate === existing ? row : candidate)
79
129
  : [...previous.endpoints, row];
80
130
  endpoints.sort((left, right) => right.total - left.total ||
81
- `${left.method} ${left.path}`.localeCompare(`${right.method} ${right.path}`, "en"));
131
+ endpointKey(left).localeCompare(endpointKey(right), "en"));
82
132
  this.scopes.set(identity, { ...previous, ...counts, endpoints });
83
133
  this.notify();
84
134
  }
@@ -1 +1 @@
1
- {"version":3,"file":"apiRequestMetrics.js","sourceRoot":"","sources":["apiRequestMetrics.ts"],"names":[],"mappings":"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","sourcesContent":["import type { HttpOperation } from \"@tearleads/validators/operation\";\n\nexport interface ApiRequestCounts {\n readonly total: number;\n readonly inFlight: number;\n readonly succeeded: number;\n readonly failed: number;\n readonly cancelled: number;\n}\n\nexport interface ApiRequestEndpointCounts extends ApiRequestCounts {\n readonly method: string;\n readonly path: string;\n}\n\nexport interface ApiRequestMetricsSnapshot extends ApiRequestCounts {\n readonly startedAt: string;\n readonly endpoints: readonly ApiRequestEndpointCounts[];\n}\n\nexport interface ApiRequestInstrumentation {\n readonly metrics: ApiRequestMetrics;\n readonly getIdentity: () => string | null;\n}\n\ntype Outcome = \"succeeded\" | \"failed\" | \"cancelled\";\n\nconst emptyCounts: ApiRequestCounts = {\n total: 0,\n inFlight: 0,\n succeeded: 0,\n failed: 0,\n cancelled: 0,\n};\n\nfunction emptySnapshot(): ApiRequestMetricsSnapshot {\n return { ...emptyCounts, startedAt: new Date().toISOString(), endpoints: [] };\n}\n\n/**\n * In-memory HTTP attempt counts, partitioned by identity at dispatch time.\n * Only operation templates are retained: no URLs, queries, bodies or tokens.\n * Hosts may reuse this store across client lifetimes until a page reload.\n */\nexport class ApiRequestMetrics {\n private readonly scopes = new Map<string | null, ApiRequestMetricsSnapshot>();\n private readonly listeners = new Set<() => void>();\n private readonly generations = new Map<string | null, number>();\n\n getSnapshot = (identity: string | null): ApiRequestMetricsSnapshot => {\n let snapshot = this.scopes.get(identity);\n if (!snapshot) {\n snapshot = emptySnapshot();\n this.scopes.set(identity, snapshot);\n }\n return snapshot;\n };\n\n subscribe = (listener: () => void): (() => void) => {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n };\n\n reset(identity: string | null): void {\n this.generations.set(identity, (this.generations.get(identity) ?? 0) + 1);\n this.scopes.set(identity, emptySnapshot());\n this.notify();\n }\n\n begin(\n identity: string | null,\n operation: HttpOperation,\n ): (outcome: Outcome) => void {\n const generation = this.generations.get(identity) ?? 0;\n const previous = this.getSnapshot(identity);\n const existing = previous.endpoints.find(\n (row) => row.method === operation.method && row.path === operation.path,\n );\n let row: ApiRequestEndpointCounts = {\n ...(existing ?? emptyCounts),\n method: operation.method,\n path: operation.path,\n total: (existing?.total ?? 0) + 1,\n inFlight: (existing?.inFlight ?? 0) + 1,\n };\n this.update(identity, previous, existing, row, {\n total: previous.total + 1,\n inFlight: previous.inFlight + 1,\n });\n // Reset replaces the scope's endpoints. Capture a generation independently\n // of snapshots, which are replaced for every concurrent request update.\n let completed = false;\n return (outcome) => {\n if (completed) return;\n completed = true;\n if ((this.generations.get(identity) ?? 0) !== generation) return;\n const current = this.getSnapshot(identity);\n const endpoint = current.endpoints.find(\n (candidate) =>\n candidate.method === row.method && candidate.path === row.path,\n );\n if (!endpoint) return;\n row = {\n ...endpoint,\n inFlight: endpoint.inFlight - 1,\n [outcome]: endpoint[outcome] + 1,\n };\n this.update(identity, current, endpoint, row, {\n inFlight: current.inFlight - 1,\n [outcome]: current[outcome] + 1,\n });\n };\n }\n\n private update(\n identity: string | null,\n previous: ApiRequestMetricsSnapshot,\n existing: ApiRequestEndpointCounts | undefined,\n row: ApiRequestEndpointCounts,\n counts: Partial<ApiRequestCounts>,\n ): void {\n const endpoints = existing\n ? previous.endpoints.map((candidate) =>\n candidate === existing ? row : candidate,\n )\n : [...previous.endpoints, row];\n endpoints.sort(\n (left, right) =>\n right.total - left.total ||\n `${left.method} ${left.path}`.localeCompare(\n `${right.method} ${right.path}`,\n \"en\",\n ),\n );\n this.scopes.set(identity, { ...previous, ...counts, endpoints });\n this.notify();\n }\n\n private notify(): void {\n for (const listener of this.listeners) {\n // Instrumentation must never change a request's outcome.\n try {\n listener();\n } catch {\n /* A broken observer cannot block HTTP. */\n }\n }\n }\n}\n"]}
1
+ {"version":3,"file":"apiRequestMetrics.js","sourceRoot":"","sources":["apiRequestMetrics.ts"],"names":[],"mappings":"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","sourcesContent":["import type { HttpOperation } from \"@tearleads/validators/operation\";\nimport {\n type ApiRequestDurationSummary,\n DurationSampleWindow,\n} from \"./durationSampleWindow\";\nimport type { ApiServerTiming } from \"./serverTiming\";\n\nexport interface ApiRequestCounts {\n readonly total: number;\n readonly inFlight: number;\n readonly succeeded: number;\n readonly failed: number;\n readonly cancelled: number;\n}\n\nexport interface ApiRequestEndpointCounts extends ApiRequestCounts {\n readonly method: string;\n readonly path: string;\n /** Dispatch until response headers, a connection failure, or cancellation. */\n readonly duration: ApiRequestDurationSummary | null;\n /** The API's Server-Timing `total`; null until a response reports it. */\n readonly serverDuration: ApiRequestDurationSummary | null;\n /** The API's Server-Timing `db`; null until a response reports it. */\n readonly databaseDuration: ApiRequestDurationSummary | null;\n}\n\nexport interface ApiRequestMetricsSnapshot extends ApiRequestCounts {\n readonly startedAt: string;\n readonly endpoints: readonly ApiRequestEndpointCounts[];\n}\n\nexport interface ApiRequestInstrumentation {\n readonly metrics: ApiRequestMetrics;\n readonly getIdentity: () => string | null;\n}\n\ntype Outcome = \"succeeded\" | \"failed\" | \"cancelled\";\n\ninterface EndpointDurations {\n readonly duration: DurationSampleWindow;\n readonly serverDuration: DurationSampleWindow;\n readonly databaseDuration: DurationSampleWindow;\n}\n\nconst emptyCounts: ApiRequestCounts = {\n total: 0,\n inFlight: 0,\n succeeded: 0,\n failed: 0,\n cancelled: 0,\n};\n\ntype EndpointDurationSummaries = Pick<\n ApiRequestEndpointCounts,\n \"duration\" | \"serverDuration\" | \"databaseDuration\"\n>;\n\nconst emptyDurations: EndpointDurationSummaries = {\n duration: null,\n serverDuration: null,\n databaseDuration: null,\n};\n\nfunction emptySnapshot(): ApiRequestMetricsSnapshot {\n return { ...emptyCounts, startedAt: new Date().toISOString(), endpoints: [] };\n}\n\nfunction endpointKey(row: { readonly method: string; readonly path: string }) {\n return `${row.method} ${row.path}`;\n}\n\n/**\n * In-memory HTTP attempt counts and durations, partitioned by identity at\n * dispatch time. Only operation templates are retained: no URLs, queries,\n * bodies or tokens. Hosts may reuse this store across client lifetimes until a\n * page reload.\n */\nexport class ApiRequestMetrics {\n private readonly scopes = new Map<string | null, ApiRequestMetricsSnapshot>();\n private readonly durations = new Map<\n string | null,\n Map<string, EndpointDurations>\n >();\n private readonly listeners = new Set<() => void>();\n private readonly generations = new Map<string | null, number>();\n\n /** `now` must be monotonic; tests inject a fake clock. */\n constructor(private readonly now: () => number = () => performance.now()) {}\n\n getSnapshot = (identity: string | null): ApiRequestMetricsSnapshot => {\n let snapshot = this.scopes.get(identity);\n if (!snapshot) {\n snapshot = emptySnapshot();\n this.scopes.set(identity, snapshot);\n }\n return snapshot;\n };\n\n subscribe = (listener: () => void): (() => void) => {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n };\n\n reset(identity: string | null): void {\n this.generations.set(identity, (this.generations.get(identity) ?? 0) + 1);\n this.scopes.set(identity, emptySnapshot());\n this.durations.delete(identity);\n this.notify();\n }\n\n begin(\n identity: string | null,\n operation: HttpOperation,\n ): (outcome: Outcome, serverTiming?: ApiServerTiming | null) => void {\n const generation = this.generations.get(identity) ?? 0;\n const startedAt = this.now();\n const previous = this.getSnapshot(identity);\n const existing = previous.endpoints.find(\n (row) => row.method === operation.method && row.path === operation.path,\n );\n let row: ApiRequestEndpointCounts = {\n ...emptyDurations,\n ...(existing ?? emptyCounts),\n method: operation.method,\n path: operation.path,\n total: (existing?.total ?? 0) + 1,\n inFlight: (existing?.inFlight ?? 0) + 1,\n };\n this.update(identity, previous, existing, row, {\n total: previous.total + 1,\n inFlight: previous.inFlight + 1,\n });\n // Reset replaces the scope's endpoints. Capture a generation independently\n // of snapshots, which are replaced for every concurrent request update.\n let completed = false;\n return (outcome, serverTiming) => {\n if (completed) return;\n completed = true;\n if ((this.generations.get(identity) ?? 0) !== generation) return;\n const current = this.getSnapshot(identity);\n const endpoint = current.endpoints.find(\n (candidate) =>\n candidate.method === row.method && candidate.path === row.path,\n );\n if (!endpoint) return;\n row = {\n ...endpoint,\n ...this.recordDurations(\n identity,\n endpoint,\n this.now() - startedAt,\n serverTiming,\n ),\n inFlight: endpoint.inFlight - 1,\n [outcome]: endpoint[outcome] + 1,\n };\n this.update(identity, current, endpoint, row, {\n inFlight: current.inFlight - 1,\n [outcome]: current[outcome] + 1,\n });\n };\n }\n\n private recordDurations(\n identity: string | null,\n endpoint: ApiRequestEndpointCounts,\n elapsedMs: number,\n serverTiming: ApiServerTiming | null | undefined,\n ): EndpointDurationSummaries {\n let byEndpoint = this.durations.get(identity);\n if (!byEndpoint) {\n byEndpoint = new Map();\n this.durations.set(identity, byEndpoint);\n }\n const key = endpointKey(endpoint);\n let windows = byEndpoint.get(key);\n if (!windows) {\n windows = {\n duration: new DurationSampleWindow(),\n serverDuration: new DurationSampleWindow(),\n databaseDuration: new DurationSampleWindow(),\n };\n byEndpoint.set(key, windows);\n }\n windows.duration.record(elapsedMs);\n const serverMs = serverTiming?.totalMs;\n if (typeof serverMs === \"number\") windows.serverDuration.record(serverMs);\n const databaseMs = serverTiming?.databaseMs;\n if (typeof databaseMs === \"number\") {\n windows.databaseDuration.record(databaseMs);\n }\n return {\n duration: windows.duration.summarize(),\n serverDuration: windows.serverDuration.summarize(),\n databaseDuration: windows.databaseDuration.summarize(),\n };\n }\n\n private update(\n identity: string | null,\n previous: ApiRequestMetricsSnapshot,\n existing: ApiRequestEndpointCounts | undefined,\n row: ApiRequestEndpointCounts,\n counts: Partial<ApiRequestCounts>,\n ): void {\n const endpoints = existing\n ? previous.endpoints.map((candidate) =>\n candidate === existing ? row : candidate,\n )\n : [...previous.endpoints, row];\n endpoints.sort(\n (left, right) =>\n right.total - left.total ||\n endpointKey(left).localeCompare(endpointKey(right), \"en\"),\n );\n this.scopes.set(identity, { ...previous, ...counts, endpoints });\n this.notify();\n }\n\n private notify(): void {\n for (const listener of this.listeners) {\n // Instrumentation must never change a request's outcome.\n try {\n listener();\n } catch {\n /* A broken observer cannot block HTTP. */\n }\n }\n }\n}\n"]}
@@ -0,0 +1,26 @@
1
+ /** Recent percentiles plus all-time totals for one endpoint's durations. */
2
+ export interface ApiRequestDurationSummary {
3
+ /** Durations recorded since reset. */
4
+ readonly samples: number;
5
+ /** Sum of every recorded duration since reset. */
6
+ readonly totalMs: number;
7
+ /** Nearest-rank percentiles over the most recent samples only. */
8
+ readonly p50Ms: number;
9
+ readonly p95Ms: number;
10
+ /** Largest duration since reset, including samples outside the window. */
11
+ readonly maxMs: number;
12
+ }
13
+ export declare const DURATION_SAMPLE_WINDOW_SIZE = 256;
14
+ /**
15
+ * A fixed-size ring of recent durations, so a long session keeps bounded
16
+ * memory and sorts at most DURATION_SAMPLE_WINDOW_SIZE values per update.
17
+ */
18
+ export declare class DurationSampleWindow {
19
+ private readonly recent;
20
+ private nextSlot;
21
+ private samples;
22
+ private totalMs;
23
+ private maxMs;
24
+ record(durationMs: number): void;
25
+ summarize(): ApiRequestDurationSummary | null;
26
+ }
@@ -0,0 +1,42 @@
1
+ export const DURATION_SAMPLE_WINDOW_SIZE = 256;
2
+ function nearestRank(sorted, percentile) {
3
+ const rank = Math.ceil(percentile * sorted.length);
4
+ return sorted[Math.max(0, rank - 1)] ?? 0;
5
+ }
6
+ /**
7
+ * A fixed-size ring of recent durations, so a long session keeps bounded
8
+ * memory and sorts at most DURATION_SAMPLE_WINDOW_SIZE values per update.
9
+ */
10
+ export class DurationSampleWindow {
11
+ recent = [];
12
+ nextSlot = 0;
13
+ samples = 0;
14
+ totalMs = 0;
15
+ maxMs = 0;
16
+ record(durationMs) {
17
+ const sample = Math.max(0, durationMs);
18
+ if (this.recent.length < DURATION_SAMPLE_WINDOW_SIZE) {
19
+ this.recent.push(sample);
20
+ }
21
+ else {
22
+ this.recent[this.nextSlot] = sample;
23
+ this.nextSlot = (this.nextSlot + 1) % DURATION_SAMPLE_WINDOW_SIZE;
24
+ }
25
+ this.samples += 1;
26
+ this.totalMs += sample;
27
+ this.maxMs = Math.max(this.maxMs, sample);
28
+ }
29
+ summarize() {
30
+ if (this.samples === 0)
31
+ return null;
32
+ const sorted = [...this.recent].sort((left, right) => left - right);
33
+ return {
34
+ samples: this.samples,
35
+ totalMs: this.totalMs,
36
+ p50Ms: nearestRank(sorted, 0.5),
37
+ p95Ms: nearestRank(sorted, 0.95),
38
+ maxMs: this.maxMs,
39
+ };
40
+ }
41
+ }
42
+ //# sourceMappingURL=durationSampleWindow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"durationSampleWindow.js","sourceRoot":"","sources":["durationSampleWindow.ts"],"names":[],"mappings":"AAaA,MAAM,CAAC,MAAM,2BAA2B,GAAG,GAAG,CAAC;AAE/C,SAAS,WAAW,CAAC,MAAyB,EAAE,UAAkB;IAChE,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IACnD,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AAC5C,CAAC;AAED;;;GAGG;AACH,MAAM,OAAO,oBAAoB;IACd,MAAM,GAAa,EAAE,CAAC;IAC/B,QAAQ,GAAG,CAAC,CAAC;IACb,OAAO,GAAG,CAAC,CAAC;IACZ,OAAO,GAAG,CAAC,CAAC;IACZ,KAAK,GAAG,CAAC,CAAC;IAElB,MAAM,CAAC,UAAkB;QACvB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;QACvC,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,2BAA2B,EAAE,CAAC;YACrD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC3B,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,MAAM,CAAC;YACpC,IAAI,CAAC,QAAQ,GAAG,CAAC,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC,GAAG,2BAA2B,CAAC;QACpE,CAAC;QACD,IAAI,CAAC,OAAO,IAAI,CAAC,CAAC;QAClB,IAAI,CAAC,OAAO,IAAI,MAAM,CAAC;QACvB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAC5C,CAAC;IAED,SAAS;QACP,IAAI,IAAI,CAAC,OAAO,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QACpC,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC;QACpE,OAAO;YACL,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,GAAG,CAAC;YAC/B,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC;YAChC,KAAK,EAAE,IAAI,CAAC,KAAK;SAClB,CAAC;IACJ,CAAC;CACF","sourcesContent":["/** Recent percentiles plus all-time totals for one endpoint's durations. */\nexport interface ApiRequestDurationSummary {\n /** Durations recorded since reset. */\n readonly samples: number;\n /** Sum of every recorded duration since reset. */\n readonly totalMs: number;\n /** Nearest-rank percentiles over the most recent samples only. */\n readonly p50Ms: number;\n readonly p95Ms: number;\n /** Largest duration since reset, including samples outside the window. */\n readonly maxMs: number;\n}\n\nexport const DURATION_SAMPLE_WINDOW_SIZE = 256;\n\nfunction nearestRank(sorted: readonly number[], percentile: number): number {\n const rank = Math.ceil(percentile * sorted.length);\n return sorted[Math.max(0, rank - 1)] ?? 0;\n}\n\n/**\n * A fixed-size ring of recent durations, so a long session keeps bounded\n * memory and sorts at most DURATION_SAMPLE_WINDOW_SIZE values per update.\n */\nexport class DurationSampleWindow {\n private readonly recent: number[] = [];\n private nextSlot = 0;\n private samples = 0;\n private totalMs = 0;\n private maxMs = 0;\n\n record(durationMs: number): void {\n const sample = Math.max(0, durationMs);\n if (this.recent.length < DURATION_SAMPLE_WINDOW_SIZE) {\n this.recent.push(sample);\n } else {\n this.recent[this.nextSlot] = sample;\n this.nextSlot = (this.nextSlot + 1) % DURATION_SAMPLE_WINDOW_SIZE;\n }\n this.samples += 1;\n this.totalMs += sample;\n this.maxMs = Math.max(this.maxMs, sample);\n }\n\n summarize(): ApiRequestDurationSummary | null {\n if (this.samples === 0) return null;\n const sorted = [...this.recent].sort((left, right) => left - right);\n return {\n samples: this.samples,\n totalMs: this.totalMs,\n p50Ms: nearestRank(sorted, 0.5),\n p95Ms: nearestRank(sorted, 0.95),\n maxMs: this.maxMs,\n };\n }\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import { parseServerTiming, serverTimingHeaderName } from "./serverTiming.js";
1
2
  function beginMeasurement(instrumentation, operation) {
2
3
  let finish;
3
4
  try {
@@ -6,21 +7,25 @@ function beginMeasurement(instrumentation, operation) {
6
7
  catch {
7
8
  // Optional host instrumentation must not prevent HTTP dispatch.
8
9
  }
9
- return (outcome) => {
10
+ return (outcome, response) => {
10
11
  try {
11
- finish?.(outcome);
12
+ finish?.(outcome, response &&
13
+ parseServerTiming(response.headers.get(serverTimingHeaderName)));
12
14
  }
13
15
  catch {
14
16
  // Nor may a recording failure replace the transport's result.
15
17
  }
16
18
  };
17
19
  }
18
- /** Counts each fetch, including automatic retries, without consuming bodies. */
20
+ /**
21
+ * Counts and times each fetch, including automatic retries, without consuming
22
+ * bodies: an attempt ends when its response headers arrive or it fails.
23
+ */
19
24
  export async function measureApiRequest(instrumentation, operation, fetchResponse, signal) {
20
25
  const finish = beginMeasurement(instrumentation, operation);
21
26
  try {
22
27
  const response = await fetchResponse();
23
- finish(response.status >= 200 && response.status < 400 ? "succeeded" : "failed");
28
+ finish(response.status >= 200 && response.status < 400 ? "succeeded" : "failed", response);
24
29
  return response;
25
30
  }
26
31
  catch (error) {
@@ -1 +1 @@
1
- {"version":3,"file":"measureApiRequest.js","sourceRoot":"","sources":["measureApiRequest.ts"],"names":[],"mappings":"AAOA,SAAS,gBAAgB,CACvB,eAAsD,EACtD,SAAwB;IAExB,IAAI,MAAqC,CAAC;IAC1C,IAAI,CAAC;QACH,MAAM,GAAG,eAAe,EAAE,OAAO,CAAC,KAAK,CACrC,eAAe,CAAC,WAAW,EAAE,EAC7B,SAAS,CACV,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,gEAAgE;IAClE,CAAC;IACD,OAAO,CAAC,OAAO,EAAE,EAAE;QACjB,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,OAAO,CAAC,CAAC;QACpB,CAAC;QAAC,MAAM,CAAC;YACP,8DAA8D;QAChE,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AAED,gFAAgF;AAChF,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,eAAsD,EACtD,SAAwB,EACxB,aAAsC,EACtC,MAA+B;IAE/B,MAAM,MAAM,GAAG,gBAAgB,CAAC,eAAe,EAAE,SAAS,CAAC,CAAC;IAC5D,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,aAAa,EAAE,CAAC;QACvC,MAAM,CACJ,QAAQ,CAAC,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,CACzE,CAAC;QACF,OAAO,QAAQ,CAAC;IAClB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;QACjD,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC","sourcesContent":["import type { HttpOperation } from \"@tearleads/validators/operation\";\nimport type { ApiRequestInstrumentation } from \"./apiRequestMetrics\";\n\ntype FinishMeasurement = ReturnType<\n ApiRequestInstrumentation[\"metrics\"][\"begin\"]\n>;\n\nfunction beginMeasurement(\n instrumentation: ApiRequestInstrumentation | undefined,\n operation: HttpOperation,\n): FinishMeasurement {\n let finish: FinishMeasurement | undefined;\n try {\n finish = instrumentation?.metrics.begin(\n instrumentation.getIdentity(),\n operation,\n );\n } catch {\n // Optional host instrumentation must not prevent HTTP dispatch.\n }\n return (outcome) => {\n try {\n finish?.(outcome);\n } catch {\n // Nor may a recording failure replace the transport's result.\n }\n };\n}\n\n/** Counts each fetch, including automatic retries, without consuming bodies. */\nexport async function measureApiRequest(\n instrumentation: ApiRequestInstrumentation | undefined,\n operation: HttpOperation,\n fetchResponse: () => Promise<Response>,\n signal: AbortSignal | undefined,\n): Promise<Response> {\n const finish = beginMeasurement(instrumentation, operation);\n try {\n const response = await fetchResponse();\n finish(\n response.status >= 200 && response.status < 400 ? \"succeeded\" : \"failed\",\n );\n return response;\n } catch (error) {\n finish(signal?.aborted ? \"cancelled\" : \"failed\");\n throw error;\n }\n}\n"]}
1
+ {"version":3,"file":"measureApiRequest.js","sourceRoot":"","sources":["measureApiRequest.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAO3E,SAAS,gBAAgB,CACvB,eAAsD,EACtD,SAAwB;IAExB,IAAI,MAAqC,CAAC;IAC1C,IAAI,CAAC;QACH,MAAM,GAAG,eAAe,EAAE,OAAO,CAAC,KAAK,CACrC,eAAe,CAAC,WAAW,EAAE,EAC7B,SAAS,CACV,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,gEAAgE;IAClE,CAAC;IACD,OAAO,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE;QAC3B,IAAI,CAAC;YACH,MAAM,EAAE,CACN,OAAO,EACP,QAAQ;gBACN,iBAAiB,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC,CAClE,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,8DAA8D;QAChE,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,eAAsD,EACtD,SAAwB,EACxB,aAAsC,EACtC,MAA+B;IAE/B,MAAM,MAAM,GAAG,gBAAgB,CAAC,eAAe,EAAE,SAAS,CAAC,CAAC;IAC5D,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,aAAa,EAAE,CAAC;QACvC,MAAM,CACJ,QAAQ,CAAC,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,EACxE,QAAQ,CACT,CAAC;QACF,OAAO,QAAQ,CAAC;IAClB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;QACjD,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC","sourcesContent":["import type { HttpOperation } from \"@tearleads/validators/operation\";\nimport type { ApiRequestInstrumentation } from \"./apiRequestMetrics\";\nimport { parseServerTiming, serverTimingHeaderName } from \"./serverTiming\";\n\ntype FinishMeasurement = ReturnType<\n ApiRequestInstrumentation[\"metrics\"][\"begin\"]\n>;\ntype Outcome = Parameters<FinishMeasurement>[0];\n\nfunction beginMeasurement(\n instrumentation: ApiRequestInstrumentation | undefined,\n operation: HttpOperation,\n): (outcome: Outcome, response?: Response) => void {\n let finish: FinishMeasurement | undefined;\n try {\n finish = instrumentation?.metrics.begin(\n instrumentation.getIdentity(),\n operation,\n );\n } catch {\n // Optional host instrumentation must not prevent HTTP dispatch.\n }\n return (outcome, response) => {\n try {\n finish?.(\n outcome,\n response &&\n parseServerTiming(response.headers.get(serverTimingHeaderName)),\n );\n } catch {\n // Nor may a recording failure replace the transport's result.\n }\n };\n}\n\n/**\n * Counts and times each fetch, including automatic retries, without consuming\n * bodies: an attempt ends when its response headers arrive or it fails.\n */\nexport async function measureApiRequest(\n instrumentation: ApiRequestInstrumentation | undefined,\n operation: HttpOperation,\n fetchResponse: () => Promise<Response>,\n signal: AbortSignal | undefined,\n): Promise<Response> {\n const finish = beginMeasurement(instrumentation, operation);\n try {\n const response = await fetchResponse();\n finish(\n response.status >= 200 && response.status < 400 ? \"succeeded\" : \"failed\",\n response,\n );\n return response;\n } catch (error) {\n finish(signal?.aborted ? \"cancelled\" : \"failed\");\n throw error;\n }\n}\n"]}
@@ -0,0 +1,12 @@
1
+ export declare const serverTimingHeaderName = "Server-Timing";
2
+ /** The API's own `total` and `db` Server-Timing durations, when reported. */
3
+ export interface ApiServerTiming {
4
+ readonly totalMs: number | null;
5
+ readonly databaseMs: number | null;
6
+ }
7
+ /**
8
+ * Reads `total;dur=<ms>, db;dur=<ms>` in any order. Unknown metrics, missing
9
+ * metrics and malformed durations are ignored; the first entry of a metric
10
+ * wins. Returns null when neither metric is present.
11
+ */
12
+ export declare function parseServerTiming(header: string | null): ApiServerTiming | null;
@@ -0,0 +1,49 @@
1
+ export const serverTimingHeaderName = "Server-Timing";
2
+ function readDuration(parameters) {
3
+ for (const parameter of parameters) {
4
+ const separator = parameter.indexOf("=");
5
+ if (separator < 0)
6
+ continue;
7
+ if (parameter.slice(0, separator).trim().toLowerCase() !== "dur")
8
+ continue;
9
+ const value = parameter
10
+ .slice(separator + 1)
11
+ .trim()
12
+ .replace(/^"(.*)"$/, "$1");
13
+ // Only the first `dur` counts; a malformed one leaves the metric unset.
14
+ if (!/^\d+(\.\d+)?$/.test(value))
15
+ return null;
16
+ return Number(value);
17
+ }
18
+ return null;
19
+ }
20
+ /**
21
+ * Reads `total;dur=<ms>, db;dur=<ms>` in any order. Unknown metrics, missing
22
+ * metrics and malformed durations are ignored; the first entry of a metric
23
+ * wins. Returns null when neither metric is present.
24
+ */
25
+ export function parseServerTiming(header) {
26
+ if (header === null)
27
+ return null;
28
+ let totalMs = null;
29
+ let databaseMs = null;
30
+ const seen = new Set();
31
+ for (const entry of header.split(",")) {
32
+ const [name = "", ...parameters] = entry.split(";");
33
+ const metric = name.trim().toLowerCase();
34
+ if (metric !== "total" && metric !== "db")
35
+ continue;
36
+ if (seen.has(metric))
37
+ continue;
38
+ seen.add(metric);
39
+ const duration = readDuration(parameters);
40
+ if (metric === "total")
41
+ totalMs = duration;
42
+ else
43
+ databaseMs = duration;
44
+ }
45
+ return totalMs === null && databaseMs === null
46
+ ? null
47
+ : { totalMs, databaseMs };
48
+ }
49
+ //# sourceMappingURL=serverTiming.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serverTiming.js","sourceRoot":"","sources":["serverTiming.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,sBAAsB,GAAG,eAAe,CAAC;AAQtD,SAAS,YAAY,CAAC,UAA6B;IACjD,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,MAAM,SAAS,GAAG,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACzC,IAAI,SAAS,GAAG,CAAC;YAAE,SAAS;QAC5B,IAAI,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,KAAK,KAAK;YAAE,SAAS;QAC3E,MAAM,KAAK,GAAG,SAAS;aACpB,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC;aACpB,IAAI,EAAE;aACN,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;QAC7B,wEAAwE;QACxE,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAC9C,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAAqB;IAErB,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACjC,IAAI,OAAO,GAAkB,IAAI,CAAC;IAClC,IAAI,UAAU,GAAkB,IAAI,CAAC;IACrC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACtC,MAAM,CAAC,IAAI,GAAG,EAAE,EAAE,GAAG,UAAU,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACpD,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACzC,IAAI,MAAM,KAAK,OAAO,IAAI,MAAM,KAAK,IAAI;YAAE,SAAS;QACpD,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC;YAAE,SAAS;QAC/B,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACjB,MAAM,QAAQ,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC;QAC1C,IAAI,MAAM,KAAK,OAAO;YAAE,OAAO,GAAG,QAAQ,CAAC;;YACtC,UAAU,GAAG,QAAQ,CAAC;IAC7B,CAAC;IACD,OAAO,OAAO,KAAK,IAAI,IAAI,UAAU,KAAK,IAAI;QAC5C,CAAC,CAAC,IAAI;QACN,CAAC,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC;AAC9B,CAAC","sourcesContent":["export const serverTimingHeaderName = \"Server-Timing\";\n\n/** The API's own `total` and `db` Server-Timing durations, when reported. */\nexport interface ApiServerTiming {\n readonly totalMs: number | null;\n readonly databaseMs: number | null;\n}\n\nfunction readDuration(parameters: readonly string[]): number | null {\n for (const parameter of parameters) {\n const separator = parameter.indexOf(\"=\");\n if (separator < 0) continue;\n if (parameter.slice(0, separator).trim().toLowerCase() !== \"dur\") continue;\n const value = parameter\n .slice(separator + 1)\n .trim()\n .replace(/^\"(.*)\"$/, \"$1\");\n // Only the first `dur` counts; a malformed one leaves the metric unset.\n if (!/^\\d+(\\.\\d+)?$/.test(value)) return null;\n return Number(value);\n }\n return null;\n}\n\n/**\n * Reads `total;dur=<ms>, db;dur=<ms>` in any order. Unknown metrics, missing\n * metrics and malformed durations are ignored; the first entry of a metric\n * wins. Returns null when neither metric is present.\n */\nexport function parseServerTiming(\n header: string | null,\n): ApiServerTiming | null {\n if (header === null) return null;\n let totalMs: number | null = null;\n let databaseMs: number | null = null;\n const seen = new Set<string>();\n for (const entry of header.split(\",\")) {\n const [name = \"\", ...parameters] = entry.split(\";\");\n const metric = name.trim().toLowerCase();\n if (metric !== \"total\" && metric !== \"db\") continue;\n if (seen.has(metric)) continue;\n seen.add(metric);\n const duration = readDuration(parameters);\n if (metric === \"total\") totalMs = duration;\n else databaseMs = duration;\n }\n return totalMs === null && databaseMs === null\n ? null\n : { totalMs, databaseMs };\n}\n"]}
@@ -0,0 +1,9 @@
1
+ export const documentCreateSeedRefinement = {
2
+ description: "a document create seed must contain exactly one outgoing update and no container rekeys, and serialize within the document sync request limit",
3
+ id: "request.document-create-seed",
4
+ };
5
+ export const containerCreateMetadataSeedRefinement = {
6
+ description: "a container create metadata seed must contain exactly one outgoing update and no container rekeys, and serialize within the document sync request limit",
7
+ id: "request.container-create-metadata-seed",
8
+ };
9
+ //# sourceMappingURL=documentCreateSeedRefinements.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"documentCreateSeedRefinements.js","sourceRoot":"","sources":["documentCreateSeedRefinements.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,4BAA4B,GAAG;IAC1C,WAAW,EACT,+IAA+I;IACjJ,EAAE,EAAE,8BAA8B;CAC1B,CAAC;AAEX,MAAM,CAAC,MAAM,qCAAqC,GAAG;IACnD,WAAW,EACT,yJAAyJ;IAC3J,EAAE,EAAE,wCAAwC;CACpC,CAAC","sourcesContent":["export const documentCreateSeedRefinement = {\n description:\n \"a document create seed must contain exactly one outgoing update and no container rekeys, and serialize within the document sync request limit\",\n id: \"request.document-create-seed\",\n} as const;\n\nexport const containerCreateMetadataSeedRefinement = {\n description:\n \"a container create metadata seed must contain exactly one outgoing update and no container rekeys, and serialize within the document sync request limit\",\n id: \"request.container-create-metadata-seed\",\n} as const;\n"]}