@rexezuge/shared 0.0.0-stage → 1.0.1
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/LICENSE +21 -0
- package/dist/Base64.d.ts +52 -0
- package/dist/Base64.d.ts.map +1 -0
- package/dist/Base64.js +81 -0
- package/dist/Base64.js.map +1 -0
- package/dist/Clock.d.ts +56 -0
- package/dist/Clock.d.ts.map +1 -0
- package/dist/Clock.js +59 -0
- package/dist/Clock.js.map +1 -0
- package/dist/CryptoUtil.d.ts +79 -0
- package/dist/CryptoUtil.d.ts.map +1 -0
- package/dist/CryptoUtil.js +97 -0
- package/dist/CryptoUtil.js.map +1 -0
- package/dist/CursorCodec.d.ts +61 -0
- package/dist/CursorCodec.d.ts.map +1 -0
- package/dist/CursorCodec.js +60 -0
- package/dist/CursorCodec.js.map +1 -0
- package/dist/EmailUtil.d.ts +29 -0
- package/dist/EmailUtil.d.ts.map +1 -0
- package/dist/EmailUtil.js +31 -0
- package/dist/EmailUtil.js.map +1 -0
- package/dist/ErrorSanitizationUtil.d.ts +69 -0
- package/dist/ErrorSanitizationUtil.d.ts.map +1 -0
- package/dist/ErrorSanitizationUtil.js +98 -0
- package/dist/ErrorSanitizationUtil.js.map +1 -0
- package/dist/IdGenerator.d.ts +27 -0
- package/dist/IdGenerator.d.ts.map +1 -0
- package/dist/IdGenerator.js +28 -0
- package/dist/IdGenerator.js.map +1 -0
- package/dist/Identity.d.ts +36 -0
- package/dist/Identity.d.ts.map +1 -0
- package/dist/Identity.js +39 -0
- package/dist/Identity.js.map +1 -0
- package/dist/LanguageTag.d.ts +25 -0
- package/dist/LanguageTag.d.ts.map +1 -0
- package/dist/LanguageTag.js +29 -0
- package/dist/LanguageTag.js.map +1 -0
- package/dist/LocaleUtil.d.ts +35 -0
- package/dist/LocaleUtil.d.ts.map +1 -0
- package/dist/LocaleUtil.js +97 -0
- package/dist/LocaleUtil.js.map +1 -0
- package/dist/PasswordFingerprint.d.ts +30 -0
- package/dist/PasswordFingerprint.d.ts.map +1 -0
- package/dist/PasswordFingerprint.js +33 -0
- package/dist/PasswordFingerprint.js.map +1 -0
- package/dist/RemoteUrlPolicy.d.ts +121 -0
- package/dist/RemoteUrlPolicy.d.ts.map +1 -0
- package/dist/RemoteUrlPolicy.js +339 -0
- package/dist/RemoteUrlPolicy.js.map +1 -0
- package/dist/Result.d.ts +32 -0
- package/dist/Result.d.ts.map +1 -0
- package/dist/Result.js +19 -0
- package/dist/Result.js.map +1 -0
- package/dist/SubrequestMeter.d.ts +204 -0
- package/dist/SubrequestMeter.d.ts.map +1 -0
- package/dist/SubrequestMeter.js +169 -0
- package/dist/SubrequestMeter.js.map +1 -0
- package/dist/TimeZoneUtil.d.ts +47 -0
- package/dist/TimeZoneUtil.d.ts.map +1 -0
- package/dist/TimeZoneUtil.js +101 -0
- package/dist/TimeZoneUtil.js.map +1 -0
- package/dist/TimestampUtil.d.ts +53 -0
- package/dist/TimestampUtil.d.ts.map +1 -0
- package/dist/TimestampUtil.js +69 -0
- package/dist/TimestampUtil.js.map +1 -0
- package/dist/TokenHashUtil.d.ts +34 -0
- package/dist/TokenHashUtil.d.ts.map +1 -0
- package/dist/TokenHashUtil.js +37 -0
- package/dist/TokenHashUtil.js.map +1 -0
- package/dist/UUIDUtil.d.ts +70 -0
- package/dist/UUIDUtil.d.ts.map +1 -0
- package/dist/UUIDUtil.js +79 -0
- package/dist/UUIDUtil.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/package.json +34 -3
- package/README.md +0 -3
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What one Worker invocation may spend, counted.
|
|
3
|
+
*
|
|
4
|
+
* ## Provenance
|
|
5
|
+
*
|
|
6
|
+
* Taken from Edge-Sonic's `packages/shared/src/utils/SubrequestMeter.ts`. It is
|
|
7
|
+
* the only subrequest-accounting helper among the nine repos and the most
|
|
8
|
+
* heavily measured; it is reproduced with its code unchanged and its prose
|
|
9
|
+
* generalised away from Edge-Sonic's own layer names and issue docs. No other
|
|
10
|
+
* repo carries a competing copy.
|
|
11
|
+
*
|
|
12
|
+
* ### Why this exists at all
|
|
13
|
+
*
|
|
14
|
+
* Cloudflare counts a **subrequest** per invocation and kills the invocation when
|
|
15
|
+
* the count is exceeded — it does not throw something a `catch` can see, so the
|
|
16
|
+
* whole invocation dies with `Too many subrequests by single Worker invocation`
|
|
17
|
+
* and everything after the ceiling is lost. The only defence is to never issue the
|
|
18
|
+
* request that crosses it.
|
|
19
|
+
*
|
|
20
|
+
* A subrequest is not only `fetch`. The platform's own D1 limits page states:
|
|
21
|
+
*
|
|
22
|
+
* > Queries per Worker invocation (read subrequest limits) — 1000 (Workers Paid) / **50 (Free)**
|
|
23
|
+
*
|
|
24
|
+
* So a D1 query is a subrequest, and on the Free plan a `KV` read, a Durable
|
|
25
|
+
* Object RPC and a Secrets Store read are subrequests too. Budgeting **only**
|
|
26
|
+
* `fetch` shipped a real bug: a scan chunk cost ~40 counted requests and ~200
|
|
27
|
+
* uncounted ones, every chunk died at the ceiling, and the scan only ever
|
|
28
|
+
* "finished" because each dead invocation left a little progress behind.
|
|
29
|
+
*
|
|
30
|
+
* ### Why the count is pessimistic
|
|
31
|
+
*
|
|
32
|
+
* The platform does not say whether a D1 `batch()` of N statements costs 1 or N.
|
|
33
|
+
* The D1 page counts *queries*, which reads as N; the Workers page counts
|
|
34
|
+
* subrequests, which reads as 1. So the meter charges **one per statement**, which
|
|
35
|
+
* is the reading that cannot kill an invocation. If the friendlier reading turns
|
|
36
|
+
* out to be right, the meter over-counts and a chunk does slightly less work per
|
|
37
|
+
* poll — a cost in throughput, never a correctness or availability problem. That
|
|
38
|
+
* asymmetry is the whole reason to choose it: an over-count is a slow scan, an
|
|
39
|
+
* under-count is a dead one.
|
|
40
|
+
*
|
|
41
|
+
* Measured on a Free account on 2026-10-05 there are two budgets: external `fetch`
|
|
42
|
+
* is 50 and **D1 statements are 1,000**, in a separate pool — 1,000 D1 statements
|
|
43
|
+
* and 50 outbound requests in one invocation survive together. A D1 overrun
|
|
44
|
+
* *throws* and an ordinary `catch` sees it; the external ceiling does not. This
|
|
45
|
+
* counter's single ceiling covers both, which is the conservative choice: bounding
|
|
46
|
+
* both by the resource that cannot be handled when it runs out is the choice that
|
|
47
|
+
* cannot take the product down. That is not a licence to catch and continue — a
|
|
48
|
+
* swallowed limit is still a limit.
|
|
49
|
+
*
|
|
50
|
+
* ### Why it is an interface and not a class everywhere
|
|
51
|
+
*
|
|
52
|
+
* Charge points below the layer that decides the budget accept the **interface**
|
|
53
|
+
* as a constructor dependency, and the composition root hands them the one counter
|
|
54
|
+
* for the invocation. Nothing that only spends subrequests constructs a meter.
|
|
55
|
+
*/
|
|
56
|
+
/**
|
|
57
|
+
* What spent the subrequests, for the operator-facing breakdown.
|
|
58
|
+
*
|
|
59
|
+
* `d1` is separated from `kv` and `fetch` because the three have different
|
|
60
|
+
* remedies: a chunk that ran out on `fetch` needs a bigger request budget on a
|
|
61
|
+
* Paid plan, and one that ran out on `d1` needs a smaller page size. Collapsing
|
|
62
|
+
* them into a single number is how the ceiling became invisible in the first
|
|
63
|
+
* place.
|
|
64
|
+
*/
|
|
65
|
+
type SubrequestKind = 'd1' | 'kv' | 'fetch' | 'rpc' | 'secret';
|
|
66
|
+
declare const SUBREQUEST_KINDS: readonly SubrequestKind[];
|
|
67
|
+
/**
|
|
68
|
+
* The counter every charge point writes to.
|
|
69
|
+
*
|
|
70
|
+
* A method rather than a mutable field so a charge point can be handed the
|
|
71
|
+
* *interface* and the test that owns the ceiling keeps the only real instance.
|
|
72
|
+
* `charge` is the whole write surface; `spent` and `byKind` are the only read
|
|
73
|
+
* surface a charge point needs.
|
|
74
|
+
*/
|
|
75
|
+
interface SubrequestMeter {
|
|
76
|
+
/**
|
|
77
|
+
* Record `count` subrequests issued.
|
|
78
|
+
*
|
|
79
|
+
* Never throws and never refuses. A meter that could reject would turn "the
|
|
80
|
+
* budget ran out" into an error at a charge point deep inside a DAO, which is
|
|
81
|
+
* the one place that cannot afford to decide anything — the decision belongs
|
|
82
|
+
* *before* the work, in `canAfford`.
|
|
83
|
+
*/
|
|
84
|
+
charge(count?: number, kind?: SubrequestKind): void;
|
|
85
|
+
/**
|
|
86
|
+
* Subrequests issued so far in this invocation.
|
|
87
|
+
*/
|
|
88
|
+
readonly spent: number;
|
|
89
|
+
/**
|
|
90
|
+
* The ceiling this counter enforces, or `Infinity` when it enforces none.
|
|
91
|
+
*/
|
|
92
|
+
readonly ceiling: number;
|
|
93
|
+
/**
|
|
94
|
+
* How many more fit, floored at zero.
|
|
95
|
+
*/
|
|
96
|
+
readonly remaining: number;
|
|
97
|
+
/**
|
|
98
|
+
* Whether the ceiling has been reached.
|
|
99
|
+
*/
|
|
100
|
+
readonly exhausted: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Whether `count` more would still fit.
|
|
103
|
+
*/
|
|
104
|
+
canAfford(count?: number): boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Lower this meter's effective ceiling for the current unit of work. Never
|
|
107
|
+
* raises it — a narrower budget may only tighten the platform's ceiling, so a
|
|
108
|
+
* caller asking for more than the platform allows is clamped rather than
|
|
109
|
+
* trusted. The same asymmetry as everywhere else here: an over-count costs
|
|
110
|
+
* throughput, an under-count costs an invocation.
|
|
111
|
+
*/
|
|
112
|
+
setCeiling(limit: number): void;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* One counter per Worker invocation.
|
|
116
|
+
*
|
|
117
|
+
* `reset()` rather than a fresh instance per chunk because the counter is owned by
|
|
118
|
+
* the **request scope**, which is created once per invocation: a chunk does not own
|
|
119
|
+
* the budget, it borrows the invocation's. Constructing a new counter per chunk
|
|
120
|
+
* would be a second place that decides what the ceiling is.
|
|
121
|
+
*/
|
|
122
|
+
declare class SubrequestCounter implements SubrequestMeter {
|
|
123
|
+
private readonly limit;
|
|
124
|
+
private counts;
|
|
125
|
+
/**
|
|
126
|
+
* Spend per kind, so a result can report *what* ran out rather than only that
|
|
127
|
+
* something did. A single total is what made this ceiling un-diagnosable: the
|
|
128
|
+
* operator saw "paused" and the possible causes have different fixes.
|
|
129
|
+
*/
|
|
130
|
+
private readonly byKindSpent;
|
|
131
|
+
/**
|
|
132
|
+
* The platform's ceiling, or a narrower one `setCeiling` imposed for the current
|
|
133
|
+
* unit of work. Restored to `limit` by `reset()`, so a narrowed ceiling cannot
|
|
134
|
+
* leak past the chunk that asked for it.
|
|
135
|
+
*/
|
|
136
|
+
private effective;
|
|
137
|
+
constructor(limit?: number);
|
|
138
|
+
charge(count?: number, kind?: SubrequestKind): void;
|
|
139
|
+
get spent(): number;
|
|
140
|
+
/**
|
|
141
|
+
* The **effective** ceiling: the platform's, or a narrower one a caller imposed.
|
|
142
|
+
*/
|
|
143
|
+
get ceiling(): number;
|
|
144
|
+
get remaining(): number;
|
|
145
|
+
get exhausted(): boolean;
|
|
146
|
+
canAfford(count?: number): boolean;
|
|
147
|
+
setCeiling(limit: number): void;
|
|
148
|
+
/**
|
|
149
|
+
* Zero the count and restore the platform's ceiling.
|
|
150
|
+
*
|
|
151
|
+
* One method rather than a public `counts` field so "start measuring again" is a
|
|
152
|
+
* named operation, and so the per-kind breakdown cannot drift out of step with
|
|
153
|
+
* the total — exactly the kind of second-answer defect this file exists to
|
|
154
|
+
* prevent. The ceiling is **restored**, not kept: a narrowed ceiling belongs to
|
|
155
|
+
* the unit of work that asked for it.
|
|
156
|
+
*/
|
|
157
|
+
reset(): void;
|
|
158
|
+
/**
|
|
159
|
+
* Spend per kind, as a plain object. A copy, so a caller cannot reach in and
|
|
160
|
+
* change the totals the operator is shown.
|
|
161
|
+
*/
|
|
162
|
+
breakdown(): Record<SubrequestKind, number>;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* A meter that enforces nothing, for a caller that has not been given one.
|
|
166
|
+
*
|
|
167
|
+
* `Infinity` rather than a small number, because the alternative is a default
|
|
168
|
+
* ceiling that a test would satisfy by accident — the failure this file is about
|
|
169
|
+
* is a path that spends nothing because nothing was watching it, and a limit of 50
|
|
170
|
+
* would hide that behind a passing assertion.
|
|
171
|
+
*/
|
|
172
|
+
declare const UNMETERED_SUBREQUESTS: SubrequestMeter;
|
|
173
|
+
/**
|
|
174
|
+
* What an invocation spent, broken down.
|
|
175
|
+
*
|
|
176
|
+
* A total alone is what made this ceiling undiagnosable. An operator whose scan
|
|
177
|
+
* keeps pausing was told "paused" and nothing else, while the causes have
|
|
178
|
+
* different fixes.
|
|
179
|
+
*/
|
|
180
|
+
interface SubrequestSpend extends Record<SubrequestKind, number> {
|
|
181
|
+
/**
|
|
182
|
+
* Every subrequest spent, whatever its kind. The number the ceiling is enforced
|
|
183
|
+
* against.
|
|
184
|
+
*/
|
|
185
|
+
readonly total: number;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Flatten a meter's breakdown into a report, total included.
|
|
189
|
+
*
|
|
190
|
+
* A function rather than a method on the counter so the *shape* the rest of the
|
|
191
|
+
* system passes around — including the zeroes for kinds that spent nothing — is
|
|
192
|
+
* built in one place. A caller that assembled it by hand would omit an absent kind,
|
|
193
|
+
* and the operator surface would render a missing field as a missing measurement
|
|
194
|
+
* rather than as `0`.
|
|
195
|
+
*/
|
|
196
|
+
declare function subrequestSpend(byKind: Record<SubrequestKind, number>, total: number): SubrequestSpend;
|
|
197
|
+
/**
|
|
198
|
+
* Nothing spent, for a code path that did no work — so a zero report is a
|
|
199
|
+
* *measurement* rather than an absent field.
|
|
200
|
+
*/
|
|
201
|
+
declare const NO_SUBREQUESTS_SPENT: SubrequestSpend;
|
|
202
|
+
export { SubrequestCounter, UNMETERED_SUBREQUESTS, SUBREQUEST_KINDS, subrequestSpend, NO_SUBREQUESTS_SPENT };
|
|
203
|
+
export type { SubrequestMeter, SubrequestKind, SubrequestSpend };
|
|
204
|
+
//# sourceMappingURL=SubrequestMeter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SubrequestMeter.d.ts","sourceRoot":"","sources":["../src/SubrequestMeter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH;;;;;;;;GAQG;AACH,KAAK,cAAc,GAAG,IAAI,GAAG,IAAI,GAAG,OAAO,GAAG,KAAK,GAAG,QAAQ,CAAC;AAE/D,QAAA,MAAM,gBAAgB,EAAE,SAAS,cAAc,EAA2C,CAAC;AAE3F;;;;;;;GAOG;AACH,UAAU,eAAe;IACvB;;;;;;;OAOG;IACH,MAAM,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IACpD;;OAEG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;OAEG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;OAEG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;OAEG;IACH,SAAS,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACnC;;;;;;OAMG;IACH,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED;;;;;;;GAOG;AACH,cAAM,iBAAkB,YAAW,eAAe;IAgBpC,OAAO,CAAC,QAAQ,CAAC,KAAK;IAflC,OAAO,CAAC,MAAM,CAAa;IAE3B;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAiF;IAC7G;;;;OAIG;IACH,OAAO,CAAC,SAAS,CAAS;gBAEG,KAAK,GAAE,MAAiB;IAI9C,MAAM,CAAC,KAAK,SAAI,EAAE,IAAI,GAAE,cAAqB,GAAG,IAAI;IAM3D,IAAW,KAAK,IAAI,MAAM,CAEzB;IAED;;OAEG;IACH,IAAW,OAAO,IAAI,MAAM,CAE3B;IAED,IAAW,SAAS,IAAI,MAAM,CAG7B;IAED,IAAW,SAAS,IAAI,OAAO,CAE9B;IAEM,SAAS,CAAC,KAAK,SAAI,GAAG,OAAO;IAI7B,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAStC;;;;;;;;OAQG;IACI,KAAK,IAAI,IAAI;IAMpB;;;OAGG;IACI,SAAS,IAAI,MAAM,CAAC,cAAc,EAAE,MAAM,CAAC;CAGnD;AAED;;;;;;;GAOG;AACH,QAAA,MAAM,qBAAqB,EAAE,eAAiD,CAAC;AAE/E;;;;;;GAMG;AACH,UAAU,eAAgB,SAAQ,MAAM,CAAC,cAAc,EAAE,MAAM,CAAC;IAC9D;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;GAQG;AACH,iBAAS,eAAe,CAAC,MAAM,EAAE,MAAM,CAAC,cAAc,EAAE,MAAM,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,eAAe,CAE/F;AAED;;;GAGG;AACH,QAAA,MAAM,oBAAoB,EAAE,eAAmF,CAAC;AAEhH,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,eAAe,EAAE,oBAAoB,EAAE,CAAC;AAC7G,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,eAAe,EAAE,CAAC"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What one Worker invocation may spend, counted.
|
|
3
|
+
*
|
|
4
|
+
* ## Provenance
|
|
5
|
+
*
|
|
6
|
+
* Taken from Edge-Sonic's `packages/shared/src/utils/SubrequestMeter.ts`. It is
|
|
7
|
+
* the only subrequest-accounting helper among the nine repos and the most
|
|
8
|
+
* heavily measured; it is reproduced with its code unchanged and its prose
|
|
9
|
+
* generalised away from Edge-Sonic's own layer names and issue docs. No other
|
|
10
|
+
* repo carries a competing copy.
|
|
11
|
+
*
|
|
12
|
+
* ### Why this exists at all
|
|
13
|
+
*
|
|
14
|
+
* Cloudflare counts a **subrequest** per invocation and kills the invocation when
|
|
15
|
+
* the count is exceeded — it does not throw something a `catch` can see, so the
|
|
16
|
+
* whole invocation dies with `Too many subrequests by single Worker invocation`
|
|
17
|
+
* and everything after the ceiling is lost. The only defence is to never issue the
|
|
18
|
+
* request that crosses it.
|
|
19
|
+
*
|
|
20
|
+
* A subrequest is not only `fetch`. The platform's own D1 limits page states:
|
|
21
|
+
*
|
|
22
|
+
* > Queries per Worker invocation (read subrequest limits) — 1000 (Workers Paid) / **50 (Free)**
|
|
23
|
+
*
|
|
24
|
+
* So a D1 query is a subrequest, and on the Free plan a `KV` read, a Durable
|
|
25
|
+
* Object RPC and a Secrets Store read are subrequests too. Budgeting **only**
|
|
26
|
+
* `fetch` shipped a real bug: a scan chunk cost ~40 counted requests and ~200
|
|
27
|
+
* uncounted ones, every chunk died at the ceiling, and the scan only ever
|
|
28
|
+
* "finished" because each dead invocation left a little progress behind.
|
|
29
|
+
*
|
|
30
|
+
* ### Why the count is pessimistic
|
|
31
|
+
*
|
|
32
|
+
* The platform does not say whether a D1 `batch()` of N statements costs 1 or N.
|
|
33
|
+
* The D1 page counts *queries*, which reads as N; the Workers page counts
|
|
34
|
+
* subrequests, which reads as 1. So the meter charges **one per statement**, which
|
|
35
|
+
* is the reading that cannot kill an invocation. If the friendlier reading turns
|
|
36
|
+
* out to be right, the meter over-counts and a chunk does slightly less work per
|
|
37
|
+
* poll — a cost in throughput, never a correctness or availability problem. That
|
|
38
|
+
* asymmetry is the whole reason to choose it: an over-count is a slow scan, an
|
|
39
|
+
* under-count is a dead one.
|
|
40
|
+
*
|
|
41
|
+
* Measured on a Free account on 2026-10-05 there are two budgets: external `fetch`
|
|
42
|
+
* is 50 and **D1 statements are 1,000**, in a separate pool — 1,000 D1 statements
|
|
43
|
+
* and 50 outbound requests in one invocation survive together. A D1 overrun
|
|
44
|
+
* *throws* and an ordinary `catch` sees it; the external ceiling does not. This
|
|
45
|
+
* counter's single ceiling covers both, which is the conservative choice: bounding
|
|
46
|
+
* both by the resource that cannot be handled when it runs out is the choice that
|
|
47
|
+
* cannot take the product down. That is not a licence to catch and continue — a
|
|
48
|
+
* swallowed limit is still a limit.
|
|
49
|
+
*
|
|
50
|
+
* ### Why it is an interface and not a class everywhere
|
|
51
|
+
*
|
|
52
|
+
* Charge points below the layer that decides the budget accept the **interface**
|
|
53
|
+
* as a constructor dependency, and the composition root hands them the one counter
|
|
54
|
+
* for the invocation. Nothing that only spends subrequests constructs a meter.
|
|
55
|
+
*/
|
|
56
|
+
const SUBREQUEST_KINDS = ['d1', 'kv', 'fetch', 'rpc', 'secret'];
|
|
57
|
+
/**
|
|
58
|
+
* One counter per Worker invocation.
|
|
59
|
+
*
|
|
60
|
+
* `reset()` rather than a fresh instance per chunk because the counter is owned by
|
|
61
|
+
* the **request scope**, which is created once per invocation: a chunk does not own
|
|
62
|
+
* the budget, it borrows the invocation's. Constructing a new counter per chunk
|
|
63
|
+
* would be a second place that decides what the ceiling is.
|
|
64
|
+
*/
|
|
65
|
+
class SubrequestCounter {
|
|
66
|
+
limit;
|
|
67
|
+
counts = 0;
|
|
68
|
+
/**
|
|
69
|
+
* Spend per kind, so a result can report *what* ran out rather than only that
|
|
70
|
+
* something did. A single total is what made this ceiling un-diagnosable: the
|
|
71
|
+
* operator saw "paused" and the possible causes have different fixes.
|
|
72
|
+
*/
|
|
73
|
+
byKindSpent = { d1: 0, kv: 0, fetch: 0, rpc: 0, secret: 0 };
|
|
74
|
+
/**
|
|
75
|
+
* The platform's ceiling, or a narrower one `setCeiling` imposed for the current
|
|
76
|
+
* unit of work. Restored to `limit` by `reset()`, so a narrowed ceiling cannot
|
|
77
|
+
* leak past the chunk that asked for it.
|
|
78
|
+
*/
|
|
79
|
+
effective;
|
|
80
|
+
constructor(limit = Infinity) {
|
|
81
|
+
this.limit = limit;
|
|
82
|
+
this.effective = limit;
|
|
83
|
+
}
|
|
84
|
+
charge(count = 1, kind = 'd1') {
|
|
85
|
+
if (count <= 0)
|
|
86
|
+
return;
|
|
87
|
+
this.counts += count;
|
|
88
|
+
this.byKindSpent[kind] += count;
|
|
89
|
+
}
|
|
90
|
+
get spent() {
|
|
91
|
+
return this.counts;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The **effective** ceiling: the platform's, or a narrower one a caller imposed.
|
|
95
|
+
*/
|
|
96
|
+
get ceiling() {
|
|
97
|
+
return this.effective;
|
|
98
|
+
}
|
|
99
|
+
get remaining() {
|
|
100
|
+
if (this.effective === Infinity)
|
|
101
|
+
return Infinity;
|
|
102
|
+
return Math.max(0, this.effective - this.counts);
|
|
103
|
+
}
|
|
104
|
+
get exhausted() {
|
|
105
|
+
return this.counts >= this.effective;
|
|
106
|
+
}
|
|
107
|
+
canAfford(count = 1) {
|
|
108
|
+
return this.counts + count <= this.effective;
|
|
109
|
+
}
|
|
110
|
+
setCeiling(limit) {
|
|
111
|
+
// An unmetered counter stays unmetered: a ceiling of `Infinity` cannot be
|
|
112
|
+
// lowered to a finite one, because the callers that would impose one are the
|
|
113
|
+
// *production* ones and a test double asking for a bound must not start
|
|
114
|
+
// refusing statements.
|
|
115
|
+
if (!Number.isFinite(this.effective) || !Number.isFinite(limit))
|
|
116
|
+
return;
|
|
117
|
+
this.effective = Math.max(0, Math.min(this.effective, Math.floor(limit)));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Zero the count and restore the platform's ceiling.
|
|
121
|
+
*
|
|
122
|
+
* One method rather than a public `counts` field so "start measuring again" is a
|
|
123
|
+
* named operation, and so the per-kind breakdown cannot drift out of step with
|
|
124
|
+
* the total — exactly the kind of second-answer defect this file exists to
|
|
125
|
+
* prevent. The ceiling is **restored**, not kept: a narrowed ceiling belongs to
|
|
126
|
+
* the unit of work that asked for it.
|
|
127
|
+
*/
|
|
128
|
+
reset() {
|
|
129
|
+
this.counts = 0;
|
|
130
|
+
this.effective = this.limit;
|
|
131
|
+
for (const kind of SUBREQUEST_KINDS)
|
|
132
|
+
this.byKindSpent[kind] = 0;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Spend per kind, as a plain object. A copy, so a caller cannot reach in and
|
|
136
|
+
* change the totals the operator is shown.
|
|
137
|
+
*/
|
|
138
|
+
breakdown() {
|
|
139
|
+
return { ...this.byKindSpent };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* A meter that enforces nothing, for a caller that has not been given one.
|
|
144
|
+
*
|
|
145
|
+
* `Infinity` rather than a small number, because the alternative is a default
|
|
146
|
+
* ceiling that a test would satisfy by accident — the failure this file is about
|
|
147
|
+
* is a path that spends nothing because nothing was watching it, and a limit of 50
|
|
148
|
+
* would hide that behind a passing assertion.
|
|
149
|
+
*/
|
|
150
|
+
const UNMETERED_SUBREQUESTS = new SubrequestCounter(Infinity);
|
|
151
|
+
/**
|
|
152
|
+
* Flatten a meter's breakdown into a report, total included.
|
|
153
|
+
*
|
|
154
|
+
* A function rather than a method on the counter so the *shape* the rest of the
|
|
155
|
+
* system passes around — including the zeroes for kinds that spent nothing — is
|
|
156
|
+
* built in one place. A caller that assembled it by hand would omit an absent kind,
|
|
157
|
+
* and the operator surface would render a missing field as a missing measurement
|
|
158
|
+
* rather than as `0`.
|
|
159
|
+
*/
|
|
160
|
+
function subrequestSpend(byKind, total) {
|
|
161
|
+
return { total, d1: byKind.d1, kv: byKind.kv, fetch: byKind.fetch, rpc: byKind.rpc, secret: byKind.secret };
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Nothing spent, for a code path that did no work — so a zero report is a
|
|
165
|
+
* *measurement* rather than an absent field.
|
|
166
|
+
*/
|
|
167
|
+
const NO_SUBREQUESTS_SPENT = subrequestSpend({ d1: 0, kv: 0, fetch: 0, rpc: 0, secret: 0 }, 0);
|
|
168
|
+
export { SubrequestCounter, UNMETERED_SUBREQUESTS, SUBREQUEST_KINDS, subrequestSpend, NO_SUBREQUESTS_SPENT };
|
|
169
|
+
//# sourceMappingURL=SubrequestMeter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SubrequestMeter.js","sourceRoot":"","sources":["../src/SubrequestMeter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAaH,MAAM,gBAAgB,GAA8B,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;AAkD3F;;;;;;;GAOG;AACH,MAAM,iBAAiB;IAgBQ;IAfrB,MAAM,GAAW,CAAC,CAAC;IAE3B;;;;OAIG;IACc,WAAW,GAAmC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;IAC7G;;;;OAIG;IACK,SAAS,CAAS;IAE1B,YAA6B,QAAgB,QAAQ;QAAxB,UAAK,GAAL,KAAK,CAAmB;QACnD,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;IACzB,CAAC;IAEM,MAAM,CAAC,KAAK,GAAG,CAAC,EAAE,OAAuB,IAAI;QAClD,IAAI,KAAK,IAAI,CAAC;YAAE,OAAO;QACvB,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC;QACrB,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC;IAClC,CAAC;IAED,IAAW,KAAK;QACd,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED;;OAEG;IACH,IAAW,OAAO;QAChB,OAAO,IAAI,CAAC,SAAS,CAAC;IACxB,CAAC;IAED,IAAW,SAAS;QAClB,IAAI,IAAI,CAAC,SAAS,KAAK,QAAQ;YAAE,OAAO,QAAQ,CAAC;QACjD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;IACnD,CAAC;IAED,IAAW,SAAS;QAClB,OAAO,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,SAAS,CAAC;IACvC,CAAC;IAEM,SAAS,CAAC,KAAK,GAAG,CAAC;QACxB,OAAO,IAAI,CAAC,MAAM,GAAG,KAAK,IAAI,IAAI,CAAC,SAAS,CAAC;IAC/C,CAAC;IAEM,UAAU,CAAC,KAAa;QAC7B,0EAA0E;QAC1E,6EAA6E;QAC7E,wEAAwE;QACxE,uBAAuB;QACvB,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;YAAE,OAAO;QACxE,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC5E,CAAC;IAED;;;;;;;;OAQG;IACI,KAAK;QACV,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;QAChB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC;QAC5B,KAAK,MAAM,IAAI,IAAI,gBAAgB;YAAE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAClE,CAAC;IAED;;;OAGG;IACI,SAAS;QACd,OAAO,EAAE,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IACjC,CAAC;CACF;AAED;;;;;;;GAOG;AACH,MAAM,qBAAqB,GAAoB,IAAI,iBAAiB,CAAC,QAAQ,CAAC,CAAC;AAiB/E;;;;;;;;GAQG;AACH,SAAS,eAAe,CAAC,MAAsC,EAAE,KAAa;IAC5E,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC;AAC9G,CAAC;AAED;;;GAGG;AACH,MAAM,oBAAoB,GAAoB,eAAe,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AAEhH,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,eAAe,EAAE,oBAAoB,EAAE,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time-zone validation and wall-clock helpers.
|
|
3
|
+
*
|
|
4
|
+
* ## Provenance
|
|
5
|
+
*
|
|
6
|
+
* Taken from Mail-Otter's `TimeZoneUtil.ts` (the only repo that factored the
|
|
7
|
+
* zone-local day start behind a helper). It is well-documented and IANA-correct;
|
|
8
|
+
* no other repo carries a competing copy, so it is kept with one fix.
|
|
9
|
+
*
|
|
10
|
+
* ## Design decision
|
|
11
|
+
*
|
|
12
|
+
* Mail-Otter's `normalize` used `this.isValid(timeZone) ? (timeZone as string).trim() : …`,
|
|
13
|
+
* a type-cast to paper over the fact that `isValid` returns `boolean` and so
|
|
14
|
+
* does not narrow `string | null | undefined`. The kit bans that pattern (no
|
|
15
|
+
* non-null assertions, no `as`-laundering), so the runtime check is a module-level
|
|
16
|
+
* **type guard** and `normalize` runs on the narrowed value. Same behaviour, no
|
|
17
|
+
* cast.
|
|
18
|
+
*
|
|
19
|
+
* `getLocalDayStartUnixSeconds` keeps the two-pass DST handling and the warning
|
|
20
|
+
* not to build it with `new Date('YYYY-MM-DDT00:00:00')` — a zone-less date-time
|
|
21
|
+
* string parses in the runtime's zone, which is always UTC in Workers, silently
|
|
22
|
+
* ignoring `timeZone` entirely.
|
|
23
|
+
*/
|
|
24
|
+
declare const DEFAULT_TIME_ZONE = "UTC";
|
|
25
|
+
declare class TimeZoneUtil {
|
|
26
|
+
static isValid(timeZone: string | null | undefined): boolean;
|
|
27
|
+
static normalize(timeZone: string | null | undefined): string;
|
|
28
|
+
static todayInZone(timeZone: string | null | undefined, now?: Date): string;
|
|
29
|
+
/**
|
|
30
|
+
* Unix seconds of local midnight for the calendar day containing `now`.
|
|
31
|
+
*
|
|
32
|
+
* Two passes are needed because the zone offset depends on the instant: read
|
|
33
|
+
* the wall clock in the target zone, treat that as UTC to derive the offset,
|
|
34
|
+
* then re-anchor local midnight with the offset. The offset is sampled at
|
|
35
|
+
* `now` rather than at the target midnight, which is exact except within a
|
|
36
|
+
* few hours of a DST transition — irrelevant for a 24-hour window.
|
|
37
|
+
*
|
|
38
|
+
* Do NOT build this with `new Date('YYYY-MM-DDT00:00:00')`. A date-time
|
|
39
|
+
* string with no zone designator parses in the *runtime's* zone, and the
|
|
40
|
+
* Workers runtime is always UTC, which silently ignores `timeZone` entirely.
|
|
41
|
+
*
|
|
42
|
+
* An unknown zone falls back to UTC rather than throwing.
|
|
43
|
+
*/
|
|
44
|
+
static getLocalDayStartUnixSeconds(now: Date, timeZone: string | null | undefined): number;
|
|
45
|
+
}
|
|
46
|
+
export { TimeZoneUtil, DEFAULT_TIME_ZONE };
|
|
47
|
+
//# sourceMappingURL=TimeZoneUtil.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TimeZoneUtil.d.ts","sourceRoot":"","sources":["../src/TimeZoneUtil.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,QAAA,MAAM,iBAAiB,QAAQ,CAAC;AAkChC,cAAM,YAAY;WACF,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO;WAIrD,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM;WAItD,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,GAAG,GAAE,IAAiB,GAAG,MAAM;IAK9F;;;;;;;;;;;;;;OAcG;WACW,2BAA2B,CAAC,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM;CAelG;AAED,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,CAAC"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time-zone validation and wall-clock helpers.
|
|
3
|
+
*
|
|
4
|
+
* ## Provenance
|
|
5
|
+
*
|
|
6
|
+
* Taken from Mail-Otter's `TimeZoneUtil.ts` (the only repo that factored the
|
|
7
|
+
* zone-local day start behind a helper). It is well-documented and IANA-correct;
|
|
8
|
+
* no other repo carries a competing copy, so it is kept with one fix.
|
|
9
|
+
*
|
|
10
|
+
* ## Design decision
|
|
11
|
+
*
|
|
12
|
+
* Mail-Otter's `normalize` used `this.isValid(timeZone) ? (timeZone as string).trim() : …`,
|
|
13
|
+
* a type-cast to paper over the fact that `isValid` returns `boolean` and so
|
|
14
|
+
* does not narrow `string | null | undefined`. The kit bans that pattern (no
|
|
15
|
+
* non-null assertions, no `as`-laundering), so the runtime check is a module-level
|
|
16
|
+
* **type guard** and `normalize` runs on the narrowed value. Same behaviour, no
|
|
17
|
+
* cast.
|
|
18
|
+
*
|
|
19
|
+
* `getLocalDayStartUnixSeconds` keeps the two-pass DST handling and the warning
|
|
20
|
+
* not to build it with `new Date('YYYY-MM-DDT00:00:00')` — a zone-less date-time
|
|
21
|
+
* string parses in the runtime's zone, which is always UTC in Workers, silently
|
|
22
|
+
* ignoring `timeZone` entirely.
|
|
23
|
+
*/
|
|
24
|
+
const DEFAULT_TIME_ZONE = 'UTC';
|
|
25
|
+
const WALL_CLOCK_FORMAT = {
|
|
26
|
+
hour12: false,
|
|
27
|
+
year: 'numeric',
|
|
28
|
+
month: '2-digit',
|
|
29
|
+
day: '2-digit',
|
|
30
|
+
hour: '2-digit',
|
|
31
|
+
minute: '2-digit',
|
|
32
|
+
second: '2-digit',
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* A run-time check that narrows to `string`, so callers do not have to cast.
|
|
36
|
+
*
|
|
37
|
+
* `new Intl.DateTimeFormat` throws a `RangeError` on an unknown zone; the guard
|
|
38
|
+
* catches it and answers `false` for `null`/`undefined`/non-strings in one place.
|
|
39
|
+
*/
|
|
40
|
+
function isTimeZoneString(value) {
|
|
41
|
+
if (!value || typeof value !== 'string')
|
|
42
|
+
return false;
|
|
43
|
+
const trimmed = value.trim();
|
|
44
|
+
if (trimmed === '')
|
|
45
|
+
return false;
|
|
46
|
+
// Trim-then-validate, so `normalize`'s `.trim()` is load-bearing: Mail-Otter's
|
|
47
|
+
// guard validated the *untrimmed* string, so a whitespace-padded IANA zone
|
|
48
|
+
// threw in `Intl` and silently fell back to UTC. A padded zone is a typo, not an
|
|
49
|
+
// unknown zone, so it is trimmed and accepted rather than defaulted.
|
|
50
|
+
try {
|
|
51
|
+
new Intl.DateTimeFormat('en-US', { timeZone: trimmed });
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
class TimeZoneUtil {
|
|
59
|
+
static isValid(timeZone) {
|
|
60
|
+
return isTimeZoneString(timeZone);
|
|
61
|
+
}
|
|
62
|
+
static normalize(timeZone) {
|
|
63
|
+
return isTimeZoneString(timeZone) ? timeZone.trim() : DEFAULT_TIME_ZONE;
|
|
64
|
+
}
|
|
65
|
+
static todayInZone(timeZone, now = new Date()) {
|
|
66
|
+
const zone = this.normalize(timeZone);
|
|
67
|
+
return new Intl.DateTimeFormat('en-CA', { timeZone: zone, year: 'numeric', month: '2-digit', day: '2-digit' }).format(now);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Unix seconds of local midnight for the calendar day containing `now`.
|
|
71
|
+
*
|
|
72
|
+
* Two passes are needed because the zone offset depends on the instant: read
|
|
73
|
+
* the wall clock in the target zone, treat that as UTC to derive the offset,
|
|
74
|
+
* then re-anchor local midnight with the offset. The offset is sampled at
|
|
75
|
+
* `now` rather than at the target midnight, which is exact except within a
|
|
76
|
+
* few hours of a DST transition — irrelevant for a 24-hour window.
|
|
77
|
+
*
|
|
78
|
+
* Do NOT build this with `new Date('YYYY-MM-DDT00:00:00')`. A date-time
|
|
79
|
+
* string with no zone designator parses in the *runtime's* zone, and the
|
|
80
|
+
* Workers runtime is always UTC, which silently ignores `timeZone` entirely.
|
|
81
|
+
*
|
|
82
|
+
* An unknown zone falls back to UTC rather than throwing.
|
|
83
|
+
*/
|
|
84
|
+
static getLocalDayStartUnixSeconds(now, timeZone) {
|
|
85
|
+
const zone = this.normalize(timeZone);
|
|
86
|
+
const parts = new Intl.DateTimeFormat('en-CA', { timeZone: zone, ...WALL_CLOCK_FORMAT }).formatToParts(now);
|
|
87
|
+
const part = (type) => parts.find((p) => p.type === type)?.value ?? '0';
|
|
88
|
+
const year = Number(part('year'));
|
|
89
|
+
const month = Number(part('month'));
|
|
90
|
+
const day = Number(part('day'));
|
|
91
|
+
// Some ICU builds report midnight as hour 24 under `hour12: false`.
|
|
92
|
+
const hour = Number(part('hour')) % 24;
|
|
93
|
+
const minute = Number(part('minute'));
|
|
94
|
+
const second = Number(part('second'));
|
|
95
|
+
const wallClockAsUtcMs = Date.UTC(year, month - 1, day, hour, minute, second);
|
|
96
|
+
const zoneOffsetMs = wallClockAsUtcMs - now.getTime();
|
|
97
|
+
return Math.floor((Date.UTC(year, month - 1, day, 0, 0, 0) - zoneOffsetMs) / 1000);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export { TimeZoneUtil, DEFAULT_TIME_ZONE };
|
|
101
|
+
//# sourceMappingURL=TimeZoneUtil.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TimeZoneUtil.js","sourceRoot":"","sources":["../src/TimeZoneUtil.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type Clock } from './Clock';
|
|
2
|
+
/**
|
|
3
|
+
* Unix timestamps, in seconds and milliseconds.
|
|
4
|
+
*
|
|
5
|
+
* ## Provenance
|
|
6
|
+
*
|
|
7
|
+
* Converged from all nine `TimestampUtil.ts` copies. They fall into three
|
|
8
|
+
* shapes: Durable-DAV's minimal two-method set (`getCurrentUnixTimestampInSeconds`
|
|
9
|
+
* + `addDays`) with a deliberate "seconds everywhere" argument; AWS's compact set
|
|
10
|
+
* plus an **injectable `Clock`** on the current-time readers; and the full
|
|
11
|
+
* ChordDHT/Mail-Otter set (`…InMilliseconds`, `addMinutes`, `addHours`,
|
|
12
|
+
* `subtractMinutes`, `subtractDays`, `convertIso…`) reading `Date.now()` directly.
|
|
13
|
+
*
|
|
14
|
+
* ## Design decisions
|
|
15
|
+
*
|
|
16
|
+
* - The **full method set** is kept: the union of the three shapes loses nothing
|
|
17
|
+
* and each method exists because at least two repos independently reached for
|
|
18
|
+
* it.
|
|
19
|
+
* - The current-time reads go through an **injectable `Clock`** defaulting to
|
|
20
|
+
* `SYSTEM_CLOCK` (AWS's shape). Reading the clock through one seam lets a test
|
|
21
|
+
* pin "now" without racing `Date.now()`, and a caller cannot accidentally store
|
|
22
|
+
* milliseconds in a column compared against seconds.
|
|
23
|
+
* - Seconds is the storage unit (Durable-DAV's argument): where millisecond
|
|
24
|
+
* precision is genuinely wanted — ordering within a single pass — reach for
|
|
25
|
+
* `…InMilliseconds` (or `clock.now()`) explicitly, so the unit is visible at the
|
|
26
|
+
* call site. `add*`/`subtract*` operate on the **second** unit and are pure
|
|
27
|
+
* calendar arithmetic, never `Date` arithmetic, so they cannot pick up a
|
|
28
|
+
* timezone or a DST transition.
|
|
29
|
+
*/
|
|
30
|
+
declare class TimestampUtil {
|
|
31
|
+
/**
|
|
32
|
+
* Now, in whole milliseconds.
|
|
33
|
+
*
|
|
34
|
+
* @param clock Inject a `FixedClock` to pin "now" in a test; production leaves
|
|
35
|
+
* it defaulted.
|
|
36
|
+
*/
|
|
37
|
+
static getCurrentUnixTimestampInMilliseconds(clock?: Clock): number;
|
|
38
|
+
/**
|
|
39
|
+
* Now, in whole seconds. The default storage unit.
|
|
40
|
+
*
|
|
41
|
+
* @param clock Inject a `FixedClock` to pin "now" in a test; production leaves
|
|
42
|
+
* it defaulted.
|
|
43
|
+
*/
|
|
44
|
+
static getCurrentUnixTimestampInSeconds(clock?: Clock): number;
|
|
45
|
+
static addMinutes(timestamp: number, minutes: number): number;
|
|
46
|
+
static addHours(timestamp: number, hours: number): number;
|
|
47
|
+
static addDays(timestamp: number, days: number): number;
|
|
48
|
+
static subtractMinutes(timestamp: number, minutes: number): number;
|
|
49
|
+
static subtractDays(timestamp: number, days: number): number;
|
|
50
|
+
static convertIsoToUnixTimestampInSeconds(isoString: string): number;
|
|
51
|
+
}
|
|
52
|
+
export { TimestampUtil };
|
|
53
|
+
//# sourceMappingURL=TimestampUtil.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TimestampUtil.d.ts","sourceRoot":"","sources":["../src/TimestampUtil.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,cAAM,aAAa;IACjB;;;;;OAKG;WACW,qCAAqC,CAAC,KAAK,GAAE,KAAoB,GAAG,MAAM;IAIxF;;;;;OAKG;WACW,gCAAgC,CAAC,KAAK,GAAE,KAAoB,GAAG,MAAM;WAIrE,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM;WAItD,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM;WAIlD,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM;WAIhD,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM;WAI3D,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM;WAIrD,kCAAkC,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;CAG5E;AAED,OAAO,EAAE,aAAa,EAAE,CAAC"}
|