or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

array-operations.mdawareness.mddocument-management.mdindex.mdmap-operations.mdposition-undo.mdsynchronization.mdtext-operations.mdxml-support.md

map-operations.mddocs/

0

# Map Operations

1

2

## Overview

3

4

The `Map` type in pycrdt provides collaborative dictionary/map functionality with automatic conflict resolution across multiple clients. It supports a complete dict-like interface with additional collaborative features like change tracking, deep observation, and type-safe variants.

5

6

## Core Types

7

8

### Map

9

10

Collaborative map with dict-like interface and change tracking.

11

12

```python { .api }

13

class Map[T]:

14

def __init__(

15

self,

16

init: dict[str, T] | None = None,

17

*,

18

_doc: Doc | None = None,

19

_integrated: _Map | None = None,

20

) -> None:

21

"""

22

Create a new collaborative map.

23

24

Args:

25

init (dict, optional): Initial map contents

26

_doc (Doc, optional): Parent document

27

_integrated (_Map, optional): Native map instance

28

"""

29

30

# Dict-like interface

31

def __len__(self) -> int:

32

"""Get the number of key-value pairs in the map."""

33

34

def __str__(self) -> str:

35

"""Get string representation of the map."""

36

37

def __iter__(self) -> Iterable[str]:

38

"""Iterate over map keys."""

39

40

def __contains__(self, item: str) -> bool:

41

"""Check if key exists in map."""

42

43

def __getitem__(self, key: str) -> T:

44

"""Get value by key."""

45

46

def __setitem__(self, key: str, value: T) -> None:

47

"""Set value for key."""

48

49

def __delitem__(self, key: str) -> None:

50

"""Delete key-value pair."""

51

52

# Map manipulation methods

53

def get(self, key: str, default_value: T_DefaultValue = None) -> T | T_DefaultValue | None:

54

"""

55

Get value for key with optional default.

56

57

Args:

58

key (str): Key to lookup

59

default_value: Default value if key not found

60

61

Returns:

62

T | T_DefaultValue | None: Value or default

63

"""

64

65

def pop(self, key: str, default_value: T_DefaultValue = None) -> T | T_DefaultValue:

66

"""

67

Remove key and return its value.

68

69

Args:

70

key (str): Key to remove

71

default_value: Default value if key not found

72

73

Returns:

74

T | T_DefaultValue: Removed value or default

75

"""

76

77

def keys(self) -> Iterable[str]:

78

"""Get all keys in the map."""

79

80

def values(self) -> Iterable[T]:

81

"""Get all values in the map."""

82

83

def items(self) -> Iterable[tuple[str, T]]:

84

"""Get all key-value pairs as tuples."""

85

86

def clear(self) -> None:

87

"""Remove all key-value pairs from the map."""

88

89

def update(self, value: dict[str, T]) -> None:

90

"""

91

Update map with key-value pairs from another dict.

92

93

Args:

94

value (dict): Dictionary to merge into this map

95

"""

96

97

def to_py(self) -> dict[str, T] | None:

98

"""

99

Convert map to a Python dictionary.

100

101

Returns:

102

dict | None: Map contents as dict, or None if empty

103

"""

104

105

def observe(self, callback: Callable[[MapEvent], None]) -> Subscription:

106

"""

107

Observe map changes.

108

109

Args:

110

callback: Function called when map changes occur

111

112

Returns:

113

Subscription: Handle for unsubscribing

114

"""

115

116

def observe_deep(self, callback: Callable[[list[MapEvent]], None]) -> Subscription:

117

"""

118

Observe deep changes including nested structures.

119

120

Args:

121

callback: Function called with list of change events

122

123

Returns:

124

Subscription: Handle for unsubscribing

125

"""

126

127

def unobserve(self, subscription: Subscription) -> None:

128

"""

129

Remove an event observer.

130

131

Args:

132

subscription: Subscription handle to remove

133

"""

134

135

async def events(

136

self,

137

deep: bool = False,

138

max_buffer_size: float = float("inf")

139

) -> MemoryObjectReceiveStream:

140

"""

141

Get an async stream of map events.

142

143

Args:

144

deep (bool): Include deep change events

145

max_buffer_size (float): Maximum event buffer size

146

147

Returns:

148

MemoryObjectReceiveStream: Async event stream

149

"""

150

```

151

152

### MapEvent

153

154

Event emitted when map changes occur.

155

156

```python { .api }

157

class MapEvent:

158

@property

159

def target(self) -> Map:

160

"""Get the map that changed."""

161

162

@property

163

def keys(self) -> list[str]:

164

"""Get the list of keys that changed."""

165

166

@property

167

def path(self) -> list[int | str]:

168

"""Get the path to the changed map within the document structure."""

169

```

170

171

### TypedMap

172

173

Type-safe wrapper for Map with typed attributes.

174

175

```python { .api }

176

class TypedMap:

177

"""

178

Type-safe map container with runtime type checking.

179

180

Usage:

181

class UserMap(TypedMap):

182

name: str

183

age: int

184

active: bool

185

186

user = UserMap()

187

user.name = "Alice" # Type-safe

188

user.age = 30 # Type-safe

189

name: str = user.name # Typed access

190

"""

191

```

192

193

## Usage Examples

194

195

### Basic Map Operations

196

197

```python

198

from pycrdt import Doc, Map

199

200

doc = Doc()

201

config = doc.get("config", type=Map)

202

203

# Basic dict operations

204

config["theme"] = "dark"

205

config["font_size"] = 14

206

config["auto_save"] = True

207

print(len(config)) # 3

208

209

# Dict-like access

210

print(config["theme"]) # "dark"

211

print(config.get("missing")) # None

212

print(config.get("missing", "default")) # "default"

213

214

# Check keys

215

print("theme" in config) # True

216

print("color" in config) # False

217

218

# Iteration

219

for key in config:

220

print(f"{key}: {config[key]}")

221

222

for key, value in config.items():

223

print(f"{key} = {value}")

224

```

225

226

### Map Manipulation

227

228

```python

229

from pycrdt import Doc, Map

230

231

doc = Doc()

232

settings = doc.get("settings", type=Map)

233

234

# Build settings

235

settings.update({

236

"width": 800,

237

"height": 600,

238

"fullscreen": False,

239

"vsync": True

240

})

241

242

# Modify individual settings

243

settings["width"] = 1024

244

settings["height"] = 768

245

246

# Remove settings

247

old_value = settings.pop("vsync")

248

print(f"Removed vsync: {old_value}")

249

250

# Remove with default

251

fps_limit = settings.pop("fps_limit", 60)

252

print(f"FPS limit: {fps_limit}")

253

254

# Delete by key

255

del settings["fullscreen"]

256

257

# Check current state

258

print(f"Current settings: {dict(settings.items())}")

259

```

260

261

### Nested Data Structures

262

263

```python

264

from pycrdt import Doc, Map, Array

265

266

doc = Doc()

267

user_profile = doc.get("profile", type=Map)

268

269

# Create nested structure

270

user_profile["personal"] = Map()

271

user_profile["personal"]["name"] = "Alice"

272

user_profile["personal"]["email"] = "alice@example.com"

273

274

user_profile["preferences"] = Map()

275

user_profile["preferences"]["theme"] = "dark"

276

user_profile["preferences"]["notifications"] = True

277

278

user_profile["tags"] = Array()

279

user_profile["tags"].extend(["developer", "python", "collaborative"])

280

281

# Access nested data

282

print(user_profile["personal"]["name"]) # "Alice"

283

print(user_profile["preferences"]["theme"]) # "dark"

284

print(list(user_profile["tags"])) # ["developer", "python", "collaborative"]

285

286

# Modify nested structures

287

user_profile["personal"]["age"] = 30

288

user_profile["tags"].append("crdt")

289

```

290

291

### Type-Safe Maps

292

293

```python

294

from pycrdt import TypedMap, Doc

295

296

class UserProfile(TypedMap):

297

name: str

298

age: int

299

email: str

300

active: bool

301

302

class DatabaseConfig(TypedMap):

303

host: str

304

port: int

305

ssl_enabled: bool

306

timeout: float

307

308

doc = Doc()

309

310

# Create typed maps

311

user = UserProfile()

312

db_config = DatabaseConfig()

313

314

# Type-safe operations

315

user.name = "Bob" # OK

316

user.age = 25 # OK

317

user.email = "bob@test.com" # OK

318

user.active = True # OK

319

320

db_config.host = "localhost"

321

db_config.port = 5432

322

db_config.ssl_enabled = True

323

db_config.timeout = 30.0

324

325

try:

326

user.age = "not a number" # May raise TypeError

327

except TypeError as e:

328

print(f"Type error: {e}")

329

330

# Typed access

331

name: str = user.name # Typed

332

port: int = db_config.port # Typed

333

```

334

335

### Event Observation

336

337

```python

338

from pycrdt import Doc, Map, MapEvent

339

340

doc = Doc()

341

data = doc.get("data", type=Map)

342

343

def on_map_change(event: MapEvent):

344

print(f"Map changed: {event.target}")

345

print(f"Changed keys: {event.keys}")

346

print(f"Path: {event.path}")

347

348

# Subscribe to changes

349

subscription = data.observe(on_map_change)

350

351

# Make changes to trigger events

352

data["key1"] = "value1"

353

data["key2"] = "value2"

354

data.update({"key3": "value3", "key4": "value4"})

355

del data["key1"]

356

357

# Clean up

358

data.unobserve(subscription)

359

```

360

361

### Deep Event Observation

362

363

```python

364

from pycrdt import Doc, Map, Array

365

366

doc = Doc()

367

root = doc.get("root", type=Map)

368

369

# Create nested structure

370

root["level1"] = Map()

371

root["level1"]["level2"] = Map()

372

root["level1"]["level2"]["data"] = Array()

373

374

def on_deep_change(events):

375

print(f"Deep changes detected: {len(events)} events")

376

for event in events:

377

if hasattr(event, 'keys'): # MapEvent

378

print(f" Map change at {event.path}: keys {event.keys}")

379

elif hasattr(event, 'delta'): # ArrayEvent

380

print(f" Array change at {event.path}: {event.delta}")

381

382

# Subscribe to deep changes

383

subscription = root.observe_deep(on_deep_change)

384

385

# Make nested changes

386

root["level1"]["level2"]["data"].append("item1")

387

root["level1"]["level2"]["new_key"] = "new_value"

388

root["level1"]["another_map"] = Map()

389

390

# Clean up

391

root.unobserve(subscription)

392

```

393

394

### Async Event Streaming

395

396

```python

397

import anyio

398

from pycrdt import Doc, Map

399

400

async def monitor_map_changes(map_obj: Map):

401

async with map_obj.events() as event_stream:

402

async for event in event_stream:

403

print(f"Map event: keys {event.keys}")

404

405

doc = Doc()

406

config = doc.get("config", type=Map)

407

408

async def main():

409

async with anyio.create_task_group() as tg:

410

tg.start_soon(monitor_map_changes, config)

411

412

# Make changes

413

await anyio.sleep(0.1)

414

config["setting1"] = "value1"

415

await anyio.sleep(0.1)

416

config.update({"setting2": "value2", "setting3": "value3"})

417

await anyio.sleep(0.1)

418

419

anyio.run(main)

420

```

421

422

### Collaborative Map Editing

423

424

```python

425

from pycrdt import Doc, Map

426

427

# Simulate two clients editing the same map

428

doc1 = Doc(client_id=1)

429

doc2 = Doc(client_id=2)

430

431

config1 = doc1.get("shared_config", type=Map)

432

config2 = doc2.get("shared_config", type=Map)

433

434

# Client 1 sets initial configuration

435

with doc1.transaction(origin="client1"):

436

config1.update({

437

"theme": "light",

438

"font_size": 12,

439

"auto_save": True

440

})

441

442

# Sync to client 2

443

update = doc1.get_update()

444

doc2.apply_update(update)

445

print(f"Client 2 config: {dict(config2.items())}")

446

447

# Client 2 makes concurrent changes

448

with doc2.transaction(origin="client2"):

449

config2["theme"] = "dark" # Conflict with client 1

450

config2["line_numbers"] = True # New setting

451

config2["font_size"] = 14 # Different value

452

453

# Both clients make more changes

454

with doc1.transaction(origin="client1"):

455

config1["word_wrap"] = True # New setting from client 1

456

457

with doc2.transaction(origin="client2"):

458

config2["auto_save"] = False # Change existing setting

459

460

# Sync changes

461

update1 = doc1.get_update(doc2.get_state())

462

update2 = doc2.get_update(doc1.get_state())

463

464

doc2.apply_update(update1)

465

doc1.apply_update(update2)

466

467

# Both clients now have consistent state

468

print(f"Client 1 final: {dict(config1.items())}")

469

print(f"Client 2 final: {dict(config2.items())}")

470

```

471

472

### Complex Data Management

473

474

```python

475

from pycrdt import Doc, Map, Array

476

477

doc = Doc()

478

database = doc.get("database", type=Map)

479

480

# Create complex data structure

481

database["users"] = Map()

482

database["posts"] = Map()

483

database["comments"] = Map()

484

485

# Add users

486

users = database["users"]

487

users["1"] = Map()

488

users["1"].update({"name": "Alice", "email": "alice@test.com", "posts": []})

489

490

users["2"] = Map()

491

users["2"].update({"name": "Bob", "email": "bob@test.com", "posts": []})

492

493

# Add posts

494

posts = database["posts"]

495

posts["1"] = Map()

496

posts["1"].update({

497

"title": "First Post",

498

"content": "Hello, world!",

499

"author_id": "1",

500

"comments": []

501

})

502

503

posts["2"] = Map()

504

posts["2"].update({

505

"title": "Second Post",

506

"content": "More content",

507

"author_id": "2",

508

"comments": []

509

})

510

511

# Link posts to users

512

users["1"]["posts"].append("1")

513

users["2"]["posts"].append("2")

514

515

# Add comments

516

comments = database["comments"]

517

comments["1"] = Map()

518

comments["1"].update({

519

"content": "Great post!",

520

"author_id": "2",

521

"post_id": "1"

522

})

523

524

posts["1"]["comments"].append("1")

525

526

# Query operations

527

def get_user_posts(database: Map, user_id: str) -> list:

528

"""Get all posts by a user."""

529

user = database["users"][user_id]

530

post_ids = user["posts"]

531

posts_data = []

532

533

for post_id in post_ids:

534

post = database["posts"][post_id]

535

posts_data.append({

536

"id": post_id,

537

"title": post["title"],

538

"content": post["content"]

539

})

540

541

return posts_data

542

543

def get_post_with_comments(database: Map, post_id: str) -> dict:

544

"""Get post with all its comments."""

545

post = database["posts"][post_id]

546

comment_ids = post["comments"]

547

548

comments_data = []

549

for comment_id in comment_ids:

550

comment = database["comments"][comment_id]

551

author = database["users"][comment["author_id"]]

552

comments_data.append({

553

"content": comment["content"],

554

"author": author["name"]

555

})

556

557

return {

558

"title": post["title"],

559

"content": post["content"],

560

"comments": comments_data

561

}

562

563

# Use query operations

564

alice_posts = get_user_posts(database, "1")

565

print(f"Alice's posts: {alice_posts}")

566

567

first_post = get_post_with_comments(database, "1")

568

print(f"First post with comments: {first_post}")

569

```

570

571

### Map Serialization and Persistence

572

573

```python

574

import json

575

from pycrdt import Doc, Map, Array

576

577

def serialize_map(map_obj: Map) -> dict:

578

"""Serialize a collaborative map to JSON-compatible dict."""

579

result = {}

580

for key, value in map_obj.items():

581

if isinstance(value, Map):

582

result[key] = serialize_map(value)

583

elif isinstance(value, Array):

584

result[key] = serialize_array(value)

585

else:

586

result[key] = value

587

return result

588

589

def serialize_array(array_obj: Array) -> list:

590

"""Serialize a collaborative array to JSON-compatible list."""

591

result = []

592

for item in array_obj:

593

if isinstance(item, Map):

594

result.append(serialize_map(item))

595

elif isinstance(item, Array):

596

result.append(serialize_array(item))

597

else:

598

result.append(item)

599

return result

600

601

def deserialize_to_map(data: dict, doc: Doc) -> Map:

602

"""Deserialize dict to collaborative map."""

603

map_obj = Map()

604

for key, value in data.items():

605

if isinstance(value, dict):

606

map_obj[key] = deserialize_to_map(value, doc)

607

elif isinstance(value, list):

608

map_obj[key] = deserialize_to_array(value, doc)

609

else:

610

map_obj[key] = value

611

return map_obj

612

613

def deserialize_to_array(data: list, doc: Doc) -> Array:

614

"""Deserialize list to collaborative array."""

615

array_obj = Array()

616

for item in data:

617

if isinstance(item, dict):

618

array_obj.append(deserialize_to_map(item, doc))

619

elif isinstance(item, list):

620

array_obj.append(deserialize_to_array(item, doc))

621

else:

622

array_obj.append(item)

623

return array_obj

624

625

# Example usage

626

doc = Doc()

627

config = doc.get("config", type=Map)

628

629

# Build complex configuration

630

config["database"] = Map()

631

config["database"]["host"] = "localhost"

632

config["database"]["port"] = 5432

633

634

config["features"] = Array()

635

config["features"].extend(["auth", "logging", "caching"])

636

637

# Serialize to JSON

638

config_dict = serialize_map(config)

639

json_str = json.dumps(config_dict, indent=2)

640

print(f"Serialized config:\n{json_str}")

641

642

# Deserialize back

643

loaded_data = json.loads(json_str)

644

new_doc = Doc()

645

restored_config = deserialize_to_map(loaded_data, new_doc)

646

print(f"Restored config: {dict(restored_config.items())}")

647

```

648

649

## Error Handling

650

651

```python

652

from pycrdt import Doc, Map

653

654

doc = Doc()

655

data = doc.get("data", type=Map)

656

657

try:

658

# Key not found

659

value = data["nonexistent"] # May raise KeyError

660

661

# Invalid operations

662

del data["nonexistent"] # May raise KeyError

663

664

# Type mismatches in typed maps

665

class StrictMap(TypedMap):

666

number_field: int

667

668

strict_map = StrictMap()

669

strict_map.number_field = "string" # May raise TypeError

670

671

except (KeyError, TypeError, ValueError) as e:

672

print(f"Map operation failed: {e}")

673

674

# Safe operations

675

value = data.get("nonexistent", "default") # Returns "default"

676

removed = data.pop("nonexistent", None) # Returns None

677

```