@tinycloud/sdk-services 3.1.0-beta.7 → 3.1.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/dist/{BaseService-DU-lE-Qx.d.ts → BaseService-B_UKhVht.d.ts} +1 -1
- package/dist/{BaseService-C8zRy0ww.d.cts → BaseService-DWbPbov7.d.cts} +1 -1
- package/dist/encryption/index.cjs +8 -0
- package/dist/encryption/index.cjs.map +1 -1
- package/dist/encryption/index.d.cts +2 -2
- package/dist/encryption/index.d.ts +2 -2
- package/dist/encryption/index.js +8 -0
- package/dist/encryption/index.js.map +1 -1
- package/dist/index.cjs +760 -169
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +108 -17
- package/dist/index.d.ts +108 -17
- package/dist/index.js +760 -169
- package/dist/index.js.map +1 -1
- package/dist/internal/decrypt-transport-response-error.d.cts +1 -1
- package/dist/internal/decrypt-transport-response-error.d.ts +1 -1
- package/dist/kv/index.cjs +256 -9
- package/dist/kv/index.cjs.map +1 -1
- package/dist/kv/index.d.cts +182 -3
- package/dist/kv/index.d.ts +182 -3
- package/dist/kv/index.js +256 -9
- package/dist/kv/index.js.map +1 -1
- package/dist/sql/index.cjs +8 -0
- package/dist/sql/index.cjs.map +1 -1
- package/dist/sql/index.d.cts +2 -2
- package/dist/sql/index.d.ts +2 -2
- package/dist/sql/index.js +8 -0
- package/dist/sql/index.js.map +1 -1
- package/dist/{types-qwMmB7qB.d.cts → types-CVwwxqXF.d.cts} +8 -0
- package/dist/{types-qwMmB7qB.d.ts → types-CVwwxqXF.d.ts} +8 -0
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -155,6 +155,10 @@ var ErrorCodes = {
|
|
|
155
155
|
AUTH_EXPIRED: "AUTH_EXPIRED",
|
|
156
156
|
AUTH_REQUIRED: "AUTH_REQUIRED",
|
|
157
157
|
AUTH_UNAUTHORIZED: "AUTH_UNAUTHORIZED",
|
|
158
|
+
/** HTTP 401: the delegation the invocation cites was revoked. */
|
|
159
|
+
AUTH_DELEGATION_REVOKED: "AUTH_DELEGATION_REVOKED",
|
|
160
|
+
/** HTTP 401: a delegation the cited one rests on was revoked. */
|
|
161
|
+
AUTH_DELEGATION_ANCESTOR_REVOKED: "AUTH_DELEGATION_ANCESTOR_REVOKED",
|
|
158
162
|
NETWORK_ERROR: "NETWORK_ERROR",
|
|
159
163
|
TIMEOUT: "TIMEOUT",
|
|
160
164
|
ABORTED: "ABORTED",
|
|
@@ -166,6 +170,10 @@ var ErrorCodes = {
|
|
|
166
170
|
KV_PRECONDITION_FAILED: "KV_PRECONDITION_FAILED",
|
|
167
171
|
KV_CONFLICT: "KV_CONFLICT",
|
|
168
172
|
KV_RESPONSE_TOO_LARGE: "KV_RESPONSE_TOO_LARGE",
|
|
173
|
+
/** HTTP 410 from the change feed: discard sync state and restart without a cursor; `meta.reason` says why. */
|
|
174
|
+
KV_SYNC_RESET_REQUIRED: "KV_SYNC_RESET_REQUIRED",
|
|
175
|
+
/** HTTP 403 from the change feed: the retention grant was refused; `meta.reason` says why. */
|
|
176
|
+
KV_RETENTION_GRANT_REFUSED: "KV_RETENTION_GRANT_REFUSED",
|
|
169
177
|
// SQL-specific errors
|
|
170
178
|
SQL_ERROR: "SQL_ERROR",
|
|
171
179
|
SQL_PERMISSION_DENIED: "SQL_PERMISSION_DENIED",
|
|
@@ -1767,6 +1775,30 @@ var PrefixedKVService = class _PrefixedKVService {
|
|
|
1767
1775
|
removePrefix
|
|
1768
1776
|
});
|
|
1769
1777
|
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Read the change feed for everything under this prefix. A view with an
|
|
1780
|
+
* empty prefix (`""` or `"/"`) is refused: the node does not serve a
|
|
1781
|
+
* whole-space feed.
|
|
1782
|
+
*/
|
|
1783
|
+
async changes(options) {
|
|
1784
|
+
if (this._prefix.replace(/\/+$/, "") === "") {
|
|
1785
|
+
return err(serviceError(
|
|
1786
|
+
ErrorCodes.INVALID_INPUT,
|
|
1787
|
+
"KV changes needs a non-empty prefix; this view has none",
|
|
1788
|
+
"kv"
|
|
1789
|
+
));
|
|
1790
|
+
}
|
|
1791
|
+
const syncPrefix = `${this._prefix}/`;
|
|
1792
|
+
const response = await this._kv.changes({ ...options, prefix: syncPrefix });
|
|
1793
|
+
if (!response.ok) return response;
|
|
1794
|
+
return ok({
|
|
1795
|
+
...response.data,
|
|
1796
|
+
changes: response.data.changes.map((change) => ({
|
|
1797
|
+
...change,
|
|
1798
|
+
key: change.key.startsWith(syncPrefix) ? change.key.slice(syncPrefix.length) : change.key
|
|
1799
|
+
}))
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1770
1802
|
/**
|
|
1771
1803
|
* Delete a key.
|
|
1772
1804
|
*/
|
|
@@ -1819,11 +1851,36 @@ var KVAction = {
|
|
|
1819
1851
|
PUT: "tinycloud.kv/put",
|
|
1820
1852
|
LIST: "tinycloud.kv/list",
|
|
1821
1853
|
DELETE: "tinycloud.kv/del",
|
|
1822
|
-
HEAD: "tinycloud.kv/metadata"
|
|
1854
|
+
HEAD: "tinycloud.kv/metadata",
|
|
1855
|
+
/** The change feed; never implied by `*` or `tinycloud.kv/*`. */
|
|
1856
|
+
SYNC: "tinycloud.kv/sync",
|
|
1857
|
+
/** Retention attestation; presented through `retentionGrant`, never invoked or implied. */
|
|
1858
|
+
RETAIN: "tinycloud.kv/retain"
|
|
1823
1859
|
};
|
|
1824
1860
|
|
|
1825
1861
|
// src/kv/KVService.ts
|
|
1826
1862
|
var MAX_KV_BATCH_READ_ITEMS = 100;
|
|
1863
|
+
function isJsonObject(value) {
|
|
1864
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1865
|
+
}
|
|
1866
|
+
var pathByteEncoder = new TextEncoder();
|
|
1867
|
+
function comparePathsByBytes(a, b) {
|
|
1868
|
+
const left = pathByteEncoder.encode(a);
|
|
1869
|
+
const right = pathByteEncoder.encode(b);
|
|
1870
|
+
const length = Math.min(left.length, right.length);
|
|
1871
|
+
for (let index = 0; index < length; index++) {
|
|
1872
|
+
if (left[index] !== right[index]) {
|
|
1873
|
+
return left[index] - right[index];
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
return left.length - right.length;
|
|
1877
|
+
}
|
|
1878
|
+
var REVOKED_BODY = /^(?:Invalid invocation: )?delegation-(?:(revoked): [A-Za-z0-9]+|(ancestor-revoked): ancestor=[A-Za-z0-9]+ invoked=[A-Za-z0-9]+)$/;
|
|
1879
|
+
function revocationCodeOf(body) {
|
|
1880
|
+
const match = REVOKED_BODY.exec(body.trim());
|
|
1881
|
+
if (match === null) return void 0;
|
|
1882
|
+
return match[1] !== void 0 ? ErrorCodes.AUTH_DELEGATION_REVOKED : ErrorCodes.AUTH_DELEGATION_ANCESTOR_REVOKED;
|
|
1883
|
+
}
|
|
1827
1884
|
function encodeKvBatchPartName(path) {
|
|
1828
1885
|
return encodeURIComponent(path).replace(
|
|
1829
1886
|
/[!'()*]/g,
|
|
@@ -2051,15 +2108,25 @@ var KVService = class extends BaseService {
|
|
|
2051
2108
|
)?.[1];
|
|
2052
2109
|
return contentType?.includes("application/json") ? JSON.parse(text) : text;
|
|
2053
2110
|
}
|
|
2111
|
+
/**
|
|
2112
|
+
* Validate a batch read response and return its items in the caller's
|
|
2113
|
+
* request order. The node flattens invocation abilities from a `BTreeMap`,
|
|
2114
|
+
* so results arrive sorted by UTF-8 byte order of the path, not in request
|
|
2115
|
+
* order. Items that carry their path in `key` are matched by path; if no
|
|
2116
|
+
* item carries a key, items are assumed to be in the node's byte-sorted
|
|
2117
|
+
* order and the request is aligned the same way. Any mix of keyed and
|
|
2118
|
+
* keyless items, a missing requested path, an unrequested path, or a
|
|
2119
|
+
* malformed item fails closed (returns undefined).
|
|
2120
|
+
*/
|
|
2054
2121
|
normalizeBatchReadResponse(data, paths, requireData) {
|
|
2055
2122
|
if (!data || typeof data !== "object") return void 0;
|
|
2056
2123
|
const response = data;
|
|
2057
|
-
|
|
2124
|
+
const items = response.results;
|
|
2125
|
+
if (!Array.isArray(items) || items.length !== paths.length) {
|
|
2058
2126
|
return void 0;
|
|
2059
2127
|
}
|
|
2060
|
-
for (
|
|
2061
|
-
|
|
2062
|
-
if (!item || typeof item !== "object" || item.key !== paths[index] || typeof item.ok !== "boolean") {
|
|
2128
|
+
for (const item of items) {
|
|
2129
|
+
if (!item || typeof item !== "object" || typeof item.ok !== "boolean") {
|
|
2063
2130
|
return void 0;
|
|
2064
2131
|
}
|
|
2065
2132
|
if (item.ok && (!item.headers || typeof item.headers !== "object" || Object.values(item.headers).some((value) => typeof value !== "string") || requireData && typeof item.dataBase64 !== "string")) {
|
|
@@ -2069,7 +2136,24 @@ var KVService = class extends BaseService {
|
|
|
2069
2136
|
return void 0;
|
|
2070
2137
|
}
|
|
2071
2138
|
}
|
|
2072
|
-
|
|
2139
|
+
if (items.every((item) => typeof item.key === "string")) {
|
|
2140
|
+
const byPath = new Map(items.map((item) => [item.key, item]));
|
|
2141
|
+
if (byPath.size !== items.length) return void 0;
|
|
2142
|
+
const aligned2 = [];
|
|
2143
|
+
for (const path of paths) {
|
|
2144
|
+
const item = byPath.get(path);
|
|
2145
|
+
if (!item) return void 0;
|
|
2146
|
+
aligned2.push(item);
|
|
2147
|
+
}
|
|
2148
|
+
return aligned2;
|
|
2149
|
+
}
|
|
2150
|
+
if (items.some((item) => item.key !== void 0)) return void 0;
|
|
2151
|
+
const requestOrder = paths.map((_, index) => index).sort((a, b) => comparePathsByBytes(paths[a], paths[b]));
|
|
2152
|
+
const aligned = new Array(items.length);
|
|
2153
|
+
for (let position = 0; position < requestOrder.length; position++) {
|
|
2154
|
+
aligned[requestOrder[position]] = items[position];
|
|
2155
|
+
}
|
|
2156
|
+
return aligned;
|
|
2073
2157
|
}
|
|
2074
2158
|
async batchRead(keys, action, options) {
|
|
2075
2159
|
if (!this.requireAuth()) return err(authRequiredError("kv"));
|
|
@@ -2162,19 +2246,19 @@ var KVService = class extends BaseService {
|
|
|
2162
2246
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2163
2247
|
));
|
|
2164
2248
|
}
|
|
2165
|
-
const
|
|
2249
|
+
const items = this.normalizeBatchReadResponse(
|
|
2166
2250
|
await response.json(),
|
|
2167
2251
|
paths,
|
|
2168
2252
|
action === KVAction.GET
|
|
2169
2253
|
);
|
|
2170
|
-
if (!
|
|
2254
|
+
if (!items) {
|
|
2171
2255
|
return err(serviceError(
|
|
2172
2256
|
ErrorCodes.NETWORK_ERROR,
|
|
2173
2257
|
"KV batch read response did not match the requested keys",
|
|
2174
2258
|
"kv"
|
|
2175
2259
|
));
|
|
2176
2260
|
}
|
|
2177
|
-
const results =
|
|
2261
|
+
const results = items.map((item, index) => {
|
|
2178
2262
|
if (!item.ok) {
|
|
2179
2263
|
return {
|
|
2180
2264
|
key: keys[index],
|
|
@@ -2742,6 +2826,169 @@ var KVService = class extends BaseService {
|
|
|
2742
2826
|
}
|
|
2743
2827
|
});
|
|
2744
2828
|
}
|
|
2829
|
+
/**
|
|
2830
|
+
* Read one page of the `tinycloud.kv/sync` change feed for a prefix.
|
|
2831
|
+
*/
|
|
2832
|
+
async changes(options) {
|
|
2833
|
+
return this.withTelemetry("changes", options?.prefix, async () => {
|
|
2834
|
+
if (!this.requireAuth()) {
|
|
2835
|
+
return err(authRequiredError("kv"));
|
|
2836
|
+
}
|
|
2837
|
+
if (typeof options?.prefix !== "string" || options.prefix.length === 0) {
|
|
2838
|
+
return err(serviceError(
|
|
2839
|
+
ErrorCodes.INVALID_INPUT,
|
|
2840
|
+
"KV changes prefix must be a non-empty string",
|
|
2841
|
+
"kv"
|
|
2842
|
+
));
|
|
2843
|
+
}
|
|
2844
|
+
if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 1e3)) {
|
|
2845
|
+
return err(serviceError(
|
|
2846
|
+
ErrorCodes.INVALID_INPUT,
|
|
2847
|
+
"KV changes limit must be an integer from 1 through 1000",
|
|
2848
|
+
"kv"
|
|
2849
|
+
));
|
|
2850
|
+
}
|
|
2851
|
+
const headers = {};
|
|
2852
|
+
if (options.limit !== void 0) headers["x-tinycloud-limit"] = String(options.limit);
|
|
2853
|
+
if (options.cursor !== void 0) headers["x-tinycloud-cursor"] = options.cursor;
|
|
2854
|
+
if (options.retentionGrant !== void 0) {
|
|
2855
|
+
headers["x-tinycloud-retention-grant"] = options.retentionGrant;
|
|
2856
|
+
}
|
|
2857
|
+
const request = this.createRequestSignal(options.signal, options.timeout);
|
|
2858
|
+
try {
|
|
2859
|
+
const response = await this.invokeOperation(
|
|
2860
|
+
options.prefix,
|
|
2861
|
+
KVAction.SYNC,
|
|
2862
|
+
void 0,
|
|
2863
|
+
request.signal,
|
|
2864
|
+
headers
|
|
2865
|
+
);
|
|
2866
|
+
if (!response.ok) {
|
|
2867
|
+
return await this.changesFailure(response, options.prefix, request.signal);
|
|
2868
|
+
}
|
|
2869
|
+
const text = await response.text();
|
|
2870
|
+
let body;
|
|
2871
|
+
try {
|
|
2872
|
+
body = JSON.parse(text);
|
|
2873
|
+
} catch {
|
|
2874
|
+
body = void 0;
|
|
2875
|
+
}
|
|
2876
|
+
const page = this.normalizeChangesResponse(body);
|
|
2877
|
+
if (!page) {
|
|
2878
|
+
return err(serviceError(
|
|
2879
|
+
ErrorCodes.NETWORK_ERROR,
|
|
2880
|
+
"KV changes response was not a tinycloud.kv/sync page",
|
|
2881
|
+
"kv",
|
|
2882
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2883
|
+
));
|
|
2884
|
+
}
|
|
2885
|
+
return ok(page);
|
|
2886
|
+
} catch (error) {
|
|
2887
|
+
return err(wrapError("kv", error));
|
|
2888
|
+
} finally {
|
|
2889
|
+
request.dispose();
|
|
2890
|
+
}
|
|
2891
|
+
});
|
|
2892
|
+
}
|
|
2893
|
+
/**
|
|
2894
|
+
* Map a failed `kv/sync` response. 410 and the retention 403 carry
|
|
2895
|
+
* `{"error":{"code","reason"}}`; a revoked grant is a 401 whose whole body
|
|
2896
|
+
* is the node's revocation error (see REVOKED_BODY). A body read that
|
|
2897
|
+
* fails because the request was cancelled or timed out rethrows, so the
|
|
2898
|
+
* caller sees `ABORTED`/`TIMEOUT` rather than a mapped status.
|
|
2899
|
+
*/
|
|
2900
|
+
async changesFailure(response, prefix, signal) {
|
|
2901
|
+
if (response.status === 404) {
|
|
2902
|
+
return this.classifyNotFound(response, prefix);
|
|
2903
|
+
}
|
|
2904
|
+
let errorText;
|
|
2905
|
+
try {
|
|
2906
|
+
errorText = await response.text();
|
|
2907
|
+
} catch (error) {
|
|
2908
|
+
if (signal.aborted) throw error;
|
|
2909
|
+
errorText = "";
|
|
2910
|
+
}
|
|
2911
|
+
const meta = { status: response.status, statusText: response.statusText };
|
|
2912
|
+
let reason;
|
|
2913
|
+
let bodyCode;
|
|
2914
|
+
try {
|
|
2915
|
+
const body = JSON.parse(errorText);
|
|
2916
|
+
const error = isJsonObject(body) ? body.error : void 0;
|
|
2917
|
+
if (isJsonObject(error)) {
|
|
2918
|
+
if (typeof error.code === "string") bodyCode = error.code;
|
|
2919
|
+
if (typeof error.reason === "string") reason = error.reason;
|
|
2920
|
+
}
|
|
2921
|
+
} catch {
|
|
2922
|
+
}
|
|
2923
|
+
const context = `Failed to read KV changes for ${JSON.stringify(prefix)}`;
|
|
2924
|
+
if (response.status === 410) {
|
|
2925
|
+
return err(serviceError(
|
|
2926
|
+
ErrorCodes.KV_SYNC_RESET_REQUIRED,
|
|
2927
|
+
`${context}: the node requires a reset (${reason ?? "reason not given"}); discard sync state and restart without a cursor`,
|
|
2928
|
+
"kv",
|
|
2929
|
+
{ meta: { ...meta, reason } }
|
|
2930
|
+
));
|
|
2931
|
+
}
|
|
2932
|
+
if (response.status === 403 && bodyCode === "RETENTION_GRANT_REFUSED") {
|
|
2933
|
+
return err(serviceError(
|
|
2934
|
+
ErrorCodes.KV_RETENTION_GRANT_REFUSED,
|
|
2935
|
+
`${context}: the retention grant was refused (${reason ?? "reason not given"})`,
|
|
2936
|
+
"kv",
|
|
2937
|
+
{ meta: { ...meta, reason } }
|
|
2938
|
+
));
|
|
2939
|
+
}
|
|
2940
|
+
if (response.status === 401) {
|
|
2941
|
+
const revoked = revocationCodeOf(errorText);
|
|
2942
|
+
if (revoked !== void 0) {
|
|
2943
|
+
return err(serviceError(revoked, `${context}: 401 - ${errorText}`, "kv", { meta }));
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
if (response.status === 401 || response.status === 403) {
|
|
2947
|
+
return this.authorizationFailure(context, response, errorText, [prefix], KVAction.SYNC);
|
|
2948
|
+
}
|
|
2949
|
+
return err(serviceError(
|
|
2950
|
+
response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR,
|
|
2951
|
+
`${context}: ${response.status} - ${errorText}`,
|
|
2952
|
+
"kv",
|
|
2953
|
+
{ meta }
|
|
2954
|
+
));
|
|
2955
|
+
}
|
|
2956
|
+
normalizeChangesResponse(data) {
|
|
2957
|
+
if (!isJsonObject(data)) return void 0;
|
|
2958
|
+
const { changes, more, cursor, source, authority } = data;
|
|
2959
|
+
if (!Array.isArray(changes) || typeof more !== "boolean" || typeof cursor !== "string" || !isJsonObject(source) || typeof source.nodeDid !== "string" || typeof source.space !== "string" || typeof source.prefix !== "string" || !isJsonObject(authority)) {
|
|
2960
|
+
return void 0;
|
|
2961
|
+
}
|
|
2962
|
+
const { notBefore, expiresAt, retainUntil } = authority;
|
|
2963
|
+
if (notBefore !== null && typeof notBefore !== "string" || expiresAt !== null && typeof expiresAt !== "string" || retainUntil !== null && typeof retainUntil !== "string") {
|
|
2964
|
+
return void 0;
|
|
2965
|
+
}
|
|
2966
|
+
const parsed = [];
|
|
2967
|
+
for (const change of changes) {
|
|
2968
|
+
if (!isJsonObject(change) || typeof change.key !== "string") return void 0;
|
|
2969
|
+
if (change.deleted === true) {
|
|
2970
|
+
parsed.push({ key: change.key, deleted: true });
|
|
2971
|
+
continue;
|
|
2972
|
+
}
|
|
2973
|
+
const { metadata } = change;
|
|
2974
|
+
if (change.deleted !== false || typeof change.etag !== "string" || !isJsonObject(metadata) || !Object.values(metadata).every((value) => typeof value === "string")) {
|
|
2975
|
+
return void 0;
|
|
2976
|
+
}
|
|
2977
|
+
parsed.push({
|
|
2978
|
+
key: change.key,
|
|
2979
|
+
deleted: false,
|
|
2980
|
+
etag: change.etag,
|
|
2981
|
+
metadata
|
|
2982
|
+
});
|
|
2983
|
+
}
|
|
2984
|
+
return {
|
|
2985
|
+
changes: parsed,
|
|
2986
|
+
more,
|
|
2987
|
+
cursor,
|
|
2988
|
+
source: { nodeDid: source.nodeDid, space: source.space, prefix: source.prefix },
|
|
2989
|
+
authority: { notBefore, expiresAt, retainUntil }
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2745
2992
|
/**
|
|
2746
2993
|
* Delete a key.
|
|
2747
2994
|
*/
|
|
@@ -3938,7 +4185,7 @@ var AsyncQueue = class {
|
|
|
3938
4185
|
}
|
|
3939
4186
|
const waiter = this.waiters.shift();
|
|
3940
4187
|
if (waiter) {
|
|
3941
|
-
waiter({ value, done: false });
|
|
4188
|
+
waiter.resolve({ value, done: false });
|
|
3942
4189
|
return;
|
|
3943
4190
|
}
|
|
3944
4191
|
this.values.push(value);
|
|
@@ -3950,7 +4197,22 @@ var AsyncQueue = class {
|
|
|
3950
4197
|
this.closed = true;
|
|
3951
4198
|
while (this.waiters.length > 0) {
|
|
3952
4199
|
const waiter = this.waiters.shift();
|
|
3953
|
-
waiter?.({ value: void 0, done: true });
|
|
4200
|
+
waiter?.resolve({ value: void 0, done: true });
|
|
4201
|
+
}
|
|
4202
|
+
}
|
|
4203
|
+
/**
|
|
4204
|
+
* Fail the queue: pending and future `next()` calls reject with `error`,
|
|
4205
|
+
* so a `for await` consumer sees the iterator throw.
|
|
4206
|
+
*/
|
|
4207
|
+
fail(error) {
|
|
4208
|
+
if (this.closed) {
|
|
4209
|
+
return;
|
|
4210
|
+
}
|
|
4211
|
+
this.closed = true;
|
|
4212
|
+
this.failure = error;
|
|
4213
|
+
while (this.waiters.length > 0) {
|
|
4214
|
+
const waiter = this.waiters.shift();
|
|
4215
|
+
waiter?.reject(error);
|
|
3954
4216
|
}
|
|
3955
4217
|
}
|
|
3956
4218
|
next() {
|
|
@@ -3958,20 +4220,28 @@ var AsyncQueue = class {
|
|
|
3958
4220
|
const value = this.values.shift();
|
|
3959
4221
|
return Promise.resolve({ value, done: false });
|
|
3960
4222
|
}
|
|
4223
|
+
if (this.failure !== void 0) {
|
|
4224
|
+
return Promise.reject(this.failure);
|
|
4225
|
+
}
|
|
3961
4226
|
if (this.closed) {
|
|
3962
4227
|
return Promise.resolve({ value: void 0, done: true });
|
|
3963
4228
|
}
|
|
3964
|
-
return new Promise((resolve) => {
|
|
3965
|
-
this.waiters.push(resolve);
|
|
4229
|
+
return new Promise((resolve, reject) => {
|
|
4230
|
+
this.waiters.push({ resolve, reject });
|
|
3966
4231
|
});
|
|
3967
4232
|
}
|
|
3968
4233
|
};
|
|
4234
|
+
var HOOK_STREAM_RETRY_BASE_DELAY_MS = 250;
|
|
4235
|
+
var HOOK_STREAM_RETRY_MAX_DELAY_MS = 3e4;
|
|
4236
|
+
var HOOK_STREAM_HEALTHY_MS = 5e3;
|
|
3969
4237
|
var HooksService = class extends BaseService {
|
|
3970
4238
|
constructor(config = {}) {
|
|
3971
4239
|
super();
|
|
3972
4240
|
this._subscribers = /* @__PURE__ */ new Set();
|
|
3973
|
-
|
|
3974
|
-
this.
|
|
4241
|
+
/** True while the one supervisor loop runs. Set only by wake(), cleared only at loop exit. */
|
|
4242
|
+
this._supervising = false;
|
|
4243
|
+
/** Bumped on every session change; part of the plan key. */
|
|
4244
|
+
this._sessionGeneration = 0;
|
|
3975
4245
|
this._config = config;
|
|
3976
4246
|
}
|
|
3977
4247
|
get config() {
|
|
@@ -3987,35 +4257,32 @@ var HooksService = class extends BaseService {
|
|
|
3987
4257
|
if (subscriptions.length === 0) {
|
|
3988
4258
|
throw new Error("At least one hook subscription is required");
|
|
3989
4259
|
}
|
|
3990
|
-
const
|
|
4260
|
+
const requested = subscriptions.map(normalizeSubscription);
|
|
3991
4261
|
const subscriber = {
|
|
3992
|
-
requested
|
|
4262
|
+
requested,
|
|
4263
|
+
signatures: requested.map(subscriptionSignature),
|
|
3993
4264
|
ttlSeconds: options.ttlSeconds,
|
|
3994
4265
|
queue: new AsyncQueue()
|
|
3995
4266
|
};
|
|
3996
|
-
|
|
3997
|
-
const abortHandler = () => {
|
|
4267
|
+
const leave = () => {
|
|
3998
4268
|
this._subscribers.delete(subscriber);
|
|
3999
4269
|
subscriber.queue.close();
|
|
4000
|
-
|
|
4270
|
+
this.wake();
|
|
4001
4271
|
};
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
} else {
|
|
4006
|
-
options.signal.addEventListener("abort", abortHandler, { once: true });
|
|
4007
|
-
}
|
|
4272
|
+
const { signal } = options;
|
|
4273
|
+
if (signal?.aborted) {
|
|
4274
|
+
return;
|
|
4008
4275
|
}
|
|
4009
|
-
|
|
4276
|
+
signal?.addEventListener("abort", leave, { once: true });
|
|
4277
|
+
this._subscribers.add(subscriber);
|
|
4278
|
+
this.wake();
|
|
4010
4279
|
try {
|
|
4011
4280
|
for await (const event of subscriber.queue) {
|
|
4012
4281
|
yield event;
|
|
4013
4282
|
}
|
|
4014
4283
|
} finally {
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
}
|
|
4018
|
-
abortHandler();
|
|
4284
|
+
signal?.removeEventListener("abort", leave);
|
|
4285
|
+
leave();
|
|
4019
4286
|
}
|
|
4020
4287
|
}
|
|
4021
4288
|
async register(webhook) {
|
|
@@ -4156,146 +4423,292 @@ var HooksService = class extends BaseService {
|
|
|
4156
4423
|
return err(wrapError("hooks", error));
|
|
4157
4424
|
}
|
|
4158
4425
|
}
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
if (state.signature !== this._activeSignature) {
|
|
4171
|
-
this._activeSignature = state.signature;
|
|
4172
|
-
this.abortSharedStream();
|
|
4426
|
+
/**
|
|
4427
|
+
* Called after anything that may change what the supervisor should do:
|
|
4428
|
+
* subscribe, unsubscribe, session change, sign-out. Interrupts the running
|
|
4429
|
+
* step if the plan moved away from it, and starts the supervisor if none
|
|
4430
|
+
* runs. Touches only SDK-owned state, so it never throws into callers such
|
|
4431
|
+
* as `ServiceContext.setSession`.
|
|
4432
|
+
*/
|
|
4433
|
+
wake() {
|
|
4434
|
+
const phase = this._phase;
|
|
4435
|
+
if (phase && phase.key !== this.plan()?.key) {
|
|
4436
|
+
phase.interrupt.abort();
|
|
4173
4437
|
}
|
|
4174
|
-
if (
|
|
4175
|
-
|
|
4176
|
-
if (!isAbortError(error)) {
|
|
4177
|
-
throw error;
|
|
4178
|
-
}
|
|
4179
|
-
}).finally(() => {
|
|
4180
|
-
this._sharedStreamTask = void 0;
|
|
4181
|
-
this._sharedStreamAbort = void 0;
|
|
4182
|
-
if (this._subscribers.size > 0) {
|
|
4183
|
-
void this.scheduleSharedStreamRefresh();
|
|
4184
|
-
}
|
|
4185
|
-
});
|
|
4438
|
+
if (this._supervising || this._subscribers.size === 0) {
|
|
4439
|
+
return;
|
|
4186
4440
|
}
|
|
4441
|
+
this._supervising = true;
|
|
4442
|
+
this.supervise().catch(() => this.collapse());
|
|
4187
4443
|
}
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4444
|
+
/**
|
|
4445
|
+
* The only owner of the shared stream. Each iteration is one attempt
|
|
4446
|
+
* (mint → open → drain), then a classification and, unless the attempt was
|
|
4447
|
+
* superseded or released its subscribers, a backoff. Every await is a
|
|
4448
|
+
* `phase` that wake() can interrupt; an interrupted phase yields
|
|
4449
|
+
* `undefined`, so a superseded mint can never act. Outside `attempt` and
|
|
4450
|
+
* `pause`, which each own one catch, the loop touches only SDK state.
|
|
4451
|
+
*/
|
|
4452
|
+
async supervise() {
|
|
4453
|
+
let streak = 0;
|
|
4454
|
+
for (; ; ) {
|
|
4455
|
+
const plan = this.plan();
|
|
4456
|
+
if (!plan) {
|
|
4457
|
+
this._supervising = false;
|
|
4458
|
+
return;
|
|
4459
|
+
}
|
|
4460
|
+
const health = { openedAt: 0, delivered: false };
|
|
4461
|
+
const outcome = await this.phase(
|
|
4462
|
+
plan.key,
|
|
4463
|
+
(signal) => this.attempt(plan, signal, health)
|
|
4464
|
+
);
|
|
4465
|
+
if (health.delivered || health.openedAt > 0 && performance.now() - health.openedAt >= HOOK_STREAM_HEALTHY_MS) {
|
|
4466
|
+
streak = 0;
|
|
4467
|
+
}
|
|
4468
|
+
if (!outcome) {
|
|
4469
|
+
continue;
|
|
4194
4470
|
}
|
|
4195
|
-
|
|
4196
|
-
|
|
4471
|
+
if (this.plan()?.key !== plan.key) {
|
|
4472
|
+
continue;
|
|
4473
|
+
}
|
|
4474
|
+
if (outcome.kind === "stopped") {
|
|
4475
|
+
this.release();
|
|
4476
|
+
continue;
|
|
4477
|
+
}
|
|
4478
|
+
if (outcome.kind === "refused" && outcome.terminal) {
|
|
4479
|
+
this.release(outcome.error);
|
|
4480
|
+
this.report(outcome.error);
|
|
4481
|
+
continue;
|
|
4197
4482
|
}
|
|
4483
|
+
if (outcome.kind !== "ended") {
|
|
4484
|
+
this.report(outcome.error);
|
|
4485
|
+
}
|
|
4486
|
+
streak += 1;
|
|
4487
|
+
await this.phase(
|
|
4488
|
+
plan.key,
|
|
4489
|
+
(signal) => this.pause(streak, signal)
|
|
4490
|
+
);
|
|
4491
|
+
}
|
|
4492
|
+
}
|
|
4493
|
+
/**
|
|
4494
|
+
* Run one step under an interrupt that wake() fires when the plan key moves
|
|
4495
|
+
* on. An interrupted step's result is discarded. The interrupt is also
|
|
4496
|
+
* fired when the step returns, which releases whatever it left open: the
|
|
4497
|
+
* request signal and its listeners, an unread response body, a timer.
|
|
4498
|
+
*/
|
|
4499
|
+
async phase(key, step) {
|
|
4500
|
+
const interrupt = new AbortController();
|
|
4501
|
+
this._phase = { key, interrupt };
|
|
4502
|
+
try {
|
|
4503
|
+
if (this.plan()?.key !== key) {
|
|
4504
|
+
interrupt.abort();
|
|
4505
|
+
}
|
|
4506
|
+
const result = await step(interrupt.signal);
|
|
4507
|
+
return interrupt.signal.aborted ? void 0 : result;
|
|
4508
|
+
} finally {
|
|
4509
|
+
this._phase = void 0;
|
|
4510
|
+
interrupt.abort();
|
|
4198
4511
|
}
|
|
4199
|
-
const subscriptions = [...merged.values()].sort(
|
|
4200
|
-
(left, right) => subscriptionSignature(left).localeCompare(subscriptionSignature(right))
|
|
4201
|
-
);
|
|
4202
|
-
const ttlSeconds = ttlCandidates.length > 0 ? Math.min(...ttlCandidates) : void 0;
|
|
4203
|
-
const signature = JSON.stringify({
|
|
4204
|
-
subscriptions: subscriptions.map(subscriptionSignature),
|
|
4205
|
-
ttlSeconds
|
|
4206
|
-
});
|
|
4207
|
-
return {
|
|
4208
|
-
subscriptions,
|
|
4209
|
-
ttlSeconds,
|
|
4210
|
-
signature
|
|
4211
|
-
};
|
|
4212
4512
|
}
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4513
|
+
/**
|
|
4514
|
+
* One mint → open → drain. The single catch site for everything the
|
|
4515
|
+
* attempt touches that the SDK does not own: context accessors, invoke,
|
|
4516
|
+
* fetch, response bodies, the SSE parser. Never rejects.
|
|
4517
|
+
*/
|
|
4518
|
+
async attempt(plan, signal, health) {
|
|
4519
|
+
let phase = "mint";
|
|
4520
|
+
let request;
|
|
4521
|
+
const owns = () => !signal.aborted && !request?.signal.aborted && this.plan()?.key === plan.key;
|
|
4216
4522
|
try {
|
|
4217
|
-
const
|
|
4218
|
-
const
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4523
|
+
const context = this.context;
|
|
4524
|
+
const contextSignal = context.abortSignal;
|
|
4525
|
+
this._lastContextSignal = contextSignal;
|
|
4526
|
+
if (!context.isAuthenticated || contextSignal?.aborted) {
|
|
4527
|
+
return { kind: "stopped" };
|
|
4528
|
+
}
|
|
4529
|
+
request = this.createRequestSignal(signal, 0);
|
|
4530
|
+
const host = this.host;
|
|
4531
|
+
const minted = await awaitOwned(
|
|
4532
|
+
request.signal,
|
|
4533
|
+
() => context.fetch(`${host}/hooks/tickets`, {
|
|
4534
|
+
method: "POST",
|
|
4535
|
+
headers: {
|
|
4536
|
+
...serviceHeadersToRecord(this.createInvokeHeaders(plan.subscriptions)),
|
|
4537
|
+
"content-type": "application/json"
|
|
4538
|
+
},
|
|
4539
|
+
body: JSON.stringify({
|
|
4540
|
+
subscriptions: plan.subscriptions,
|
|
4541
|
+
ttlSeconds: plan.ttlSeconds
|
|
4542
|
+
}),
|
|
4543
|
+
signal: request.signal
|
|
4544
|
+
})
|
|
4222
4545
|
);
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4546
|
+
if (!owns()) return { kind: "stopped" };
|
|
4547
|
+
if (!minted.ok) {
|
|
4548
|
+
const status = minted.status;
|
|
4549
|
+
const error = await awaitOwned(
|
|
4550
|
+
request.signal,
|
|
4551
|
+
() => responseError("hooks", "failed to mint hook ticket", minted)
|
|
4552
|
+
);
|
|
4553
|
+
if (!owns()) return { kind: "stopped" };
|
|
4554
|
+
return { kind: "refused", terminal: isTerminalMintRefusal(status), error };
|
|
4555
|
+
}
|
|
4556
|
+
const body = await awaitOwned(request.signal, () => minted.json());
|
|
4557
|
+
if (!owns()) return { kind: "stopped" };
|
|
4558
|
+
const ticket = body !== null && typeof body === "object" && "ticket" in body ? body.ticket : void 0;
|
|
4559
|
+
if (typeof ticket !== "string" || ticket.length === 0) {
|
|
4560
|
+
throw new Error("Hook ticket response did not include a ticket");
|
|
4561
|
+
}
|
|
4562
|
+
phase = "open";
|
|
4563
|
+
if (!owns()) return { kind: "stopped" };
|
|
4564
|
+
const stream = await awaitOwned(
|
|
4565
|
+
request.signal,
|
|
4566
|
+
() => context.fetch(`${host}/hooks/events?ticket=${encodeURIComponent(ticket)}`, {
|
|
4567
|
+
method: "GET",
|
|
4568
|
+
headers: { accept: "text/event-stream" },
|
|
4569
|
+
signal: request.signal
|
|
4570
|
+
})
|
|
4227
4571
|
);
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
if (
|
|
4238
|
-
|
|
4239
|
-
|
|
4572
|
+
if (!owns()) return { kind: "stopped" };
|
|
4573
|
+
if (!stream.ok) {
|
|
4574
|
+
return { kind: "refused", terminal: false, error: hookStreamRefusal(stream.status) };
|
|
4575
|
+
}
|
|
4576
|
+
health.openedAt = performance.now();
|
|
4577
|
+
const iterator = parseSseStream(stream.body, request.signal)[Symbol.asyncIterator]();
|
|
4578
|
+
try {
|
|
4579
|
+
for (; ; ) {
|
|
4580
|
+
const next = await awaitOwned(request.signal, () => iterator.next());
|
|
4581
|
+
if (!owns()) return { kind: "stopped" };
|
|
4582
|
+
if (next.done) break;
|
|
4583
|
+
const message = next.value;
|
|
4584
|
+
if (!message.data) continue;
|
|
4585
|
+
const event = parseHookEvent(message);
|
|
4586
|
+
if (!owns()) return { kind: "stopped" };
|
|
4587
|
+
this.dispatch(event);
|
|
4588
|
+
health.delivered = true;
|
|
4240
4589
|
}
|
|
4590
|
+
} finally {
|
|
4591
|
+
const closing = iterator.return?.();
|
|
4592
|
+
if (closing) void Promise.resolve(closing).then(() => void 0, () => void 0);
|
|
4241
4593
|
}
|
|
4594
|
+
return owns() ? { kind: "ended" } : { kind: "stopped" };
|
|
4595
|
+
} catch (thrown) {
|
|
4596
|
+
return request?.signal.aborted || thrown === SUPERSEDED ? { kind: "stopped" } : { kind: "failed", error: hookStreamFailure(phase, thrown) };
|
|
4597
|
+
}
|
|
4598
|
+
}
|
|
4599
|
+
/**
|
|
4600
|
+
* Back off before the next attempt. `config.streamRetry` overrides the
|
|
4601
|
+
* delay and the clock; whatever they do, the pause ends by the delay or the
|
|
4602
|
+
* interrupt, then yields one timer turn so an instant clock cannot starve
|
|
4603
|
+
* due timers or the process.
|
|
4604
|
+
*/
|
|
4605
|
+
async pause(attempt, signal) {
|
|
4606
|
+
try {
|
|
4607
|
+
this._lastContextSignal = this.context.abortSignal;
|
|
4608
|
+
} catch {
|
|
4609
|
+
}
|
|
4610
|
+
const contextSignal = this._lastContextSignal;
|
|
4611
|
+
const waits = [];
|
|
4612
|
+
const aborts = [];
|
|
4613
|
+
try {
|
|
4614
|
+
const retry = this._config.streamRetry;
|
|
4615
|
+
const delayMs = retry?.delay?.(attempt) ?? hookStreamRetryDelay(attempt);
|
|
4616
|
+
waits.push((retry?.wait ?? sleep)(delayMs, attempt, signal));
|
|
4617
|
+
} catch {
|
|
4618
|
+
waits.push(Promise.resolve());
|
|
4619
|
+
}
|
|
4620
|
+
aborts.push(abortPromise(signal));
|
|
4621
|
+
if (contextSignal) aborts.push(abortPromise(contextSignal));
|
|
4622
|
+
try {
|
|
4623
|
+
await Promise.race([...waits, ...aborts.map(({ promise }) => promise)]);
|
|
4624
|
+
} catch {
|
|
4242
4625
|
} finally {
|
|
4243
|
-
|
|
4244
|
-
|
|
4626
|
+
for (const abort of aborts) abort.dispose();
|
|
4627
|
+
}
|
|
4628
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
4629
|
+
}
|
|
4630
|
+
/** The stream the current subscribers need, or undefined when none remain. */
|
|
4631
|
+
plan() {
|
|
4632
|
+
if (this._subscribers.size === 0) {
|
|
4633
|
+
return void 0;
|
|
4634
|
+
}
|
|
4635
|
+
const merged = /* @__PURE__ */ new Map();
|
|
4636
|
+
let ttlSeconds;
|
|
4637
|
+
for (const subscriber of this._subscribers) {
|
|
4638
|
+
subscriber.requested.forEach(
|
|
4639
|
+
(subscription, index) => merged.set(subscriber.signatures[index], subscription)
|
|
4640
|
+
);
|
|
4641
|
+
if (typeof subscriber.ttlSeconds === "number") {
|
|
4642
|
+
ttlSeconds = Math.min(subscriber.ttlSeconds, ttlSeconds ?? Infinity);
|
|
4245
4643
|
}
|
|
4246
4644
|
}
|
|
4645
|
+
const signatures = [...merged.keys()].sort();
|
|
4646
|
+
return {
|
|
4647
|
+
key: JSON.stringify([this._sessionGeneration, signatures, ttlSeconds ?? null]),
|
|
4648
|
+
subscriptions: signatures.map((signature) => merged.get(signature)),
|
|
4649
|
+
ttlSeconds
|
|
4650
|
+
};
|
|
4247
4651
|
}
|
|
4248
|
-
|
|
4249
|
-
this.
|
|
4652
|
+
dispatch(event) {
|
|
4653
|
+
for (const subscriber of this._subscribers) {
|
|
4654
|
+
if (matchesAnySubscription(event, subscriber.requested)) {
|
|
4655
|
+
subscriber.queue.push(event);
|
|
4656
|
+
}
|
|
4657
|
+
}
|
|
4250
4658
|
}
|
|
4251
|
-
|
|
4252
|
-
|
|
4659
|
+
/**
|
|
4660
|
+
* Detach every subscriber. With `error`, their iterators throw it after
|
|
4661
|
+
* draining buffered events; without, they complete.
|
|
4662
|
+
*/
|
|
4663
|
+
release(error) {
|
|
4664
|
+
const subscribers = [...this._subscribers];
|
|
4665
|
+
this._subscribers.clear();
|
|
4666
|
+
for (const { queue } of subscribers) {
|
|
4667
|
+
if (error) {
|
|
4668
|
+
queue.fail(error);
|
|
4669
|
+
} else {
|
|
4670
|
+
queue.close();
|
|
4671
|
+
}
|
|
4672
|
+
}
|
|
4253
4673
|
}
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
const ticketResponse = await this.context.fetch(`${host}/hooks/tickets`, {
|
|
4258
|
-
method: "POST",
|
|
4259
|
-
headers: {
|
|
4260
|
-
...serviceHeadersToRecord(headers),
|
|
4261
|
-
"content-type": "application/json"
|
|
4262
|
-
},
|
|
4263
|
-
body: JSON.stringify({
|
|
4264
|
-
subscriptions,
|
|
4265
|
-
ttlSeconds
|
|
4266
|
-
}),
|
|
4267
|
-
signal
|
|
4268
|
-
});
|
|
4269
|
-
if (!ticketResponse.ok) {
|
|
4270
|
-
throw await responseError(
|
|
4271
|
-
"hooks",
|
|
4272
|
-
"failed to mint hook ticket",
|
|
4273
|
-
ticketResponse
|
|
4274
|
-
);
|
|
4674
|
+
report(error) {
|
|
4675
|
+
if (error.meta && typeof error.meta === "object") {
|
|
4676
|
+
Object.freeze(error.meta);
|
|
4275
4677
|
}
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4678
|
+
Object.freeze(error);
|
|
4679
|
+
try {
|
|
4680
|
+
this.emitError(error, "stream");
|
|
4681
|
+
} catch {
|
|
4279
4682
|
}
|
|
4280
|
-
return ticketJson;
|
|
4281
4683
|
}
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4684
|
+
/**
|
|
4685
|
+
* Unreachable by construction. If the loop ever rejects anyway, fail its
|
|
4686
|
+
* subscribers visibly instead of stranding them or crashing the process.
|
|
4687
|
+
*/
|
|
4688
|
+
collapse() {
|
|
4689
|
+
this._supervising = false;
|
|
4690
|
+
this._phase?.interrupt.abort();
|
|
4691
|
+
this._phase = void 0;
|
|
4692
|
+
const error = serviceError(
|
|
4693
|
+
ErrorCodes.NETWORK_ERROR,
|
|
4694
|
+
"hook stream supervisor stopped unexpectedly",
|
|
4695
|
+
"hooks"
|
|
4290
4696
|
);
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4697
|
+
this.report(error);
|
|
4698
|
+
this.release(error);
|
|
4699
|
+
}
|
|
4700
|
+
onSessionChange(session) {
|
|
4701
|
+
super.onSessionChange(session);
|
|
4702
|
+
this._sessionGeneration += 1;
|
|
4703
|
+
this.wake();
|
|
4704
|
+
}
|
|
4705
|
+
onSignOut() {
|
|
4706
|
+
super.onSignOut();
|
|
4707
|
+
this.release();
|
|
4708
|
+
this.wake();
|
|
4709
|
+
}
|
|
4710
|
+
createHookHeaders(action, path) {
|
|
4711
|
+
return this.context.invoke(this.session, "hooks", path, action);
|
|
4299
4712
|
}
|
|
4300
4713
|
createInvokeHeaders(subscriptions) {
|
|
4301
4714
|
const entries = subscriptions.map((subscription) => ({
|
|
@@ -4326,11 +4739,168 @@ function buildScopePath(service, pathPrefix) {
|
|
|
4326
4739
|
const normalized = normalizePathPrefix(pathPrefix);
|
|
4327
4740
|
return normalized ? `${service}/${normalized}` : service;
|
|
4328
4741
|
}
|
|
4742
|
+
function hookStreamRetryDelay(attempt) {
|
|
4743
|
+
const cap = Math.min(
|
|
4744
|
+
HOOK_STREAM_RETRY_BASE_DELAY_MS * 2 ** Math.max(0, attempt - 1),
|
|
4745
|
+
HOOK_STREAM_RETRY_MAX_DELAY_MS
|
|
4746
|
+
);
|
|
4747
|
+
return cap / 2 + Math.random() * (cap / 2);
|
|
4748
|
+
}
|
|
4749
|
+
function isTerminalMintRefusal(status) {
|
|
4750
|
+
return typeof status === "number" && status >= 400 && status < 500 && status !== 408 && status !== 429;
|
|
4751
|
+
}
|
|
4752
|
+
var SUPERSEDED = /* @__PURE__ */ Symbol("hooks attempt superseded");
|
|
4753
|
+
async function awaitOwned(signal, operation) {
|
|
4754
|
+
if (signal.aborted) {
|
|
4755
|
+
if (typeof operation !== "function") {
|
|
4756
|
+
void operation.then(cancelLateResponse, () => void 0);
|
|
4757
|
+
}
|
|
4758
|
+
throw SUPERSEDED;
|
|
4759
|
+
}
|
|
4760
|
+
const promise = typeof operation === "function" ? operation() : operation;
|
|
4761
|
+
return new Promise((resolve, reject) => {
|
|
4762
|
+
let settled = false;
|
|
4763
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
4764
|
+
const onAbort = () => {
|
|
4765
|
+
if (settled) return;
|
|
4766
|
+
settled = true;
|
|
4767
|
+
cleanup();
|
|
4768
|
+
reject(SUPERSEDED);
|
|
4769
|
+
};
|
|
4770
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
4771
|
+
promise.then(
|
|
4772
|
+
(value) => {
|
|
4773
|
+
if (settled) {
|
|
4774
|
+
cancelLateResponse(value);
|
|
4775
|
+
return;
|
|
4776
|
+
}
|
|
4777
|
+
settled = true;
|
|
4778
|
+
cleanup();
|
|
4779
|
+
resolve(value);
|
|
4780
|
+
},
|
|
4781
|
+
(error) => {
|
|
4782
|
+
if (settled) return;
|
|
4783
|
+
settled = true;
|
|
4784
|
+
cleanup();
|
|
4785
|
+
reject(error);
|
|
4786
|
+
}
|
|
4787
|
+
);
|
|
4788
|
+
});
|
|
4789
|
+
}
|
|
4790
|
+
function cancelLateResponse(value) {
|
|
4791
|
+
if (!(value instanceof Response) || !value.body) return;
|
|
4792
|
+
try {
|
|
4793
|
+
const cancellation = value.body.cancel();
|
|
4794
|
+
void cancellation.then(() => void 0, () => void 0);
|
|
4795
|
+
} catch {
|
|
4796
|
+
}
|
|
4797
|
+
}
|
|
4798
|
+
function hookStreamRefusal(status) {
|
|
4799
|
+
const code = typeof status === "number" ? status : void 0;
|
|
4800
|
+
return serviceError(
|
|
4801
|
+
ErrorCodes.NETWORK_ERROR,
|
|
4802
|
+
`hook stream open refused: ${code ?? "unknown status"}`,
|
|
4803
|
+
"hooks",
|
|
4804
|
+
{ meta: { phase: "open", status: code } }
|
|
4805
|
+
);
|
|
4806
|
+
}
|
|
4807
|
+
var STREAM_ERROR_NAMES = /* @__PURE__ */ new Set([
|
|
4808
|
+
"Error",
|
|
4809
|
+
"TypeError",
|
|
4810
|
+
"AbortError",
|
|
4811
|
+
"TimeoutError",
|
|
4812
|
+
"SyntaxError",
|
|
4813
|
+
"RangeError",
|
|
4814
|
+
"NetworkError"
|
|
4815
|
+
]);
|
|
4816
|
+
function hookStreamFailure(phase, thrown) {
|
|
4817
|
+
const candidateName = readProperty(thrown, "name");
|
|
4818
|
+
const name = typeof candidateName === "string" && STREAM_ERROR_NAMES.has(candidateName) ? candidateName : "Error";
|
|
4819
|
+
const code = identifier(readProperty(thrown, "code")) ?? identifier(readProperty(readProperty(thrown, "cause"), "code"));
|
|
4820
|
+
return serviceError(
|
|
4821
|
+
ErrorCodes.NETWORK_ERROR,
|
|
4822
|
+
`hook stream ${phase} failed: ${name}${code ? ` (${code})` : ""}`,
|
|
4823
|
+
"hooks",
|
|
4824
|
+
{ meta: { phase, errorName: name, ...code ? { errorCode: code } : {} } }
|
|
4825
|
+
);
|
|
4826
|
+
}
|
|
4827
|
+
var STREAM_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
4828
|
+
"ECONNREFUSED",
|
|
4829
|
+
"ECONNRESET",
|
|
4830
|
+
"ECONNABORTED",
|
|
4831
|
+
"ETIMEDOUT",
|
|
4832
|
+
"EPIPE",
|
|
4833
|
+
"ENOTFOUND",
|
|
4834
|
+
"EAI_AGAIN",
|
|
4835
|
+
"EHOSTUNREACH",
|
|
4836
|
+
"ENETUNREACH",
|
|
4837
|
+
"ENETDOWN",
|
|
4838
|
+
"EADDRNOTAVAIL",
|
|
4839
|
+
"UND_ERR_SOCKET",
|
|
4840
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
4841
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
4842
|
+
"UND_ERR_BODY_TIMEOUT",
|
|
4843
|
+
"UND_ERR_CLOSED",
|
|
4844
|
+
"UND_ERR_ABORTED",
|
|
4845
|
+
"UND_ERR_RESPONSE_STATUS_CODE",
|
|
4846
|
+
"ERR_TLS_CERT_ALTNAME_INVALID",
|
|
4847
|
+
"CERT_HAS_EXPIRED",
|
|
4848
|
+
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
|
4849
|
+
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
|
4850
|
+
"SELF_SIGNED_CERT_IN_CHAIN",
|
|
4851
|
+
"ERR_SSL_WRONG_VERSION_NUMBER"
|
|
4852
|
+
]);
|
|
4853
|
+
function identifier(value) {
|
|
4854
|
+
return typeof value === "string" && STREAM_ERROR_CODES.has(value) ? value : void 0;
|
|
4855
|
+
}
|
|
4856
|
+
function readProperty(value, key) {
|
|
4857
|
+
if (value === null || typeof value !== "object" && typeof value !== "function") {
|
|
4858
|
+
return void 0;
|
|
4859
|
+
}
|
|
4860
|
+
try {
|
|
4861
|
+
return Reflect.get(value, key);
|
|
4862
|
+
} catch {
|
|
4863
|
+
return void 0;
|
|
4864
|
+
}
|
|
4865
|
+
}
|
|
4866
|
+
function sleep(delayMs, _attempt, signal) {
|
|
4867
|
+
return new Promise((resolve) => {
|
|
4868
|
+
const finish = () => {
|
|
4869
|
+
clearTimeout(timer);
|
|
4870
|
+
signal.removeEventListener("abort", stop);
|
|
4871
|
+
resolve();
|
|
4872
|
+
};
|
|
4873
|
+
const timer = setTimeout(finish, delayMs);
|
|
4874
|
+
timer.unref?.();
|
|
4875
|
+
const stop = () => finish();
|
|
4876
|
+
if (signal.aborted) {
|
|
4877
|
+
finish();
|
|
4878
|
+
} else {
|
|
4879
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
4880
|
+
}
|
|
4881
|
+
});
|
|
4882
|
+
}
|
|
4883
|
+
function abortPromise(signal) {
|
|
4884
|
+
let resolve;
|
|
4885
|
+
const promise = new Promise((done) => {
|
|
4886
|
+
resolve = done;
|
|
4887
|
+
});
|
|
4888
|
+
const onAbort = () => resolve();
|
|
4889
|
+
if (signal.aborted) {
|
|
4890
|
+
resolve();
|
|
4891
|
+
} else {
|
|
4892
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
4893
|
+
}
|
|
4894
|
+
return {
|
|
4895
|
+
promise,
|
|
4896
|
+
dispose: () => signal.removeEventListener("abort", onAbort)
|
|
4897
|
+
};
|
|
4898
|
+
}
|
|
4329
4899
|
function normalizeSubscription(subscription) {
|
|
4330
4900
|
return {
|
|
4331
4901
|
...subscription,
|
|
4332
4902
|
pathPrefix: normalizePathPrefix(subscription.pathPrefix),
|
|
4333
|
-
abilities: subscription.abilities
|
|
4903
|
+
abilities: subscription.abilities ? [...subscription.abilities] : []
|
|
4334
4904
|
};
|
|
4335
4905
|
}
|
|
4336
4906
|
function subscriptionSignature(subscription) {
|
|
@@ -4396,9 +4966,6 @@ async function responseError(service, message, response) {
|
|
|
4396
4966
|
meta: { ...error.meta, status: response.status, statusText: response.statusText }
|
|
4397
4967
|
};
|
|
4398
4968
|
}
|
|
4399
|
-
function isAbortError(error) {
|
|
4400
|
-
return error instanceof DOMException && error.name === "AbortError";
|
|
4401
|
-
}
|
|
4402
4969
|
async function* parseSseStream(body, signal) {
|
|
4403
4970
|
if (!body) {
|
|
4404
4971
|
throw new Error("Hook stream response does not expose a readable body");
|
|
@@ -4427,11 +4994,24 @@ async function* parseSseStream(body, signal) {
|
|
|
4427
4994
|
async function* readBodyChunks(body, signal) {
|
|
4428
4995
|
const asyncIterable = body;
|
|
4429
4996
|
if (typeof asyncIterable?.[Symbol.asyncIterator] === "function") {
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4997
|
+
const iterator = asyncIterable[Symbol.asyncIterator]();
|
|
4998
|
+
const cancel = () => {
|
|
4999
|
+
try {
|
|
5000
|
+
const closing = iterator.return?.();
|
|
5001
|
+
if (closing) void Promise.resolve(closing).then(() => void 0, () => void 0);
|
|
5002
|
+
} catch {
|
|
4433
5003
|
}
|
|
4434
|
-
|
|
5004
|
+
};
|
|
5005
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
5006
|
+
try {
|
|
5007
|
+
while (!signal?.aborted) {
|
|
5008
|
+
const next = await iterator.next();
|
|
5009
|
+
if (next.done) break;
|
|
5010
|
+
yield next.value;
|
|
5011
|
+
}
|
|
5012
|
+
} finally {
|
|
5013
|
+
signal?.removeEventListener("abort", cancel);
|
|
5014
|
+
cancel();
|
|
4435
5015
|
}
|
|
4436
5016
|
return;
|
|
4437
5017
|
}
|
|
@@ -4440,21 +5020,23 @@ async function* readBodyChunks(body, signal) {
|
|
|
4440
5020
|
throw new Error("Unsupported hook stream body type");
|
|
4441
5021
|
}
|
|
4442
5022
|
const reader = stream.getReader();
|
|
5023
|
+
const cancelReader = () => {
|
|
5024
|
+
try {
|
|
5025
|
+
const cancellation = reader.cancel?.();
|
|
5026
|
+
if (cancellation) void cancellation.then(() => void 0, () => void 0);
|
|
5027
|
+
} catch {
|
|
5028
|
+
}
|
|
5029
|
+
};
|
|
5030
|
+
signal?.addEventListener("abort", cancelReader, { once: true });
|
|
4443
5031
|
try {
|
|
4444
5032
|
while (!signal?.aborted) {
|
|
4445
5033
|
const { done, value } = await reader.read();
|
|
4446
|
-
if (done)
|
|
4447
|
-
|
|
4448
|
-
}
|
|
4449
|
-
if (value) {
|
|
4450
|
-
yield value;
|
|
4451
|
-
}
|
|
5034
|
+
if (done) break;
|
|
5035
|
+
if (value) yield value;
|
|
4452
5036
|
}
|
|
4453
5037
|
} finally {
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
} catch {
|
|
4457
|
-
}
|
|
5038
|
+
signal?.removeEventListener("abort", cancelReader);
|
|
5039
|
+
cancelReader();
|
|
4458
5040
|
reader.releaseLock?.();
|
|
4459
5041
|
}
|
|
4460
5042
|
}
|
|
@@ -6289,6 +6871,7 @@ var SecretsService = class {
|
|
|
6289
6871
|
}
|
|
6290
6872
|
async listAll() {
|
|
6291
6873
|
const entries = /* @__PURE__ */ new Map();
|
|
6874
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
6292
6875
|
let cursor;
|
|
6293
6876
|
do {
|
|
6294
6877
|
const result = await this.vault.listPage({
|
|
@@ -6311,6 +6894,14 @@ var SecretsService = class {
|
|
|
6311
6894
|
"Secret catalog listing was truncated without a continuation cursor."
|
|
6312
6895
|
);
|
|
6313
6896
|
}
|
|
6897
|
+
if (cursor !== void 0) {
|
|
6898
|
+
if (seenCursors.has(cursor)) {
|
|
6899
|
+
return invalidSecretInput(
|
|
6900
|
+
"Secret catalog listing repeated a continuation cursor; the node does not support paged KV lists."
|
|
6901
|
+
);
|
|
6902
|
+
}
|
|
6903
|
+
seenCursors.add(cursor);
|
|
6904
|
+
}
|
|
6314
6905
|
} while (cursor !== void 0);
|
|
6315
6906
|
return {
|
|
6316
6907
|
ok: true,
|
|
@@ -7089,10 +7680,10 @@ var EncryptionService = class extends BaseService {
|
|
|
7089
7680
|
get crypto() {
|
|
7090
7681
|
return this._config.crypto;
|
|
7091
7682
|
}
|
|
7092
|
-
async discoverNetwork(
|
|
7683
|
+
async discoverNetwork(identifier2, ownerDid) {
|
|
7093
7684
|
this.assertActive();
|
|
7094
7685
|
const result = await discoverNetwork({
|
|
7095
|
-
identifier,
|
|
7686
|
+
identifier: identifier2,
|
|
7096
7687
|
...ownerDid !== void 0 ? { ownerDid } : {},
|
|
7097
7688
|
...this._config.node !== void 0 ? { node: this._config.node } : {},
|
|
7098
7689
|
...this._config.wellKnown !== void 0 ? { wellKnown: this._config.wellKnown } : {}
|