@zapier/kitcore 0.18.0 → 0.20.0
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/CHANGELOG.md +63 -0
- package/README.md +45 -0
- package/dist/index.cjs +47 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +293 -60
- package/dist/index.d.ts +293 -60
- package/dist/index.mjs +47 -16
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,68 @@
|
|
|
1
1
|
# @zapier/kitcore
|
|
2
2
|
|
|
3
|
+
## 0.20.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 4b9d735: `createSdk` now checks that each id's providers match the contract its reference
|
|
8
|
+
declared, not only that a provider exists. A graph joined by string id used to
|
|
9
|
+
compile on the strength of the string alone, so a provider whose types
|
|
10
|
+
contradicted the reference reached the consumer and broke at runtime.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
const doubleRef = declareMethod<"double", { value: number }, number>({
|
|
14
|
+
id: "double",
|
|
15
|
+
});
|
|
16
|
+
// Written for a display surface: it formats instead of computing.
|
|
17
|
+
const formatsIt = defineMethod({
|
|
18
|
+
name: "double",
|
|
19
|
+
run: ({ input }: { input: { value: number } }) => `${input.value * 2}`,
|
|
20
|
+
});
|
|
21
|
+
// Compiled clean before. Rejected at createSdk now.
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
A provider may accept **wider** input and must return a **subtype** of the
|
|
25
|
+
declared output, since the comparison is on the call a consumer makes. See the
|
|
26
|
+
kitcore README for how this applies to optional references, module references,
|
|
27
|
+
and `declareDefault`.
|
|
28
|
+
|
|
29
|
+
`selectExports` and `omitExports` now forward their source's ledgers. They
|
|
30
|
+
returned a bare aggregate before, which dropped both the requirements and the
|
|
31
|
+
contract ledger for everything behind them, so a head that reached a module
|
|
32
|
+
through either helper got no graph checking at all.
|
|
33
|
+
|
|
34
|
+
**Both changes can surface an error in a graph that compiled before.** That is
|
|
35
|
+
the point, since the mismatch was already there. Correct the provider, or widen
|
|
36
|
+
the reference to what the provider actually offers.
|
|
37
|
+
|
|
38
|
+
This release also fixes a latent problem with nested plugins. The cost of
|
|
39
|
+
checking a graph grew exponentially with aggregate nesting depth, and with the
|
|
40
|
+
length of a method import chain. Deep enough composition failed to compile at
|
|
41
|
+
all, reporting `error TS2589`. Both are close to linear now, so nesting and
|
|
42
|
+
chaining stay affordable.
|
|
43
|
+
|
|
44
|
+
Contract checking itself is new work, and it grows the declaration files a
|
|
45
|
+
package publishes. Every downstream typecheck and editor pays that cost.
|
|
46
|
+
|
|
47
|
+
**A hand-written annotation must be updated to keep its check.** `LeafSummary`
|
|
48
|
+
now requires the surfaced binding as a fourth argument, so a three-argument use
|
|
49
|
+
is a compile error. A `PluginSummary` written with two arguments still compiles
|
|
50
|
+
and declares no contract, which drops that plugin out of the check silently.
|
|
51
|
+
|
|
52
|
+
Added `ContractEntry`, `MethodContract`, `DeclarationSummary`, and
|
|
53
|
+
`OptionalDeclarationSummary`, for annotating a plugin by hand instead of letting
|
|
54
|
+
`define*` infer it.
|
|
55
|
+
|
|
56
|
+
## 0.19.0
|
|
57
|
+
|
|
58
|
+
### Minor Changes
|
|
59
|
+
|
|
60
|
+
- 1ec9410: Added `RetryHttpRequestOptions.onRetry` and exported `RetryHttpRequestAttempt` for observing retries scheduled by `retryHttpRequestPlugin`. The callback runs before the retry wait and receives the request, operation ID, failed attempt number, selected delay, and response or error. Promise results are not awaited, and thrown errors or rejected promises do not cancel the retry. When a response is present, read only its status and headers.
|
|
61
|
+
|
|
62
|
+
### Patch Changes
|
|
63
|
+
|
|
64
|
+
- 1ec9410: `retryHttpRequestPlugin` now waits at least 100 milliseconds before retrying when generated backoff is capped by a zero or near-zero `maxDelayMilliseconds`. Use `maxAttempts: 1` to disable retries.
|
|
65
|
+
|
|
3
66
|
## 0.18.0
|
|
4
67
|
|
|
5
68
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -454,6 +454,51 @@ Two flavors, by what happens when nothing provides the id:
|
|
|
454
454
|
- **Required** (`declareMethod` / `declareProperty`): an unsatisfied reference is a **compile-time** error (with a runtime backstop). Use when the capability must be present.
|
|
455
455
|
- **Optional** (`declareOptionalProperty` / `declareOptionalMethod`): binds `undefined` instead of failing, so the consumer handles absence in code (`{ ...DEFAULTS, ...imports.config }` for a value, `imports.track?.(...)` for a method). Use to reference a _foreign_ capability userland may or may not import, without claiming its slot.
|
|
456
456
|
|
|
457
|
+
### The contract is checked, not just the id
|
|
458
|
+
|
|
459
|
+
`createSdk` checks that each id's providers match the contract the reference declared, not only that a provider exists. The comparison is on the call a consumer makes, so a provider may accept **wider** input and must return a **subtype** of the declared output. A provider written from scratch is checked the same as one written against the reference.
|
|
460
|
+
|
|
461
|
+
```ts
|
|
462
|
+
const doubleRef = declareMethod<"double", { value: number }, number>({
|
|
463
|
+
id: "double",
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// Rejected at createSdk: this author wrote `double` for a display surface, so
|
|
467
|
+
// it formats instead of computing. The declaration promised a number.
|
|
468
|
+
defineMethod({
|
|
469
|
+
name: "double",
|
|
470
|
+
run: ({ input }: { input: { value: number } }) => `${input.value * 2}`,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Also rejected: an `item` method is a fine plugin, but the declaration asked
|
|
474
|
+
// for a bare number, not an envelope.
|
|
475
|
+
defineMethod({
|
|
476
|
+
name: "double",
|
|
477
|
+
output: "item",
|
|
478
|
+
run: async ({ input }: { input: { value: number } }) => ({
|
|
479
|
+
data: input.value * 2,
|
|
480
|
+
}),
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// Accepted.
|
|
484
|
+
defineMethod({
|
|
485
|
+
name: "double",
|
|
486
|
+
run: ({ input }: { input: { value: number } }) => input.value * 2,
|
|
487
|
+
});
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Two modules declaring one id independently is the intended use, and stays legal while they agree. They are caught when they disagree, because no single provider can satisfy both. Every reachable provider under an id must honor the contract, so a sound `declareDefault` beside an unsound explicit provider is still rejected: the explicit one is what the runtime picks.
|
|
491
|
+
|
|
492
|
+
An optional reference is checked the same way when a provider does appear. Its contract is the binding a consumer sees, so `undefined` is part of it: a consumer of an optional reference handles the absent case regardless, and a by-reference provider is allowed to pass `undefined` explicitly.
|
|
493
|
+
|
|
494
|
+
A module reference (`declarePlugin`) is checked through its export references, leaf by leaf, so a module method typed differently from its reference is caught. What is NOT checked is whether the real module exports everything the reference promised: only the module's own id is a requirement, so a promised export the module lacks still fails at runtime.
|
|
495
|
+
|
|
496
|
+
Spell the contract out. An id-only `declareMethod<"double">({ id: "double" })` defaults to `unknown` on both sides. That makes it over-strict on input, since a consumer may call it with no argument and any provider needing real input is rejected, and vacuous on output, since every provider returns a subtype of `unknown`. No `declare*` call opts out of the compatibility check.
|
|
497
|
+
|
|
498
|
+
What the graph checks do NOT reach. A `defineHook` carries no summary, so a declaration reachable only through a hook's `imports` is checked by neither ledger. `addPlugin` performs no graph checks at all. And a positional surface cannot be declared by id: `declareMethod` describes an object call only, so a positional provider is correctly rejected against one, and referencing a positional method means casting past the ledger (`curl` does exactly that for `fetch`). Those three are limitations, not design.
|
|
499
|
+
|
|
500
|
+
If you annotate a plugin by hand rather than letting `define*` infer it, pass the contract. `LeafSummary` takes the surfaced binding as a fourth argument (`LeafSummary<"", "setup", typeof imports, MethodContract<SetupOptions, Promise<void>>>`), and a reference is `DeclarationSummary<TId, TBinding>` or `OptionalDeclarationSummary<TId, TBinding>`. A `PluginSummary` written with two arguments still compiles and declares no contract, so a hand-annotated plugin drops out of the compatibility check silently.
|
|
501
|
+
|
|
457
502
|
## Defaults
|
|
458
503
|
|
|
459
504
|
When you _own_ a capability slot and can ship a working implementation, register it as a default with `declareDefault({ plugin })` rather than referencing it. A default is a real, single node in the graph, so it works out of the box, hooks and middleware can wrap it, and an explicit provider of the same id silently preempts it (that is the seam for userland or another plugin to replace it).
|
package/dist/index.cjs
CHANGED
|
@@ -775,6 +775,11 @@ function toIterable(source) {
|
|
|
775
775
|
return { [Symbol.asyncIterator]: () => source[Symbol.asyncIterator]() };
|
|
776
776
|
}
|
|
777
777
|
|
|
778
|
+
// src/utils/promise-utils.ts
|
|
779
|
+
function isPromiseLike(value) {
|
|
780
|
+
return value !== null && typeof value === "object" && typeof value.then === "function";
|
|
781
|
+
}
|
|
782
|
+
|
|
778
783
|
// src/utils/validation.ts
|
|
779
784
|
var parseOrThrow = (schema, input, { adaptError } = {}) => {
|
|
780
785
|
const result = schema.safeParse(input);
|
|
@@ -1241,7 +1246,7 @@ function createRawFunction(coreFn, options) {
|
|
|
1241
1246
|
try {
|
|
1242
1247
|
const parsed = schema ? validateOptions(schema, input, { adaptError }) : input;
|
|
1243
1248
|
const result = coreFn(parsed, context);
|
|
1244
|
-
if (result
|
|
1249
|
+
if (isPromiseLike(result)) {
|
|
1245
1250
|
return result.then(
|
|
1246
1251
|
(value) => {
|
|
1247
1252
|
fireEnd();
|
|
@@ -2023,8 +2028,9 @@ function declareOptionalMethod(config) {
|
|
|
2023
2028
|
`Plugin "${id}" is an optional stand-in (declareOptionalMethod) with no implementation. Its binding is \`undefined\` unless a real plugin is registered under this id.`
|
|
2024
2029
|
);
|
|
2025
2030
|
}
|
|
2026
|
-
// Requires nothing
|
|
2027
|
-
//
|
|
2031
|
+
// Requires nothing: a consumer that imports it still passes `createSdk`'s
|
|
2032
|
+
// completeness check unprovided. The contract is still carried, so a
|
|
2033
|
+
// provider that DOES appear under the id is checked against it. The
|
|
2028
2034
|
// `optional: true` literal drives `PluginSurface` to type the binding
|
|
2029
2035
|
// `| undefined`.
|
|
2030
2036
|
};
|
|
@@ -2074,9 +2080,12 @@ function declareOptionalProperty(config) {
|
|
|
2074
2080
|
optional: true,
|
|
2075
2081
|
imports: [],
|
|
2076
2082
|
importBindings: []
|
|
2077
|
-
// Requires nothing
|
|
2078
|
-
//
|
|
2079
|
-
//
|
|
2083
|
+
// Requires nothing: a consumer that imports it still passes `createSdk`'s
|
|
2084
|
+
// completeness check unprovided. The contract is the binding a consumer
|
|
2085
|
+
// sees, `TValue | undefined`, which is also what a by-reference provider
|
|
2086
|
+
// (`defineProperty(ref, { value })`) is allowed to pass. A consumer of an
|
|
2087
|
+
// optional reference has to handle the absent case either way, so an
|
|
2088
|
+
// explicit `undefined` breaks nothing a narrower contract would protect.
|
|
2080
2089
|
};
|
|
2081
2090
|
}
|
|
2082
2091
|
function declareDefault({
|
|
@@ -2573,9 +2582,6 @@ function applyListOutputPolicy(page, policy) {
|
|
|
2573
2582
|
var FRAMEWORK_CONFIGURATION_IDS = /* @__PURE__ */ new Set([
|
|
2574
2583
|
CORE_OPTIONS_ID
|
|
2575
2584
|
]);
|
|
2576
|
-
function isPromiseLike(value) {
|
|
2577
|
-
return value !== null && typeof value === "object" && typeof value.then === "function";
|
|
2578
|
-
}
|
|
2579
2585
|
function normalizeOutput(output) {
|
|
2580
2586
|
if (output === void 0) return { type: "raw" };
|
|
2581
2587
|
if (typeof output === "string") return { type: output };
|
|
@@ -5062,6 +5068,7 @@ var DEFAULT_IDEMPOTENT_METHODS = [
|
|
|
5062
5068
|
"TRACE"
|
|
5063
5069
|
];
|
|
5064
5070
|
var BASE_BACKOFF_MILLISECONDS = 1e3;
|
|
5071
|
+
var MIN_BACKOFF_MILLISECONDS = 100;
|
|
5065
5072
|
var JITTER_FACTOR = 0.5;
|
|
5066
5073
|
var EPOCH_THRESHOLD_SECONDS = 1e9;
|
|
5067
5074
|
function directedDelayMilliseconds(response) {
|
|
@@ -5086,6 +5093,12 @@ function backoffMilliseconds(attemptNumber) {
|
|
|
5086
5093
|
const base = BASE_BACKOFF_MILLISECONDS * 2 ** (attemptNumber - 1);
|
|
5087
5094
|
return base + Math.random() * JITTER_FACTOR * base;
|
|
5088
5095
|
}
|
|
5096
|
+
function clampBackoffMilliseconds(attemptNumber, maxDelayMilliseconds) {
|
|
5097
|
+
return Math.max(
|
|
5098
|
+
Math.min(backoffMilliseconds(attemptNumber), maxDelayMilliseconds),
|
|
5099
|
+
MIN_BACKOFF_MILLISECONDS
|
|
5100
|
+
);
|
|
5101
|
+
}
|
|
5089
5102
|
function abortReason(signal) {
|
|
5090
5103
|
const reason = signal?.reason;
|
|
5091
5104
|
return reason ?? new Error("The request was aborted.");
|
|
@@ -5120,6 +5133,20 @@ var retryHttpRequestPlugin = defineHook({
|
|
|
5120
5133
|
const maxDelayMilliseconds = options.maxDelayMilliseconds ?? DEFAULT_MAX_DELAY_MILLISECONDS;
|
|
5121
5134
|
const idempotentMethods = options.idempotentMethods ?? DEFAULT_IDEMPOTENT_METHODS;
|
|
5122
5135
|
const statuses = isIdempotent(input.request, idempotentMethods) ? options.retryStatuses ?? DEFAULT_RETRY_STATUSES : options.nonIdempotentRetryStatuses ?? DEFAULT_NON_IDEMPOTENT_RETRY_STATUSES;
|
|
5136
|
+
const notifyRetry = (notice) => {
|
|
5137
|
+
try {
|
|
5138
|
+
const observed = options.onRetry?.({
|
|
5139
|
+
...notice,
|
|
5140
|
+
request: input.attempt.operation.request,
|
|
5141
|
+
operationId: input.attempt.operation.operationId
|
|
5142
|
+
});
|
|
5143
|
+
if (isPromiseLike(observed)) {
|
|
5144
|
+
void observed.then(void 0, () => {
|
|
5145
|
+
});
|
|
5146
|
+
}
|
|
5147
|
+
} catch {
|
|
5148
|
+
}
|
|
5149
|
+
};
|
|
5123
5150
|
for (let attemptNumber = 1; ; attemptNumber++) {
|
|
5124
5151
|
const attempt = {
|
|
5125
5152
|
...input.attempt,
|
|
@@ -5141,10 +5168,16 @@ var retryHttpRequestPlugin = defineHook({
|
|
|
5141
5168
|
)) {
|
|
5142
5169
|
throw error;
|
|
5143
5170
|
}
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5171
|
+
const errorDelay = clampBackoffMilliseconds(
|
|
5172
|
+
attemptNumber,
|
|
5173
|
+
maxDelayMilliseconds
|
|
5147
5174
|
);
|
|
5175
|
+
notifyRetry({
|
|
5176
|
+
attemptNumber,
|
|
5177
|
+
delayMilliseconds: errorDelay,
|
|
5178
|
+
error
|
|
5179
|
+
});
|
|
5180
|
+
await sleep(errorDelay, attempt.signal);
|
|
5148
5181
|
continue;
|
|
5149
5182
|
}
|
|
5150
5183
|
if (!statuses.includes(response.status) || !canRetry(
|
|
@@ -5157,10 +5190,8 @@ var retryHttpRequestPlugin = defineHook({
|
|
|
5157
5190
|
const directed = directedDelayMilliseconds(response);
|
|
5158
5191
|
if (directed != null && directed > maxDelayMilliseconds)
|
|
5159
5192
|
return response;
|
|
5160
|
-
const delay =
|
|
5161
|
-
|
|
5162
|
-
maxDelayMilliseconds
|
|
5163
|
-
);
|
|
5193
|
+
const delay = directed ?? clampBackoffMilliseconds(attemptNumber, maxDelayMilliseconds);
|
|
5194
|
+
notifyRetry({ attemptNumber, delayMilliseconds: delay, response });
|
|
5164
5195
|
await response.body?.cancel().catch(() => {
|
|
5165
5196
|
});
|
|
5166
5197
|
await sleep(delay, attempt.signal);
|