@tinycloud/replica 0.1.0-beta.3 → 0.1.1-beta.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.
@@ -6852,7 +6852,12 @@ var TelemetryEvents = {
6852
6852
  SERVICE_ERROR: "service.error",
6853
6853
  SERVICE_RETRY: "service.retry",
6854
6854
  SESSION_CHANGED: "session.changed",
6855
- SESSION_EXPIRED: "session.expired"
6855
+ SESSION_EXPIRED: "session.expired",
6856
+ REPLICATION_READ: "replication.read",
6857
+ REPLICATION_WRITE: "replication.write",
6858
+ REPLICATION_SYNC: "replication.sync",
6859
+ REPLICATION_STATE: "replication.state",
6860
+ REPLICATION_DIVERGENCE: "replication.divergence"
6856
6861
  };
6857
6862
  function ok(data) {
6858
6863
  return { ok: true, data };
@@ -6876,9 +6881,34 @@ var SAFE_NUMBER_FIELDS = /* @__PURE__ */ new Set([
6876
6881
  "endedAt",
6877
6882
  "startedAt",
6878
6883
  "status",
6879
- "timestamp"
6884
+ "timestamp",
6885
+ "latencyMs",
6886
+ "stalenessMs",
6887
+ "count",
6888
+ "changes",
6889
+ "deleted",
6890
+ "fetched",
6891
+ "pages",
6892
+ "lagMs",
6893
+ "pendingCleared"
6880
6894
  ]);
6881
6895
  var SAFE_BOOLEAN_FIELDS = /* @__PURE__ */ new Set(["authenticated", "ok", "persisted"]);
6896
+ var SAFE_REPLICATION_FIELDS = {
6897
+ source: ["replica", "network", "none"],
6898
+ reason: ["hit", "absent", "deleted", "content_missing", "coverage_incomplete", "not_covered", "pending_write", "grant_missing", "grant_expired", "grant_revoked", "grant_not_yet_valid", "stale", "replica_error", "runtime_unsupported", "replica_unavailable", "unsupported_option", "network_cursor", "cursor_restart", "aborted", "REPLICA_BEHIND_OWN_WRITES", "REPLICA_UNPROVEN_SINCE_START", "NETWORK_REQUESTED"],
6899
+ op: ["get", "list", "put", "delete", "batchPut"],
6900
+ trigger: ["start", "interval", "stale_read", "manual"],
6901
+ outcome: ["found", "not_found", "error", "committed", "failed", "ambiguous", "ok", "busy", "aborted"],
6902
+ class: ["offline", "authority", "storage", "node"],
6903
+ state: ["opened", "grant_installed", "grant_missing", "runtime_unsupported", "unavailable", "revoked", "recreated", "purged", "purge_failed", "pending_store_error", "pinned", "pending_cleared", "closed", "idle", "ready"],
6904
+ kind: ["value", "missing_local", "extra_local", "keys", "order"],
6905
+ coverage: ["empty", "bootstrapping", "complete"],
6906
+ authority: ["valid", "expired", "revoked", "not-yet-valid"],
6907
+ code: ["NETWORK_ERROR", "TIMEOUT", "ABORTED", "REPLICA_BUSY", "REPLICA_BEHIND_OWN_WRITES", "REPLICA_UNPROVEN_SINCE_START", "NETWORK_REQUESTED", "INTEGRITY_ERROR", "STORAGE_ERROR", "REPLICA_CLOSED", "RUNTIME_UNSUPPORTED", "GRANT_EXPIRED", "GRANT_REVOKED", "GRANT_NOT_YET_VALID", "GRANT_MISSING", "GRANT_INVALID", "SOURCE_CHANGED", "PROTOCOL_ERROR", "SCOPE_VIOLATION", "NODE_ERROR", "CONTENT_MISMATCH", "STORAGE_FULL"],
6908
+ pendingState: ["in_flight", "committed", "ambiguous"],
6909
+ strategy: ["session", "minted", "installed"],
6910
+ verify: ["match", "diverged", "error", "aborted"]
6911
+ };
6882
6912
  function finiteNumber(value) {
6883
6913
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
6884
6914
  }
@@ -6915,6 +6945,12 @@ function projectDiagnosticData(value) {
6915
6945
  const boolean = read(value, key);
6916
6946
  if (typeof boolean === "boolean") projected[key] = boolean;
6917
6947
  }
6948
+ for (const [key, allowed] of Object.entries(SAFE_REPLICATION_FIELDS)) {
6949
+ const candidate = read(value, key);
6950
+ if (typeof candidate === "string" && allowed.includes(candidate)) {
6951
+ projected[key] = candidate;
6952
+ }
6953
+ }
6918
6954
  const url = read(value, "url");
6919
6955
  if (url !== void 0) projected.url = REDACTED;
6920
6956
  const error = read(value, "error");
@@ -7412,6 +7448,12 @@ var ServiceContext = class {
7412
7448
  get abortSignal() {
7413
7449
  return this._abortController.signal;
7414
7450
  }
7451
+ get operationAbortSignal() {
7452
+ return this._operationAbortSignal;
7453
+ }
7454
+ setOperationAbortSignal(signal) {
7455
+ this._operationAbortSignal = signal;
7456
+ }
7415
7457
  /**
7416
7458
  * Permanently retire this graph after its owner installs a replacement.
7417
7459
  * Unlike `abort()`, retirement never creates a fresh controller, so captured
@@ -8086,6 +8128,7 @@ var BaseService = class {
8086
8128
  const parents = [
8087
8129
  this.abortController.signal,
8088
8130
  this.context?.abortSignal,
8131
+ this.context?.operationAbortSignal,
8089
8132
  signal
8090
8133
  ].filter((parent) => parent !== void 0);
8091
8134
  for (const parent of parents) {
@@ -8325,14 +8368,28 @@ var KVService = class extends BaseService {
8325
8368
  */
8326
8369
  constructor(config = {}) {
8327
8370
  super();
8371
+ this.readThrough = null;
8372
+ this.readThroughReady = null;
8328
8373
  this._config = config;
8329
8374
  }
8375
+ /** Delay KV routing until an asynchronously loaded read-through is attached. */
8376
+ setReadThroughReady(ready) {
8377
+ const settled = ready.then(() => void 0, () => void 0);
8378
+ this.readThroughReady = settled;
8379
+ void settled.then(() => {
8380
+ if (this.readThroughReady === settled) this.readThroughReady = null;
8381
+ });
8382
+ }
8330
8383
  /**
8331
8384
  * Get the service configuration.
8332
8385
  */
8333
8386
  get config() {
8334
8387
  return this._config;
8335
8388
  }
8389
+ /** Install or remove the optional replication read-through policy. */
8390
+ setReadThrough(readThrough) {
8391
+ this.readThrough = readThrough;
8392
+ }
8336
8393
  handleQuotaErrorResponse(response, errorText, key) {
8337
8394
  if (response.status !== 402 && response.status !== 413) {
8338
8395
  return void 0;
@@ -8586,6 +8643,7 @@ var KVService = class extends BaseService {
8586
8643
  return aligned;
8587
8644
  }
8588
8645
  async batchRead(keys, action, options) {
8646
+ if (this.readThroughReady !== null) await this.readThroughReady;
8589
8647
  if (!this.requireAuth()) return err(authRequiredError("kv"));
8590
8648
  if (keys.length === 0) return ok({ results: [], count: 0 });
8591
8649
  if (keys.length > MAX_KV_BATCH_READ_ITEMS) {
@@ -8836,6 +8894,7 @@ var KVService = class extends BaseService {
8836
8894
  */
8837
8895
  async get(key, options) {
8838
8896
  return this.withTelemetry("get", key, async () => {
8897
+ if (this.readThroughReady !== null) await this.readThroughReady;
8839
8898
  if (!this.requireAuth()) {
8840
8899
  return err(authRequiredError("kv"));
8841
8900
  }
@@ -8849,64 +8908,38 @@ var KVService = class extends BaseService {
8849
8908
  const path = this.getFullPath(key, options?.prefix);
8850
8909
  const request = this.createRequestSignal(options?.signal, options?.timeout);
8851
8910
  try {
8852
- const response = await this.invokeOperation(
8853
- path,
8854
- KVAction.GET,
8855
- void 0,
8856
- request.signal,
8857
- options?.maxResponseBytes === void 0 ? void 0 : { "x-tinycloud-max-response-bytes": String(options.maxResponseBytes) }
8858
- );
8859
- if (!response.ok) {
8860
- if (response.status === 401 || response.status === 403) {
8861
- const errorText2 = await this.readAuthorizationText(response);
8862
- const permissionHint = parsePermissionHintFromErrorText(errorText2);
8863
- const structuredCapability = permissionHint?.service === "tinycloud.kv" && permissionHint.space === this.context.session.spaceId && permissionHint.path === path ? validatedCapabilityOf({
8864
- service: "kv",
8865
- code: ErrorCodes.AUTH_UNAUTHORIZED,
8866
- meta: {
8867
- status: response.status,
8868
- resource: `${this.context.session.spaceId}/kv/${path}`,
8869
- requiredAction: KVAction.GET
8870
- }
8871
- }) : void 0;
8872
- return this.authorizationFailure(
8873
- `Failed to get key ${JSON.stringify(key)}`,
8874
- response,
8875
- errorText2,
8876
- [path],
8877
- KVAction.GET,
8878
- structuredCapability ? { permissionHint, ...structuredCapability } : {}
8879
- );
8880
- }
8881
- if (response.status === 404) {
8882
- return this.classifyNotFound(response, key);
8883
- }
8884
- const errorText = await response.text();
8885
- if (response.status === 413) {
8886
- return err(serviceError(
8887
- ErrorCodes.KV_RESPONSE_TOO_LARGE,
8888
- `KV value at key ${JSON.stringify(key)} exceeds the requested response limit`,
8889
- "kv",
8890
- { meta: { status: response.status, statusText: response.statusText } }
8891
- ));
8911
+ const network = () => this.getFromNetwork(key, path, options, request.signal);
8912
+ if (options?.source === "network") {
8913
+ const observer = this.readThrough;
8914
+ if (observer === null) return await network();
8915
+ let outcome = "error";
8916
+ const startedAt = Date.now();
8917
+ try {
8918
+ const result = await network();
8919
+ outcome = result.ok ? "found" : result.error.code === ErrorCodes.KV_NOT_FOUND ? "not_found" : "error";
8920
+ return result;
8921
+ } finally {
8922
+ try {
8923
+ observer.observeNetworkRequested({
8924
+ op: "get",
8925
+ space: this.context.session.spaceId,
8926
+ path,
8927
+ reason: "NETWORK_REQUESTED",
8928
+ outcome,
8929
+ latencyMs: Date.now() - startedAt
8930
+ });
8931
+ } catch {
8932
+ }
8892
8933
  }
8893
- return err(
8894
- serviceError(
8895
- ErrorCodes.NETWORK_ERROR,
8896
- `Failed to get key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
8897
- "kv",
8898
- { meta: { status: response.status, statusText: response.statusText } }
8899
- )
8900
- );
8901
8934
  }
8902
- const data = await this.parseResponse(
8903
- response,
8904
- options?.raw,
8905
- options?.binary
8906
- );
8907
- return ok({
8908
- data,
8909
- headers: this.createResponseHeaders(response.headers)
8935
+ if (this.readThrough === null) return await network();
8936
+ return await this.readThrough.get({
8937
+ space: this.context.session.spaceId,
8938
+ key,
8939
+ path,
8940
+ options,
8941
+ signal: request.signal,
8942
+ network
8910
8943
  });
8911
8944
  } catch (error) {
8912
8945
  return err(wrapError("kv", error));
@@ -8915,6 +8948,56 @@ var KVService = class extends BaseService {
8915
8948
  }
8916
8949
  });
8917
8950
  }
8951
+ async getFromNetwork(key, path, options, signal) {
8952
+ const response = await this.invokeOperation(
8953
+ path,
8954
+ KVAction.GET,
8955
+ void 0,
8956
+ signal,
8957
+ options?.maxResponseBytes === void 0 ? void 0 : { "x-tinycloud-max-response-bytes": String(options.maxResponseBytes) }
8958
+ );
8959
+ if (!response.ok) {
8960
+ if (response.status === 401 || response.status === 403) {
8961
+ const errorText2 = await this.readAuthorizationText(response);
8962
+ const permissionHint = parsePermissionHintFromErrorText(errorText2);
8963
+ const structuredCapability = permissionHint?.service === "tinycloud.kv" && permissionHint.space === this.context.session.spaceId && permissionHint.path === path ? validatedCapabilityOf({
8964
+ service: "kv",
8965
+ code: ErrorCodes.AUTH_UNAUTHORIZED,
8966
+ meta: {
8967
+ status: response.status,
8968
+ resource: `${this.context.session.spaceId}/kv/${path}`,
8969
+ requiredAction: KVAction.GET
8970
+ }
8971
+ }) : void 0;
8972
+ return this.authorizationFailure(
8973
+ `Failed to get key ${JSON.stringify(key)}`,
8974
+ response,
8975
+ errorText2,
8976
+ [path],
8977
+ KVAction.GET,
8978
+ structuredCapability ? { permissionHint, ...structuredCapability } : {}
8979
+ );
8980
+ }
8981
+ if (response.status === 404) return this.classifyNotFound(response, key);
8982
+ const errorText = await response.text();
8983
+ if (response.status === 413) {
8984
+ return err(serviceError(
8985
+ ErrorCodes.KV_RESPONSE_TOO_LARGE,
8986
+ `KV value at key ${JSON.stringify(key)} exceeds the requested response limit`,
8987
+ "kv",
8988
+ { meta: { status: response.status, statusText: response.statusText } }
8989
+ ));
8990
+ }
8991
+ return err(serviceError(
8992
+ ErrorCodes.NETWORK_ERROR,
8993
+ `Failed to get key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
8994
+ "kv",
8995
+ { meta: { status: response.status, statusText: response.statusText } }
8996
+ ));
8997
+ }
8998
+ const data = await this.parseResponse(response, options?.raw, options?.binary);
8999
+ return ok({ data, headers: this.createResponseHeaders(response.headers) });
9000
+ }
8918
9001
  async batchGet(keys, options) {
8919
9002
  return this.withTelemetry(
8920
9003
  "batchGet",
@@ -8927,6 +9010,7 @@ var KVService = class extends BaseService {
8927
9010
  */
8928
9011
  async put(key, value, options) {
8929
9012
  return this.withTelemetry("put", key, async () => {
9013
+ if (this.readThroughReady !== null) await this.readThroughReady;
8930
9014
  if (!this.requireAuth()) {
8931
9015
  return err(authRequiredError("kv"));
8932
9016
  }
@@ -8941,63 +9025,15 @@ var KVService = class extends BaseService {
8941
9025
  const body = this.serializePutValue(value, options?.contentType);
8942
9026
  const request = this.createRequestSignal(options?.signal, options?.timeout);
8943
9027
  try {
8944
- const response = await this.invokeOperation(
8945
- path,
8946
- KVAction.PUT,
8947
- body,
8948
- request.signal,
8949
- {
8950
- ...options?.ifMatch === void 0 ? {} : { "if-match": options.ifMatch },
8951
- ...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
8952
- }
8953
- );
8954
- if (response.status === 401 || response.status === 403) {
8955
- return this.authorizationFailure(
8956
- `Failed to put key ${JSON.stringify(key)}`,
8957
- response,
8958
- await this.readAuthorizationText(response),
8959
- [path],
8960
- KVAction.PUT
8961
- );
8962
- }
8963
- if (!response.ok) {
8964
- const errorText = await response.text();
8965
- if (response.status === 412) {
8966
- return err(serviceError(
8967
- ErrorCodes.KV_PRECONDITION_FAILED,
8968
- `KV precondition failed for key ${JSON.stringify(key)}`,
8969
- "kv",
8970
- { meta: { status: response.status, statusText: response.statusText } }
8971
- ));
8972
- }
8973
- if (response.status === 503 && (options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0)) {
8974
- return err(serviceError(
8975
- ErrorCodes.KV_CONFLICT,
8976
- `Concurrent KV update conflicted for key ${JSON.stringify(key)}`,
8977
- "kv",
8978
- { meta: { status: response.status, statusText: response.statusText } }
8979
- ));
8980
- }
8981
- const quotaError = this.handleQuotaErrorResponse(
8982
- response,
8983
- errorText,
8984
- key
8985
- );
8986
- if (quotaError) {
8987
- return quotaError;
8988
- }
8989
- return err(
8990
- serviceError(
8991
- ErrorCodes.KV_WRITE_FAILED,
8992
- `Failed to put key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
8993
- "kv",
8994
- { meta: { status: response.status, statusText: response.statusText } }
8995
- )
8996
- );
8997
- }
8998
- return ok({
8999
- data: void 0,
9000
- headers: this.createResponseHeaders(response.headers)
9028
+ const network = () => this.putToNetwork(key, path, body, options, request.signal);
9029
+ if (this.readThrough === null) return await network();
9030
+ return await this.readThrough.write({
9031
+ op: "put",
9032
+ space: this.context.session.spaceId,
9033
+ entries: [{ path, body }],
9034
+ signal: request.signal,
9035
+ conditional: options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0,
9036
+ network
9001
9037
  });
9002
9038
  } catch (error) {
9003
9039
  return err(wrapError("kv", error));
@@ -9006,11 +9042,64 @@ var KVService = class extends BaseService {
9006
9042
  }
9007
9043
  });
9008
9044
  }
9045
+ async putToNetwork(key, path, body, options, signal) {
9046
+ const response = await this.invokeOperation(
9047
+ path,
9048
+ KVAction.PUT,
9049
+ body,
9050
+ signal,
9051
+ {
9052
+ ...options?.ifMatch === void 0 ? {} : { "if-match": options.ifMatch },
9053
+ ...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
9054
+ }
9055
+ );
9056
+ if (response.status === 401 || response.status === 403) {
9057
+ return this.authorizationFailure(
9058
+ `Failed to put key ${JSON.stringify(key)}`,
9059
+ response,
9060
+ await this.readAuthorizationText(response),
9061
+ [path],
9062
+ KVAction.PUT
9063
+ );
9064
+ }
9065
+ if (!response.ok) {
9066
+ const errorText = await response.text();
9067
+ if (response.status === 412) {
9068
+ return err(serviceError(
9069
+ ErrorCodes.KV_PRECONDITION_FAILED,
9070
+ `KV precondition failed for key ${JSON.stringify(key)}`,
9071
+ "kv",
9072
+ { meta: { status: response.status, statusText: response.statusText } }
9073
+ ));
9074
+ }
9075
+ if (response.status === 503 && (options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0)) {
9076
+ return err(serviceError(
9077
+ ErrorCodes.KV_CONFLICT,
9078
+ `Concurrent KV update conflicted for key ${JSON.stringify(key)}`,
9079
+ "kv",
9080
+ { meta: { status: response.status, statusText: response.statusText } }
9081
+ ));
9082
+ }
9083
+ const quotaError = this.handleQuotaErrorResponse(response, errorText, key);
9084
+ if (quotaError) return quotaError;
9085
+ return err(serviceError(
9086
+ ErrorCodes.KV_WRITE_FAILED,
9087
+ `Failed to put key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
9088
+ "kv",
9089
+ { meta: { status: response.status, statusText: response.statusText } }
9090
+ ));
9091
+ }
9092
+ return ok({
9093
+ data: void 0,
9094
+ headers: this.createResponseHeaders(response.headers)
9095
+ });
9096
+ }
9009
9097
  /**
9010
9098
  * Store multiple values in one TinyCloud KV invocation.
9011
9099
  */
9012
9100
  async batchPut(items, options) {
9013
9101
  return this.withTelemetry("batchPut", String(items.length), async () => {
9102
+ if (this.readThroughReady !== null) await this.readThroughReady;
9014
9103
  if (!this.requireAuth()) {
9015
9104
  return err(authRequiredError("kv"));
9016
9105
  }
@@ -9041,155 +9130,177 @@ var KVService = class extends BaseService {
9041
9130
  }
9042
9131
  seen.add(path);
9043
9132
  }
9044
- let requestMayHaveDispatched = false;
9045
9133
  const request = this.createRequestSignal(options?.signal, options?.timeout);
9046
9134
  try {
9047
- const body = new FormData();
9048
- for (let index = 0; index < items.length; index++) {
9049
- body.append(
9050
- encodeKvBatchPartName(paths[index]),
9051
- this.serializeBatchPutValue(items[index])
9052
- );
9053
- }
9054
- const headers = this.context.invokeAny(
9055
- session2,
9056
- paths.map((path) => ({
9057
- spaceId: session2.spaceId,
9058
- service: "kv",
9059
- path,
9060
- action: KVAction.PUT
9061
- }))
9135
+ const network = () => this.batchPutToNetwork(items, paths, request.signal);
9136
+ if (this.readThrough === null) return await network();
9137
+ const entries = items.map((item, index) => ({
9138
+ path: paths[index],
9139
+ body: this.serializeBatchPutValue(item)
9140
+ }));
9141
+ return await this.readThrough.write({
9142
+ op: "batchPut",
9143
+ space: session2.spaceId,
9144
+ entries,
9145
+ signal: request.signal,
9146
+ network: () => this.batchPutToNetwork(items, paths, request.signal, entries)
9147
+ });
9148
+ } catch (error) {
9149
+ const wrapped = wrapError("kv", error);
9150
+ return err({ ...wrapped, meta: { ...wrapped.meta, requestMayHaveDispatched: false } });
9151
+ } finally {
9152
+ request.dispose();
9153
+ }
9154
+ });
9155
+ }
9156
+ async batchPutToNetwork(items, paths, signal, entries) {
9157
+ const session2 = this.context.session;
9158
+ let requestMayHaveDispatched = false;
9159
+ try {
9160
+ const body = new FormData();
9161
+ for (let index = 0; index < items.length; index++) {
9162
+ body.append(
9163
+ encodeKvBatchPartName(paths[index]),
9164
+ entries?.[index]?.body ?? this.serializeBatchPutValue(items[index])
9062
9165
  );
9063
- const url = `${this.host}/invoke`;
9064
- const init = {
9065
- method: "POST",
9066
- headers,
9067
- body,
9068
- signal: request.signal
9069
- };
9070
- const fetchFn = this.context.fetch;
9071
- requestMayHaveDispatched = true;
9072
- const response = await fetchFn(url, init);
9073
- if (!response.ok) {
9074
- let errorText;
9075
- try {
9076
- errorText = await response.text();
9077
- } catch (textError) {
9078
- const cause = textError instanceof Error ? textError : new Error(String(textError));
9079
- return err(
9080
- serviceError(
9081
- ErrorCodes.KV_WRITE_FAILED,
9082
- `Failed to batch put ${items.length} key(s): ${response.status} - <response body could not be read: ${cause.message}>`,
9083
- "kv",
9084
- { cause, meta: { status: response.status, statusText: response.statusText } }
9085
- )
9086
- );
9087
- }
9088
- if (response.status === 401 || response.status === 403) {
9089
- return this.authorizationFailure(
9090
- `Failed to batch put ${items.length} key(s)`,
9091
- response,
9092
- errorText,
9093
- paths,
9094
- KVAction.PUT
9095
- );
9096
- }
9097
- const quotaError = this.handleQuotaErrorResponse(
9098
- response,
9099
- errorText,
9100
- "batch"
9101
- );
9102
- if (quotaError) {
9103
- return quotaError;
9104
- }
9166
+ }
9167
+ const headers = this.context.invokeAny(
9168
+ session2,
9169
+ paths.map((path) => ({
9170
+ spaceId: session2.spaceId,
9171
+ service: "kv",
9172
+ path,
9173
+ action: KVAction.PUT
9174
+ }))
9175
+ );
9176
+ const url = `${this.host}/invoke`;
9177
+ const init = {
9178
+ method: "POST",
9179
+ headers,
9180
+ body,
9181
+ signal
9182
+ };
9183
+ const fetchFn = this.context.fetch;
9184
+ requestMayHaveDispatched = true;
9185
+ const response = await fetchFn(url, init);
9186
+ if (!response.ok) {
9187
+ let errorText;
9188
+ try {
9189
+ errorText = await response.text();
9190
+ } catch (textError) {
9191
+ const cause = textError instanceof Error ? textError : new Error(String(textError));
9105
9192
  return err(
9106
9193
  serviceError(
9107
9194
  ErrorCodes.KV_WRITE_FAILED,
9108
- `Failed to batch put ${items.length} key(s): ${response.status} - ${errorText}`,
9195
+ `Failed to batch put ${items.length} key(s): ${response.status} - <response body could not be read: ${cause.message}>`,
9109
9196
  "kv",
9110
- { meta: { status: response.status, statusText: response.statusText } }
9197
+ { cause, meta: { status: response.status, statusText: response.statusText } }
9111
9198
  )
9112
9199
  );
9113
9200
  }
9114
- let rawBody;
9115
- try {
9116
- rawBody = await response.json();
9117
- } catch (jsonError) {
9118
- const cause = jsonError instanceof Error ? jsonError : new Error(String(jsonError));
9119
- const timedOut = isRequestTimeoutError(cause);
9120
- return err(
9121
- serviceError(
9122
- timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
9123
- timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
9124
- "kv",
9125
- {
9126
- cause,
9127
- meta: {
9128
- ...timedOut ? { timeoutMs: cause.timeoutMs } : {},
9129
- requestMayHaveDispatched: true,
9130
- responseReceived: true,
9131
- status: response.status,
9132
- outcome: "batch-unconfirmed"
9133
- }
9134
- }
9135
- )
9201
+ if (response.status === 401 || response.status === 403) {
9202
+ return this.authorizationFailure(
9203
+ `Failed to batch put ${items.length} key(s)`,
9204
+ response,
9205
+ errorText,
9206
+ paths,
9207
+ KVAction.PUT
9136
9208
  );
9137
9209
  }
9138
- const batchResponse = this.normalizeBatchPutResponse(rawBody);
9139
- if (!batchResponse) {
9140
- return err(
9141
- serviceError(
9142
- ErrorCodes.NETWORK_ERROR,
9143
- "KV batchPut response did not include matching written keys and count",
9144
- "kv",
9145
- {
9146
- meta: {
9147
- requestMayHaveDispatched: true,
9148
- responseReceived: true,
9149
- status: response.status,
9150
- outcome: "batch-unconfirmed"
9151
- }
9152
- }
9153
- )
9154
- );
9210
+ const quotaError = this.handleQuotaErrorResponse(
9211
+ response,
9212
+ errorText,
9213
+ "batch"
9214
+ );
9215
+ if (quotaError) {
9216
+ return quotaError;
9155
9217
  }
9156
- const requestedPaths = new Set(paths);
9157
- const writtenPaths = new Set(batchResponse.written);
9158
- const matchesRequest = batchResponse.count === paths.length && batchResponse.written.length === paths.length && writtenPaths.size === paths.length && batchResponse.written.every((key) => requestedPaths.has(key));
9159
- if (!matchesRequest) {
9160
- return err(
9161
- serviceError(
9162
- ErrorCodes.NETWORK_ERROR,
9163
- `KV batchPut response did not confirm all ${paths.length} requested key(s) were written`,
9164
- "kv",
9165
- {
9166
- meta: {
9167
- requestMayHaveDispatched: true,
9168
- responseReceived: true,
9169
- status: response.status,
9170
- outcome: "batch-unconfirmed"
9171
- }
9218
+ return err(
9219
+ serviceError(
9220
+ ErrorCodes.KV_WRITE_FAILED,
9221
+ `Failed to batch put ${items.length} key(s): ${response.status} - ${errorText}`,
9222
+ "kv",
9223
+ { meta: { status: response.status, statusText: response.statusText } }
9224
+ )
9225
+ );
9226
+ }
9227
+ let rawBody;
9228
+ try {
9229
+ rawBody = await response.json();
9230
+ } catch (jsonError) {
9231
+ const cause = jsonError instanceof Error ? jsonError : new Error(String(jsonError));
9232
+ const timedOut = isRequestTimeoutError(cause);
9233
+ return err(
9234
+ serviceError(
9235
+ timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
9236
+ timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
9237
+ "kv",
9238
+ {
9239
+ cause,
9240
+ meta: {
9241
+ ...timedOut ? { timeoutMs: cause.timeoutMs } : {},
9242
+ requestMayHaveDispatched: true,
9243
+ responseReceived: true,
9244
+ status: response.status,
9245
+ outcome: "batch-unconfirmed"
9172
9246
  }
9173
- )
9174
- );
9175
- }
9176
- return ok(batchResponse);
9177
- } catch (error) {
9178
- const wrapped = wrapError("kv", error);
9179
- return err({
9180
- ...wrapped,
9181
- meta: { ...wrapped.meta, requestMayHaveDispatched }
9182
- });
9183
- } finally {
9184
- request.dispose();
9247
+ }
9248
+ )
9249
+ );
9185
9250
  }
9186
- });
9251
+ const batchResponse = this.normalizeBatchPutResponse(rawBody);
9252
+ if (!batchResponse) {
9253
+ return err(
9254
+ serviceError(
9255
+ ErrorCodes.NETWORK_ERROR,
9256
+ "KV batchPut response did not include matching written keys and count",
9257
+ "kv",
9258
+ {
9259
+ meta: {
9260
+ requestMayHaveDispatched: true,
9261
+ responseReceived: true,
9262
+ status: response.status,
9263
+ outcome: "batch-unconfirmed"
9264
+ }
9265
+ }
9266
+ )
9267
+ );
9268
+ }
9269
+ const requestedPaths = new Set(paths);
9270
+ const writtenPaths = new Set(batchResponse.written);
9271
+ const matchesRequest = batchResponse.count === paths.length && batchResponse.written.length === paths.length && writtenPaths.size === paths.length && batchResponse.written.every((key) => requestedPaths.has(key));
9272
+ if (!matchesRequest) {
9273
+ return err(
9274
+ serviceError(
9275
+ ErrorCodes.NETWORK_ERROR,
9276
+ `KV batchPut response did not confirm all ${paths.length} requested key(s) were written`,
9277
+ "kv",
9278
+ {
9279
+ meta: {
9280
+ requestMayHaveDispatched: true,
9281
+ responseReceived: true,
9282
+ status: response.status,
9283
+ outcome: "batch-unconfirmed"
9284
+ }
9285
+ }
9286
+ )
9287
+ );
9288
+ }
9289
+ return ok(batchResponse);
9290
+ } catch (error) {
9291
+ const wrapped = wrapError("kv", error);
9292
+ return err({
9293
+ ...wrapped,
9294
+ meta: { ...wrapped.meta, requestMayHaveDispatched }
9295
+ });
9296
+ }
9187
9297
  }
9188
9298
  /**
9189
9299
  * List keys with optional prefix filtering.
9190
9300
  */
9191
9301
  async list(options) {
9192
9302
  return this.withTelemetry("list", options?.prefix, async () => {
9303
+ if (this.readThroughReady !== null) await this.readThroughReady;
9193
9304
  if (!this.requireAuth()) {
9194
9305
  return err(authRequiredError("kv"));
9195
9306
  }
@@ -9206,49 +9317,44 @@ var KVService = class extends BaseService {
9206
9317
  }
9207
9318
  const request = this.createRequestSignal(options?.signal, options?.timeout);
9208
9319
  try {
9209
- const response = await this.invokeOperation(
9210
- listPath,
9211
- KVAction.LIST,
9212
- void 0,
9213
- request.signal,
9214
- options?.limit === void 0 && options?.cursor === void 0 ? void 0 : {
9215
- ...options?.limit === void 0 ? {} : { "x-tinycloud-limit": String(options.limit) },
9216
- ...options?.cursor === void 0 ? {} : { "x-tinycloud-cursor": options.cursor }
9217
- }
9218
- );
9219
- if (!response.ok) {
9220
- if (response.status === 401 || response.status === 403) {
9221
- return this.authorizationFailure(
9222
- "Failed to list keys",
9223
- response,
9224
- await this.readAuthorizationText(response),
9225
- [listPath],
9226
- KVAction.LIST
9227
- );
9320
+ const network = () => this.listFromNetwork(listPath, options, request.signal);
9321
+ let page;
9322
+ if (options?.source === "network") {
9323
+ const observer = this.readThrough;
9324
+ if (observer === null) {
9325
+ page = await network();
9326
+ } else {
9327
+ let outcome = "error";
9328
+ const startedAt = Date.now();
9329
+ try {
9330
+ page = await network();
9331
+ outcome = page.ok ? "found" : "error";
9332
+ } finally {
9333
+ try {
9334
+ observer.observeNetworkRequested({
9335
+ op: "list",
9336
+ space: this.context.session.spaceId,
9337
+ path: listPath,
9338
+ reason: "NETWORK_REQUESTED",
9339
+ outcome,
9340
+ latencyMs: Date.now() - startedAt
9341
+ });
9342
+ } catch {
9343
+ }
9344
+ }
9228
9345
  }
9229
- const errorText = await response.text();
9230
- return err(
9231
- serviceError(
9232
- ErrorCodes.NETWORK_ERROR,
9233
- `Failed to list keys: ${response.status} - ${errorText}`,
9234
- "kv",
9235
- { meta: { status: response.status, statusText: response.statusText } }
9236
- )
9237
- );
9238
- }
9239
- let keys = await this.parseResponse(response, options?.raw);
9240
- keys = keys ?? [];
9241
- if (options?.removePrefix && listPath) {
9242
- const prefixWithSlash = listPath.endsWith("/") ? listPath : `${listPath}/`;
9243
- keys = keys.map(
9244
- (key) => key.startsWith(prefixWithSlash) ? key.slice(prefixWithSlash.length) : key
9245
- );
9346
+ } else if (this.readThrough === null) {
9347
+ page = await network();
9348
+ } else {
9349
+ page = await this.readThrough.list({
9350
+ space: this.context.session.spaceId,
9351
+ listPath,
9352
+ options,
9353
+ signal: request.signal,
9354
+ network
9355
+ });
9246
9356
  }
9247
- return ok({
9248
- keys,
9249
- ...response.headers.get("x-tinycloud-truncated") === null ? {} : { truncated: response.headers.get("x-tinycloud-truncated") === "true" },
9250
- ...response.headers.get("x-tinycloud-next-cursor") === null ? {} : { nextCursor: response.headers.get("x-tinycloud-next-cursor") ?? void 0 }
9251
- });
9357
+ return page.ok ? this.finishList(page.data, listPath, options?.removePrefix) : page;
9252
9358
  } catch (error) {
9253
9359
  return err(wrapError("kv", error));
9254
9360
  } finally {
@@ -9256,6 +9362,56 @@ var KVService = class extends BaseService {
9256
9362
  }
9257
9363
  });
9258
9364
  }
9365
+ async listFromNetwork(listPath, options, signal) {
9366
+ const response = await this.invokeOperation(
9367
+ listPath,
9368
+ KVAction.LIST,
9369
+ void 0,
9370
+ signal,
9371
+ options?.limit === void 0 && options?.cursor === void 0 ? void 0 : {
9372
+ ...options?.limit === void 0 ? {} : { "x-tinycloud-limit": String(options.limit) },
9373
+ ...options?.cursor === void 0 ? {} : { "x-tinycloud-cursor": options.cursor }
9374
+ }
9375
+ );
9376
+ if (!response.ok) {
9377
+ if (response.status === 401 || response.status === 403) {
9378
+ return this.authorizationFailure(
9379
+ "Failed to list keys",
9380
+ response,
9381
+ await this.readAuthorizationText(response),
9382
+ [listPath],
9383
+ KVAction.LIST
9384
+ );
9385
+ }
9386
+ const errorText = await response.text();
9387
+ return err(serviceError(
9388
+ ErrorCodes.NETWORK_ERROR,
9389
+ `Failed to list keys: ${response.status} - ${errorText}`,
9390
+ "kv",
9391
+ { meta: { status: response.status, statusText: response.statusText } }
9392
+ ));
9393
+ }
9394
+ const keys = await this.parseResponse(response, options?.raw) ?? [];
9395
+ return ok({
9396
+ keys,
9397
+ ...response.headers.get("x-tinycloud-truncated") === null ? {} : { truncated: response.headers.get("x-tinycloud-truncated") === "true" },
9398
+ ...response.headers.get("x-tinycloud-next-cursor") === null ? {} : { nextCursor: response.headers.get("x-tinycloud-next-cursor") ?? void 0 }
9399
+ });
9400
+ }
9401
+ finishList(page, listPath, removePrefix) {
9402
+ let keys = page.keys;
9403
+ if (removePrefix && listPath) {
9404
+ const prefixWithSlash = listPath.endsWith("/") ? listPath : `${listPath}/`;
9405
+ keys = keys.map(
9406
+ (key) => key.startsWith(prefixWithSlash) ? key.slice(prefixWithSlash.length) : key
9407
+ );
9408
+ }
9409
+ return ok({
9410
+ keys,
9411
+ ...page.truncated === void 0 ? {} : { truncated: page.truncated },
9412
+ ...page.nextCursor === void 0 ? {} : { nextCursor: page.nextCursor }
9413
+ });
9414
+ }
9259
9415
  /**
9260
9416
  * Read one page of the `tinycloud.kv/sync` change feed for a prefix.
9261
9417
  */
@@ -9424,61 +9580,22 @@ var KVService = class extends BaseService {
9424
9580
  */
9425
9581
  async delete(key, options) {
9426
9582
  return this.withTelemetry("delete", key, async () => {
9583
+ if (this.readThroughReady !== null) await this.readThroughReady;
9427
9584
  if (!this.requireAuth()) {
9428
9585
  return err(authRequiredError("kv"));
9429
9586
  }
9430
9587
  const path = this.getFullPath(key, options?.prefix);
9431
9588
  const request = this.createRequestSignal(options?.signal, options?.timeout);
9432
9589
  try {
9433
- const response = await this.invokeOperation(
9434
- path,
9435
- KVAction.DELETE,
9436
- void 0,
9437
- request.signal,
9438
- options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
9439
- );
9440
- if (!response.ok) {
9441
- if (response.status === 401 || response.status === 403) {
9442
- return this.authorizationFailure(
9443
- `Failed to delete key ${JSON.stringify(key)}`,
9444
- response,
9445
- await this.readAuthorizationText(response),
9446
- [path],
9447
- KVAction.DELETE
9448
- );
9449
- }
9450
- if (response.status === 404) {
9451
- return this.classifyNotFound(response, key);
9452
- }
9453
- const errorText = await response.text();
9454
- if (response.status === 412) {
9455
- return err(serviceError(
9456
- ErrorCodes.KV_PRECONDITION_FAILED,
9457
- `KV precondition failed for key ${JSON.stringify(key)}`,
9458
- "kv",
9459
- { meta: { status: response.status, statusText: response.statusText } }
9460
- ));
9461
- }
9462
- if (response.status === 503 && options?.ifMatch !== void 0) {
9463
- return err(serviceError(
9464
- ErrorCodes.KV_CONFLICT,
9465
- `Concurrent KV delete conflicted for key ${JSON.stringify(key)}`,
9466
- "kv",
9467
- { meta: { status: response.status, statusText: response.statusText } }
9468
- ));
9469
- }
9470
- return err(
9471
- serviceError(
9472
- ErrorCodes.NETWORK_ERROR,
9473
- `Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
9474
- "kv",
9475
- { meta: { status: response.status, statusText: response.statusText } }
9476
- )
9477
- );
9478
- }
9479
- return ok({
9480
- data: void 0,
9481
- headers: this.createResponseHeaders(response.headers)
9590
+ const network = () => this.deleteFromNetwork(key, path, options, request.signal);
9591
+ if (this.readThrough === null) return await network();
9592
+ return await this.readThrough.write({
9593
+ op: "delete",
9594
+ space: this.context.session.spaceId,
9595
+ entries: [{ path }],
9596
+ signal: request.signal,
9597
+ conditional: options?.ifMatch !== void 0,
9598
+ network
9482
9599
  });
9483
9600
  } catch (error) {
9484
9601
  return err(wrapError("kv", error));
@@ -9487,6 +9604,54 @@ var KVService = class extends BaseService {
9487
9604
  }
9488
9605
  });
9489
9606
  }
9607
+ async deleteFromNetwork(key, path, options, signal) {
9608
+ const response = await this.invokeOperation(
9609
+ path,
9610
+ KVAction.DELETE,
9611
+ void 0,
9612
+ signal,
9613
+ options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
9614
+ );
9615
+ if (!response.ok) {
9616
+ if (response.status === 401 || response.status === 403) {
9617
+ return this.authorizationFailure(
9618
+ `Failed to delete key ${JSON.stringify(key)}`,
9619
+ response,
9620
+ await this.readAuthorizationText(response),
9621
+ [path],
9622
+ KVAction.DELETE
9623
+ );
9624
+ }
9625
+ if (response.status === 404) return this.classifyNotFound(response, key);
9626
+ const errorText = await response.text();
9627
+ if (response.status === 412) {
9628
+ return err(serviceError(
9629
+ ErrorCodes.KV_PRECONDITION_FAILED,
9630
+ `KV precondition failed for key ${JSON.stringify(key)}`,
9631
+ "kv",
9632
+ { meta: { status: response.status, statusText: response.statusText } }
9633
+ ));
9634
+ }
9635
+ if (response.status === 503 && options?.ifMatch !== void 0) {
9636
+ return err(serviceError(
9637
+ ErrorCodes.KV_CONFLICT,
9638
+ `Concurrent KV delete conflicted for key ${JSON.stringify(key)}`,
9639
+ "kv",
9640
+ { meta: { status: response.status, statusText: response.statusText } }
9641
+ ));
9642
+ }
9643
+ return err(serviceError(
9644
+ ErrorCodes.NETWORK_ERROR,
9645
+ `Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
9646
+ "kv",
9647
+ { meta: { status: response.status, statusText: response.statusText } }
9648
+ ));
9649
+ }
9650
+ return ok({
9651
+ data: void 0,
9652
+ headers: this.createResponseHeaders(response.headers)
9653
+ });
9654
+ }
9490
9655
  /**
9491
9656
  * Get metadata for a key without retrieving the value.
9492
9657
  */
@@ -9515,7 +9680,7 @@ var KVService = class extends BaseService {
9515
9680
  );
9516
9681
  }
9517
9682
  if (response.status === 404) {
9518
- return this.classifyNotFound(response, key);
9683
+ return await this.classifyNotFound(response, key);
9519
9684
  }
9520
9685
  const errorText = await response.text();
9521
9686
  return err(
@@ -9581,7 +9746,7 @@ var KVService = class extends BaseService {
9581
9746
  signal: request.signal
9582
9747
  });
9583
9748
  if (!response.ok) {
9584
- return this.createSignedReadUrlError(response, key, path);
9749
+ return await this.createSignedReadUrlError(response, key, path);
9585
9750
  }
9586
9751
  const signedUrl = this.normalizeSignedReadUrlResponse(
9587
9752
  await response.json()
@@ -9816,7 +9981,7 @@ var SQLService = class extends BaseService {
9816
9981
  request.signal
9817
9982
  );
9818
9983
  if (!response.ok) {
9819
- return this.handleErrorResponse(response, "query", dbName, [action]);
9984
+ return await this.handleErrorResponse(response, "query", dbName, [action]);
9820
9985
  }
9821
9986
  const data = await response.json();
9822
9987
  return ok(data);
@@ -9856,7 +10021,7 @@ var SQLService = class extends BaseService {
9856
10021
  request.signal
9857
10022
  );
9858
10023
  if (!response.ok) {
9859
- return this.handleErrorResponse(response, "execute", dbName, requestedActions);
10024
+ return await this.handleErrorResponse(response, "execute", dbName, requestedActions);
9860
10025
  }
9861
10026
  const data = await response.json();
9862
10027
  return ok(data);
@@ -9882,7 +10047,7 @@ var SQLService = class extends BaseService {
9882
10047
  request.signal
9883
10048
  );
9884
10049
  if (!response.ok) {
9885
- return this.handleErrorResponse(response, "batch", dbName, requestedActions);
10050
+ return await this.handleErrorResponse(response, "batch", dbName, requestedActions);
9886
10051
  }
9887
10052
  const data = await response.json();
9888
10053
  return ok(data);
@@ -9907,7 +10072,7 @@ var SQLService = class extends BaseService {
9907
10072
  request.signal
9908
10073
  );
9909
10074
  if (!response.ok) {
9910
- return this.handleErrorResponse(response, "executeStatement", dbName, [SQLAction.WRITE]);
10075
+ return await this.handleErrorResponse(response, "executeStatement", dbName, [SQLAction.WRITE]);
9911
10076
  }
9912
10077
  const data = await response.json();
9913
10078
  return ok(data);
@@ -9932,7 +10097,7 @@ var SQLService = class extends BaseService {
9932
10097
  request.signal
9933
10098
  );
9934
10099
  if (!response.ok) {
9935
- return this.handleErrorResponse(response, "export", dbName, [SQLAction.READ]);
10100
+ return await this.handleErrorResponse(response, "export", dbName, [SQLAction.READ]);
9936
10101
  }
9937
10102
  const resp = response;
9938
10103
  if (typeof resp.blob === "function") {
@@ -10301,7 +10466,7 @@ var DuckDbService = class extends BaseService {
10301
10466
  request.signal
10302
10467
  );
10303
10468
  if (!response.ok) {
10304
- return this.handleErrorResponse(response, "query");
10469
+ return await this.handleErrorResponse(response, "query");
10305
10470
  }
10306
10471
  const data = await response.json();
10307
10472
  return ok(data);
@@ -10327,7 +10492,7 @@ var DuckDbService = class extends BaseService {
10327
10492
  { Accept: "application/vnd.apache.arrow.stream" }
10328
10493
  );
10329
10494
  if (!response.ok) {
10330
- return this.handleErrorResponse(response, "queryArrow");
10495
+ return await this.handleErrorResponse(response, "queryArrow");
10331
10496
  }
10332
10497
  const buffer = await response.arrayBuffer();
10333
10498
  return ok(buffer);
@@ -10360,7 +10525,7 @@ var DuckDbService = class extends BaseService {
10360
10525
  request.signal
10361
10526
  );
10362
10527
  if (!response.ok) {
10363
- return this.handleErrorResponse(response, "execute");
10528
+ return await this.handleErrorResponse(response, "execute");
10364
10529
  }
10365
10530
  const data = await response.json();
10366
10531
  return ok(data);
@@ -10392,7 +10557,7 @@ var DuckDbService = class extends BaseService {
10392
10557
  request.signal
10393
10558
  );
10394
10559
  if (!response.ok) {
10395
- return this.handleErrorResponse(response, "batch");
10560
+ return await this.handleErrorResponse(response, "batch");
10396
10561
  }
10397
10562
  const data = await response.json();
10398
10563
  return ok(data);
@@ -10417,7 +10582,7 @@ var DuckDbService = class extends BaseService {
10417
10582
  request.signal
10418
10583
  );
10419
10584
  if (!response.ok) {
10420
- return this.handleErrorResponse(response, "executeStatement");
10585
+ return await this.handleErrorResponse(response, "executeStatement");
10421
10586
  }
10422
10587
  const data = await response.json();
10423
10588
  return ok(data);
@@ -10442,7 +10607,7 @@ var DuckDbService = class extends BaseService {
10442
10607
  request.signal
10443
10608
  );
10444
10609
  if (!response.ok) {
10445
- return this.handleErrorResponse(response, "describe");
10610
+ return await this.handleErrorResponse(response, "describe");
10446
10611
  }
10447
10612
  const data = await response.json();
10448
10613
  return ok(data);
@@ -10467,7 +10632,7 @@ var DuckDbService = class extends BaseService {
10467
10632
  request.signal
10468
10633
  );
10469
10634
  if (!response.ok) {
10470
- return this.handleErrorResponse(response, "export");
10635
+ return await this.handleErrorResponse(response, "export");
10471
10636
  }
10472
10637
  const blob = await response.blob();
10473
10638
  return ok(blob);
@@ -10502,7 +10667,7 @@ var DuckDbService = class extends BaseService {
10502
10667
  signal: request.signal
10503
10668
  });
10504
10669
  if (!response.ok) {
10505
- return this.handleErrorResponse(response, "import");
10670
+ return await this.handleErrorResponse(response, "import");
10506
10671
  }
10507
10672
  return ok(void 0);
10508
10673
  } catch (error) {
@@ -11804,6 +11969,12 @@ function defaultVaultMessage(input) {
11804
11969
  return input.message ?? `Public key not found for ${input.did}`;
11805
11970
  case "STORAGE_ERROR":
11806
11971
  return input.message ?? input.cause.message;
11972
+ case "ALREADY_EXISTS":
11973
+ return input.message ?? `A value already exists at ${input.key}`;
11974
+ case "AUTH_UNAUTHORIZED":
11975
+ return input.message;
11976
+ case "PERMISSION_DENIED":
11977
+ return input.message;
11807
11978
  case "STORAGE_QUOTA_EXCEEDED":
11808
11979
  case "STORAGE_LIMIT_REACHED":
11809
11980
  return input.message;
@@ -11817,13 +11988,33 @@ function vaultError(input) {
11817
11988
  };
11818
11989
  return { ok: false, error };
11819
11990
  }
11991
+ function typedKvCause(error) {
11992
+ return Object.assign(new Error(error.message), {
11993
+ name: "ServiceError",
11994
+ code: error.code,
11995
+ service: error.service,
11996
+ ...error.meta !== void 0 ? { meta: error.meta } : {},
11997
+ ...error.cause !== void 0 ? { cause: error.cause } : {}
11998
+ });
11999
+ }
11820
12000
  function kvWriteError(context, error) {
11821
12001
  if (error.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error.code === ErrorCodes.STORAGE_LIMIT_REACHED) {
11822
12002
  return vaultError({ code: error.code, message: error.message, meta: error.meta });
11823
12003
  }
12004
+ const message = `${context}: ${error.message}`;
12005
+ if (error.code === ErrorCodes.AUTH_UNAUTHORIZED) {
12006
+ const code = authorizationVerdictOf(error) === "unauthenticated" ? "AUTH_UNAUTHORIZED" : "PERMISSION_DENIED";
12007
+ return vaultError({
12008
+ code,
12009
+ message,
12010
+ ...error.meta !== void 0 ? { meta: error.meta } : {},
12011
+ cause: typedKvCause(error)
12012
+ });
12013
+ }
11824
12014
  return vaultError({
11825
12015
  code: "STORAGE_ERROR",
11826
- cause: new Error(`${context}: ${error.message}`)
12016
+ cause: new Error(message, { cause: typedKvCause(error) }),
12017
+ ...error.meta !== void 0 ? { meta: error.meta } : {}
11827
12018
  });
11828
12019
  }
11829
12020
  var DataVaultService = class extends BaseService {
@@ -12103,11 +12294,18 @@ var DataVaultService = class extends BaseService {
12103
12294
  cause: new Error(envelopeResult.error.message)
12104
12295
  });
12105
12296
  }
12106
- const valuePutResult = await this.tc.kv.put(
12107
- `vault/${key}`,
12108
- JSON.stringify(envelopeResult.data)
12109
- );
12297
+ const valuePath = `vault/${key}`;
12298
+ const valueBody = JSON.stringify(envelopeResult.data);
12299
+ const valuePutResult = options?.ifNoneMatch !== void 0 ? await this.tc.kv.put(valuePath, valueBody, { ifNoneMatch: options.ifNoneMatch }) : await this.tc.kv.put(valuePath, valueBody);
12110
12300
  if (!valuePutResult.ok) {
12301
+ if (options?.ifNoneMatch !== void 0 && valuePutResult.error.code === ErrorCodes.KV_PRECONDITION_FAILED) {
12302
+ return vaultError({
12303
+ code: "ALREADY_EXISTS",
12304
+ key,
12305
+ ...valuePutResult.error.meta !== void 0 ? { meta: valuePutResult.error.meta } : {},
12306
+ cause: typedKvCause(valuePutResult.error)
12307
+ });
12308
+ }
12111
12309
  return kvWriteError("Failed to store encrypted value", valuePutResult.error);
12112
12310
  }
12113
12311
  return { ok: true, data: void 0 };
@@ -12134,7 +12332,8 @@ var DataVaultService = class extends BaseService {
12134
12332
  }
12135
12333
  try {
12136
12334
  const valueResult = await this.tc.kv.get(`vault/${key}`, {
12137
- raw: true
12335
+ raw: true,
12336
+ ...options?.source === void 0 ? {} : { source: options.source }
12138
12337
  });
12139
12338
  if (!valueResult.ok) {
12140
12339
  return vaultError({ code: "KEY_NOT_FOUND", key });
@@ -12179,7 +12378,7 @@ var DataVaultService = class extends BaseService {
12179
12378
  }
12180
12379
  let valueResult;
12181
12380
  try {
12182
- valueResult = await this.tc.kv.get(`vault/${key}`, { raw: true });
12381
+ valueResult = await this.tc.kv.get(`vault/${key}`, { raw: true, source: "network" });
12183
12382
  } catch {
12184
12383
  return { status: "node_unreachable" };
12185
12384
  }
@@ -12248,7 +12447,8 @@ var DataVaultService = class extends BaseService {
12248
12447
  }
12249
12448
  try {
12250
12449
  const valueResult = await this.tc.kv.get(`vault/${key}`, {
12251
- raw: true
12450
+ raw: true,
12451
+ source: "network"
12252
12452
  });
12253
12453
  if (!valueResult.ok) {
12254
12454
  return vaultError({ code: "KEY_NOT_FOUND", key });
@@ -12275,6 +12475,14 @@ var DataVaultService = class extends BaseService {
12275
12475
  if (this.usesNetworkEncryption) {
12276
12476
  return this.putNetworkEncrypted(key, value, options);
12277
12477
  }
12478
+ if (options?.ifNoneMatch !== void 0) {
12479
+ return vaultError({
12480
+ code: "STORAGE_ERROR",
12481
+ cause: new Error(
12482
+ "ifNoneMatch is only supported by network-encrypted vaults"
12483
+ )
12484
+ });
12485
+ }
12278
12486
  if (!this._isUnlocked || !this.masterKey) {
12279
12487
  return vaultError({
12280
12488
  code: "VAULT_LOCKED",
@@ -12374,7 +12582,8 @@ var DataVaultService = class extends BaseService {
12374
12582
  }
12375
12583
  try {
12376
12584
  const keyResult = await this.tc.kv.get(`keys/${key}`, {
12377
- raw: true
12585
+ raw: true,
12586
+ ...options?.source === void 0 ? {} : { source: options.source }
12378
12587
  });
12379
12588
  if (!keyResult.ok) {
12380
12589
  return vaultError({ code: "KEY_NOT_FOUND", key });
@@ -12383,7 +12592,8 @@ var DataVaultService = class extends BaseService {
12383
12592
  const keyBlobBytes = base64Decode(keyEnvelope.key);
12384
12593
  const entryKey = this.crypto.decrypt(this.masterKey, keyBlobBytes);
12385
12594
  const valueResult = await this.tc.kv.get(`vault/${key}`, {
12386
- raw: true
12595
+ raw: true,
12596
+ ...options?.source === void 0 ? {} : { source: options.source }
12387
12597
  });
12388
12598
  if (!valueResult.ok) {
12389
12599
  return vaultError({ code: "KEY_NOT_FOUND", key });
@@ -12493,6 +12703,7 @@ var DataVaultService = class extends BaseService {
12493
12703
  prefix: listPrefix,
12494
12704
  removePrefix: true,
12495
12705
  ...options?.limit === void 0 ? {} : { limit: options.limit },
12706
+ ...options?.source === void 0 ? {} : { source: options.source },
12496
12707
  ...options?.cursor === void 0 ? {} : { cursor: options.cursor }
12497
12708
  });
12498
12709
  if (!listResult.ok) {
@@ -13064,6 +13275,11 @@ var DataVaultService = class extends BaseService {
13064
13275
  }
13065
13276
  };
13066
13277
  DataVaultService.serviceName = "vault";
13278
+ var TC832_RESERVED_SCOPES = Object.freeze([
13279
+ "default",
13280
+ "global",
13281
+ "scoped"
13282
+ ]);
13067
13283
  function canonicalize(value) {
13068
13284
  if (value === void 0) {
13069
13285
  return "";
@@ -14383,6 +14599,11 @@ function kvPrefixCovers(prefix, key) {
14383
14599
  if (prefix.endsWith("/")) return key.startsWith(prefix);
14384
14600
  return key === prefix || key.startsWith(`${prefix}/`);
14385
14601
  }
14602
+ function authorityPathCovers(grantPath, requestedPath) {
14603
+ if (grantPath === "" || grantPath === "/") return true;
14604
+ if (grantPath.endsWith("/")) return requestedPath.startsWith(grantPath);
14605
+ return requestedPath === grantPath;
14606
+ }
14386
14607
  function requiresSecretsOptIn(space, prefix) {
14387
14608
  if (space === "secrets" || space.endsWith(":secrets")) return true;
14388
14609
  const firstSegment = prefix.split("/", 1)[0];
@@ -16496,7 +16717,7 @@ function kvPathIn(resource, space) {
16496
16717
  function grantCovers(grant, space, prefix, ability) {
16497
16718
  for (const [resource, abilities] of Object.entries(grant.att)) {
16498
16719
  const path = kvPathIn(resource, space);
16499
- if (path === void 0 || !kvPrefixCovers(path, prefix)) continue;
16720
+ if (path === void 0 || !authorityPathCovers(path, prefix)) continue;
16500
16721
  if (ability in abilities) return true;
16501
16722
  if (ability !== "tinycloud.kv/sync" && ability !== "tinycloud.kv/retain" && "tinycloud.kv/*" in abilities) return true;
16502
16723
  }
@@ -16509,8 +16730,9 @@ function assertGrantInstallable(grant, input) {
16509
16730
  `The grant was issued to ${grant.audience}, not this device (${input.deviceDid}).`
16510
16731
  );
16511
16732
  }
16733
+ const requiredPaths = input.prefix.endsWith("/") ? [input.prefix] : [input.prefix, `${input.prefix}/`];
16512
16734
  for (const ability of ["tinycloud.kv/sync", "tinycloud.kv/get"]) {
16513
- if (!grantCovers(grant, input.space, input.prefix, ability)) {
16735
+ if (!requiredPaths.every((path) => grantCovers(grant, input.space, path, ability))) {
16514
16736
  throw new ReplicaError(
16515
16737
  ReplicaErrorCode.GRANT_NOT_COVERING,
16516
16738
  `The grant does not carry ${ability} on ${input.space}/kv/${input.prefix}.`,