> ## Documentation Index
> Fetch the complete documentation index at: https://configurate.crystaworld.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Result Types: LockedConfig, UnlockedConfig, BatchRunResult

> Reference for the typed result objects returned by Configurate methods: LockedConfig, UnlockedConfig, PatchedConfig, and BatchRunResult.

tauri-plugin-configurate returns dedicated wrapper classes rather than raw objects so that keyring state — locked versus unlocked — is visible in the type system. When a method returns `LockedConfig<S>`, TypeScript tells you that keyring fields are `null`; when it returns `UnlockedConfig<S>`, you know the full data is available. The types described on this page are the building blocks of that contract.

***

## LockedConfig\<S>

The result of loading or writing a config without unlocking keyring fields. Keyring-protected fields in `data` are typed as `null` and will always be `null` at runtime — they were never fetched from the OS keyring.

```typescript theme={null}
class LockedConfig<S> {
  readonly data: InferLocked<S>;
  unlock(opts: KeyringOptions): Promise<UnlockedConfig<S>>;
}
```

<ResponseField name="data" type="InferLocked<S>">
  The config data with every `keyring()` field replaced by `null`. All other fields have their normal types.
</ResponseField>

<ResponseField name="unlock(opts)" type="Promise<UnlockedConfig<S>>">
  Fetches the keyring-protected values from the OS credential store and returns a new `UnlockedConfig<S>` with those fields populated.
</ResponseField>

```typescript theme={null}
const locked = await config.load().run();

// Keyring fields are null here
console.log(locked.data.apiKey); // null

// Fetch secrets and get the full data
const unlocked = await locked.unlock({ service: "my-app", account: "default" });
console.log(unlocked.data.apiKey); // "secret"
```

***

## UnlockedConfig\<S>

Holds the full config data with keyring fields populated. Access to `data` is revoked when you call `.lock()`, which sets the internal reference to `null`. This is an API-level access guard — it signals that the calling code is done with the sensitive values; it does not zero-clear memory.

```typescript theme={null}
class UnlockedConfig<S> {
  get data(): InferUnlocked<S>;  // throws after lock()
  lock(): void;                  // revokes access to data
}
```

<ResponseField name="data" type="InferUnlocked<S>">
  The complete config with all keyring fields resolved to their actual types. Accessing this property after calling `lock()` throws `"Cannot access data after lock() has been called. Load or unlock again."`.
</ResponseField>

<ResponseField name="lock()" type="void">
  Sets the internal data reference to `null`. Subsequent reads of `data` throw an error. Call this when you no longer need the sensitive values within the current scope.
</ResponseField>

```typescript theme={null}
const unlocked = await config.load().unlock({
  service: "my-app",
  account: "default",
});

const token = unlocked.data.apiKey; // "secret"

// Signal that we're done with the sensitive data
unlocked.lock();

unlocked.data; // throws: "Cannot access data after lock() has been called. Load or unlock again."
```

<Warning>
  `lock()` is an API-level access guard, **not** a cryptographic memory wipe. JavaScript's garbage collector manages memory reclamation and immediate clearing cannot be guaranteed. Do not rely on `lock()` for security-critical wiping.
</Warning>

***

## PatchedConfig\<S>

The result of a `patch().run()` call. Contains only the partial data that was written — not the full merged config — so the shape is `Partial<InferLocked<S>>`.

```typescript theme={null}
class PatchedConfig<S> {
  readonly data: Partial<InferLocked<S>>;
}
```

<ResponseField name="data" type="Partial<InferLocked<S>>">
  The subset of fields that were included in the patch payload, with keyring fields typed as `null`. This is not the full merged config; call `load()` separately if you need the complete state after patching.
</ResponseField>

```typescript theme={null}
const patched = await config.patch({ theme: "dark" }).run();
console.log(patched.data.theme); // "dark"
// patched.data.fontSize is undefined — it was not part of the patch
```

***

## LazyConfigEntry\<S>

The chainable builder returned by `create()`, `load()`, and `save()`. Nothing is sent over IPC until you call `.run()` or `.unlock()`.

```typescript theme={null}
class LazyConfigEntry<S> {
  lock(opts: KeyringOptions): this;
  run(): Promise<LockedConfig<S>>;
  unlock(opts: KeyringOptions): Promise<UnlockedConfig<S>>;
}
```

<ResponseField name=".lock(opts)" type="this">
  Stores the provided `KeyringOptions` on the builder. Call this before `.run()` when your schema contains `keyring()` fields, so the plugin knows where to store secrets in the OS keyring.
</ResponseField>

<ResponseField name=".run()" type="Promise<LockedConfig<S>>">
  Executes the operation and returns a `LockedConfig<S>`. If the schema has keyring fields and you did not call `.lock()` first, the operation will throw.
</ResponseField>

<ResponseField name=".unlock(opts)" type="Promise<UnlockedConfig<S>>">
  Executes the operation, stores keyring secrets, and returns an `UnlockedConfig<S>` in one step.
</ResponseField>

```typescript theme={null}
// Minimal — no keyring fields
const locked = await config.create({ theme: "dark" }).run();

// With keyring fields — must lock first
const locked = await config
  .create({ theme: "dark", apiKey: "secret" })
  .lock({ service: "my-app", account: "default" })
  .run();

// Get unlocked result in one call
const unlocked = await config
  .create({ theme: "dark", apiKey: "secret" })
  .unlock({ service: "my-app", account: "default" });
```

***

## LazyPatchEntry\<S>

The chainable builder returned by `patch()`. Adds a `.createIfMissing()` modifier not present on `LazyConfigEntry`.

```typescript theme={null}
class LazyPatchEntry<S> {
  lock(opts: KeyringOptions): this;
  createIfMissing(): this;
  run(): Promise<PatchedConfig<S>>;
  unlock(opts: KeyringOptions): Promise<UnlockedConfig<S>>;
}
```

<ResponseField name=".lock(opts)" type="this">
  Attaches `KeyringOptions` to the builder, required when patching keyring fields.
</ResponseField>

<ResponseField name=".createIfMissing()" type="this">
  When chained, the patch creates the config from the partial data if the config file does not yet exist, instead of throwing a not-found error.
</ResponseField>

<ResponseField name=".run()" type="Promise<PatchedConfig<S>>">
  Executes the patch and returns the patched subset as a `PatchedConfig<S>`.
</ResponseField>

<ResponseField name=".unlock(opts)" type="Promise<UnlockedConfig<S>>">
  Executes the patch with keyring support and returns a fully unlocked `UnlockedConfig<S>`.
</ResponseField>

```typescript theme={null}
// Basic patch
const patched = await config.patch({ theme: "light" }).run();

// Patch with keyring
const patched = await config
  .patch({ apiKey: "rotated" })
  .lock({ service: "my-app", account: "default" })
  .run();

// Upsert-style patch
const patched = await config
  .patch({ theme: "dark" })
  .createIfMissing()
  .run();
```

***

## BatchRunResult

Returned by all three static batch methods (`Configurate.loadAll`, `Configurate.saveAll`, `Configurate.patchAll`). Each entry in `results` is indexed by the `id` you assigned in the input array and holds either a success value or a structured error.

```typescript theme={null}
interface BatchRunResult {
  results: Record<string, BatchRunEntryResult>;
}

type BatchRunEntryResult =
  | { ok: true; data: unknown }
  | { ok: false; error: { kind: string; message: string } };
```

<ResponseField name="results" type="Record<string, BatchRunEntryResult>">
  A map of `id` → result. Always check `result.ok` before accessing `result.data`; if `ok` is `false`, inspect `result.error.kind` and `result.error.message` for diagnostics.
</ResponseField>

```typescript theme={null}
const result = await Configurate.loadAll([
  { id: "app", config: appConfig },
  { id: "user", config: userConfig },
]).run();

for (const [id, entry] of Object.entries(result.results)) {
  if (entry.ok) {
    console.log(id, entry.data);
  } else {
    console.error(id, entry.error.kind, entry.error.message);
  }
}
```

***

## KeyringOptions

Passed to `.lock()`, `.unlock()`, `delete()`, `exportAs()`, `importFrom()`, and `LockedConfig.unlock()` whenever a keyring operation is needed.

```typescript theme={null}
interface KeyringOptions {
  service: string; // Keyring service name
  account: string; // Keyring account name
}
```

<ResponseField name="service" type="string">
  The service name registered in the OS credential store. Typically your application name. Must not be empty and must not contain control characters.
</ResponseField>

<ResponseField name="account" type="string">
  The account name within the service. Typically `"default"` or a user identifier. Must not be empty and must not contain control characters.
</ResponseField>

```typescript theme={null}
const keyringOpts: KeyringOptions = {
  service: "com.example.my-app",
  account: "default",
};
```

***

## ConfigChangeEvent

The event payload delivered to callbacks registered with `onChange()` and `watchExternal()`.

```typescript theme={null}
interface ConfigChangeEvent {
  fileName: string;
  operation: string;
  targetId: string;
}
```

<ResponseField name="fileName" type="string">
  The `fileName` of the config instance that changed.
</ResponseField>

<ResponseField name="operation" type="string">
  A string describing what happened. One of: `"create"`, `"save"`, `"patch"`, `"delete"`, `"reset"`, `"import"`, or `"external_change"`. The `"external_change"` value is only emitted by `watchExternal()`.
</ResponseField>

<ResponseField name="targetId" type="string">
  An opaque identifier that uniquely identifies the config target. You do not need to construct or compare this value directly; it is provided so you can distinguish which config instance triggered the event when multiple instances share a listener.
</ResponseField>

```typescript theme={null}
const unlisten = await config.onChange((event: ConfigChangeEvent) => {
  console.log(`${event.operation} on ${event.fileName}`);
  // "save on app.json"
});
```
