docs/shared-dict.md

# SharedDict API

SharedDict provides process-scoped shared dictionaries for bidirectional state sharing between Erlang and Python. Use it when you need to share configuration, caches, or session state within a single Python context.

## Overview

SharedDict is designed for scenarios where Erlang and Python need to share mutable state:

| Use Case | Description |
|----------|-------------|
| Configuration | Pass runtime config from Erlang, readable by Python |
| Session state | Maintain state across multiple Python calls |
| Caches | Share cached data between Erlang and Python |
| Coordination | Exchange data without serialization overhead |

SharedDict values are pickled for cross-interpreter safety, making them safe to use with Python subinterpreters.

## Quick Start

### Erlang Side

```erlang
%% Create a SharedDict
{ok, SD} = py:shared_dict_new(),

%% Set values
ok = py:shared_dict_set(SD, <<"config">>, #{host => <<"localhost">>, port => 8080}),

%% Get values
Config = py:shared_dict_get(SD, <<"config">>),
%% #{<<"host">> => <<"localhost">>, <<"port">> => 8080}

%% Get with default
Value = py:shared_dict_get(SD, <<"missing">>, default_value),
%% default_value

%% List keys
Keys = py:shared_dict_keys(SD),
%% [<<"config">>]

%% Delete a key
ok = py:shared_dict_del(SD, <<"config">>),

%% Explicit cleanup (optional - GC handles this)
ok = py:shared_dict_destroy(SD).
```

### Python Side

```python
from erlang import SharedDict

def process_with_config(sd_handle):
    # Wrap the handle
    sd = SharedDict(sd_handle)

    # Dict-like access
    config = sd['config']
    host = config.get('host') or config.get(b'host')

    # Set values
    sd['result'] = {'status': 'ok', 'count': 42}

    # Check membership
    if 'config' in sd:
        print("Config found")

    # Iterate keys
    for key in sd.keys():
        print(f"Key: {key}")

    # Delete
    del sd['result']
```

## Erlang API

### `py:shared_dict_new/0`

Create a new SharedDict owned by the calling process.

```erlang
{ok, SD} = py:shared_dict_new().
```

The SharedDict is automatically destroyed when the owning process terminates.

### `py:shared_dict_get/2,3`

Get a value from the SharedDict.

```erlang
Value = py:shared_dict_get(SD, <<"key">>).
%% Returns undefined if key not found

Value = py:shared_dict_get(SD, <<"key">>, default).
%% Returns default if key not found
```

### `py:shared_dict_set/3`

Set a value in the SharedDict.

```erlang
ok = py:shared_dict_set(SD, <<"key">>, Value).
```

Values are pickled internally for cross-interpreter safety.

### `py:shared_dict_del/2`

Delete a key from the SharedDict.

```erlang
ok = py:shared_dict_del(SD, <<"key">>).
```

Returns `ok` even if the key doesn't exist.

### `py:shared_dict_keys/1`

Get all keys from the SharedDict.

```erlang
Keys = py:shared_dict_keys(SD).
%% [<<"key1">>, <<"key2">>]
```

### `py:shared_dict_destroy/1`

Explicitly destroy a SharedDict.

```erlang
ok = py:shared_dict_destroy(SD).
```

After destruction, any operations on the SharedDict raise an error. This is idempotent - calling multiple times is safe.

## Python API

### `SharedDict` class

Dict-like wrapper for SharedDict handles passed from Erlang.

```python
from erlang import SharedDict

# Create from handle (passed via eval/exec locals)
sd = SharedDict(handle)
```

#### Subscript Access

```python
# Get value
value = sd['key']  # Raises KeyError if not found

# Set value
sd['key'] = value

# Delete
del sd['key']  # Raises KeyError if not found
```

#### `get(key, default=None)`

Get value with optional default.

```python
value = sd.get('key')  # Returns None if not found
value = sd.get('key', 'default')
```

#### `keys()`

Get all keys as a list.

```python
for key in sd.keys():
    process(key)
```

#### `__contains__`

Check if key exists.

```python
if 'key' in sd:
    process(sd['key'])
```

#### `destroy()`

Explicitly destroy the SharedDict from Python.

```python
sd.destroy()  # Invalidates all references
```

## Design

### Thread Safety

SharedDict uses a mutex to protect concurrent access. Multiple Python threads or Erlang processes can safely read/write the same SharedDict.

### Value Storage

Values are pickled (serialized) when stored and unpickled when retrieved. This ensures:

1. **Cross-interpreter safety** - Safe to use with Python subinterpreters
2. **Type preservation** - Python types round-trip correctly
3. **Isolation** - Changes to retrieved values don't affect stored values

### Lifecycle

SharedDicts follow two cleanup paths:

1. **Automatic (GC)** - When the owning Erlang process terminates, the SharedDict is marked for garbage collection
2. **Explicit** - Call `py:shared_dict_destroy/1` or `sd.destroy()` for immediate cleanup

Use explicit destroy when you need deterministic cleanup or want to release resources before process termination.

## Examples

### Configuration Passing

```erlang
%% Erlang: Set up config before Python call
{ok, SD} = py:shared_dict_new(),
ok = py:shared_dict_set(SD, <<"db">>, #{
    host => <<"localhost">>,
    port => 5432,
    user => <<"admin">>
}),

%% Pass handle to Python
{ok, _} = py:eval(<<"process_data(handle)">>, #{<<"handle">> => SD}).
```

```python
from erlang import SharedDict

def process_data(sd_handle):
    sd = SharedDict(sd_handle)
    db_config = sd['db']

    # Use config to connect to database
    conn = connect(
        host=db_config.get('host') or db_config.get(b'host'),
        port=db_config.get('port') or db_config.get(b'port')
    )

    # Store results back
    sd['results'] = {'rows_processed': 1000}
```

### Session State

```erlang
%% Create session state for a user
{ok, Session} = py:shared_dict_new(),
ok = py:shared_dict_set(Session, <<"user_id">>, UserId),
ok = py:shared_dict_set(Session, <<"cart">>, []),

%% Multiple Python calls share the session
{ok, _} = py:call(shop, add_to_cart, [Session, ItemId]),
{ok, _} = py:call(shop, add_to_cart, [Session, Item2Id]),

%% Read final state
Cart = py:shared_dict_get(Session, <<"cart">>),

%% Cleanup when done
ok = py:shared_dict_destroy(Session).
```

### Cache Sharing

```erlang
%% Create shared cache
{ok, Cache} = py:shared_dict_new(),

%% Python can populate the cache
ok = py:exec(<<"
from erlang import SharedDict
cache = SharedDict(handle)
cache['computed'] = expensive_computation()
">>,
ok = py:eval(<<"1">>, #{<<"handle">> => Cache}),

%% Erlang can read cached values
CachedValue = py:shared_dict_get(Cache, <<"computed">>).
```

## See Also

- [State API](state.md) - Global shared state (different scope)
- [Channel](channel.md) - Message passing between Erlang and Python
- [Getting Started](getting-started.md) - Basic usage guide