or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

document-management.mdevent-system.mdindex.mdposition-tracking.mdshared-data-types.mdsnapshot-system.mdsynchronization.mdtransaction-system.mdundo-redo-system.mdxml-types.md

position-tracking.mddocs/

0

# Position Tracking

1

2

Position system that maintains references across concurrent edits and structural changes. Yjs provides both relative and absolute position types that survive concurrent modifications from multiple users.

3

4

## Capabilities

5

6

### RelativePosition

7

8

Position that remains valid across concurrent document changes by referencing document structure.

9

10

```typescript { .api }

11

/**

12

* Position that remains stable across concurrent modifications

13

*/

14

class RelativePosition {

15

/** Type ID reference or null */

16

readonly type: ID | null;

17

18

/** Type name reference or null */

19

readonly tname: string | null;

20

21

/** Item ID reference or null */

22

readonly item: ID | null;

23

24

/** Association direction (-1, 0, or 1) */

25

readonly assoc: number;

26

}

27

```

28

29

### AbsolutePosition

30

31

Position resolved to a specific index within a type at a given document state.

32

33

```typescript { .api }

34

/**

35

* Position resolved to specific index within a type

36

*/

37

class AbsolutePosition {

38

/** The type containing this position */

39

readonly type: AbstractType<any>;

40

41

/** Index within the type */

42

readonly index: number;

43

44

/** Association direction (-1, 0, or 1) */

45

readonly assoc: number;

46

}

47

```

48

49

### Creating Relative Positions

50

51

Functions for creating relative positions from types and indices.

52

53

```typescript { .api }

54

/**

55

* Create relative position from type and index

56

* @param type - Type to create position in

57

* @param index - Index within the type

58

* @param assoc - Association direction (default: 0)

59

* @returns RelativePosition that tracks this location

60

*/

61

function createRelativePositionFromTypeIndex(

62

type: AbstractType<any>,

63

index: number,

64

assoc?: number

65

): RelativePosition;

66

67

/**

68

* Create relative position from JSON representation

69

* @param json - JSON object representing position

70

* @returns RelativePosition instance

71

*/

72

function createRelativePositionFromJSON(json: any): RelativePosition;

73

```

74

75

**Usage Examples:**

76

77

```typescript

78

import * as Y from "yjs";

79

80

const doc1 = new Y.Doc();

81

const ytext1 = doc1.getText("document");

82

ytext1.insert(0, "Hello World!");

83

84

// Create relative position at index 6 (before "World")

85

const relPos = Y.createRelativePositionFromTypeIndex(ytext1, 6);

86

87

// Position remains valid after other users make changes

88

const doc2 = new Y.Doc();

89

const ytext2 = doc2.getText("document");

90

91

// Simulate receiving updates from another user

92

const update1 = Y.encodeStateAsUpdate(doc1);

93

Y.applyUpdate(doc2, update1);

94

95

// Other user inserts text at beginning

96

ytext2.insert(0, "Hi! ");

97

98

// Create update and apply back to doc1

99

const update2 = Y.encodeStateAsUpdate(doc2);

100

Y.applyUpdate(doc1, update2);

101

102

// Original position still points to correct location

103

const absPos = Y.createAbsolutePositionFromRelativePosition(relPos, doc1);

104

console.log("Position now at index:", absPos?.index); // Adjusted index

105

```

106

107

### Converting Between Position Types

108

109

Functions for converting between relative and absolute positions.

110

111

```typescript { .api }

112

/**

113

* Convert relative position to absolute position

114

* @param rpos - Relative position to convert

115

* @param doc - Document to resolve position in

116

* @returns AbsolutePosition or null if position cannot be resolved

117

*/

118

function createAbsolutePositionFromRelativePosition(

119

rpos: RelativePosition,

120

doc: Doc

121

): AbsolutePosition | null;

122

123

/**

124

* Compare two relative positions for equality

125

* @param a - First relative position

126

* @param b - Second relative position

127

* @returns True if positions are equal

128

*/

129

function compareRelativePositions(a: RelativePosition | null, b: RelativePosition | null): boolean;

130

```

131

132

**Usage Examples:**

133

134

```typescript

135

import * as Y from "yjs";

136

137

const doc = new Y.Doc();

138

const ytext = doc.getText("document");

139

ytext.insert(0, "Hello World!");

140

141

// Create multiple relative positions

142

const pos1 = Y.createRelativePositionFromTypeIndex(ytext, 0); // Start

143

const pos2 = Y.createRelativePositionFromTypeIndex(ytext, 6); // Before "World"

144

const pos3 = Y.createRelativePositionFromTypeIndex(ytext, ytext.length); // End

145

146

// Convert to absolute positions

147

const abs1 = Y.createAbsolutePositionFromRelativePosition(pos1, doc);

148

const abs2 = Y.createAbsolutePositionFromRelativePosition(pos2, doc);

149

const abs3 = Y.createAbsolutePositionFromRelativePosition(pos3, doc);

150

151

console.log("Start position:", abs1?.index); // 0

152

console.log("Middle position:", abs2?.index); // 6

153

console.log("End position:", abs3?.index); // 12

154

155

// Compare positions

156

console.log("pos1 equals pos2:", Y.compareRelativePositions(pos1, pos2)); // false

157

console.log("pos1 equals pos1:", Y.compareRelativePositions(pos1, pos1)); // true

158

```

159

160

### Position Serialization

161

162

Functions for serializing and deserializing relative positions.

163

164

```typescript { .api }

165

/**

166

* Encode relative position to binary format

167

* @param rpos - Relative position to encode

168

* @returns Binary representation as Uint8Array

169

*/

170

function encodeRelativePosition(rpos: RelativePosition): Uint8Array;

171

172

/**

173

* Decode relative position from binary format

174

* @param uint8Array - Binary data to decode

175

* @returns RelativePosition instance

176

*/

177

function decodeRelativePosition(uint8Array: Uint8Array): RelativePosition;

178

179

/**

180

* Convert relative position to JSON format

181

* @param rpos - Relative position to convert

182

* @returns JSON representation

183

*/

184

function relativePositionToJSON(rpos: RelativePosition): any;

185

```

186

187

**Usage Examples:**

188

189

```typescript

190

import * as Y from "yjs";

191

192

const doc = new Y.Doc();

193

const ytext = doc.getText("document");

194

ytext.insert(0, "Hello World!");

195

196

const relPos = Y.createRelativePositionFromTypeIndex(ytext, 6);

197

198

// Serialize to binary

199

const binary = Y.encodeRelativePosition(relPos);

200

console.log("Binary size:", binary.length);

201

202

// Deserialize from binary

203

const restoredPos = Y.decodeRelativePosition(binary);

204

205

// Serialize to JSON

206

const json = Y.relativePositionToJSON(relPos);

207

console.log("JSON:", json);

208

209

// Restore from JSON

210

const posFromJSON = Y.createRelativePositionFromJSON(json);

211

212

// All positions should be equivalent

213

console.log("Original equals restored:", Y.compareRelativePositions(relPos, restoredPos));

214

console.log("Original equals from JSON:", Y.compareRelativePositions(relPos, posFromJSON));

215

```

216

217

### Position Association

218

219

Association determines behavior when content is inserted exactly at the position.

220

221

```typescript { .api }

222

/**

223

* Association values:

224

* -1: Position moves left when content inserted at this location

225

* 0: Default behavior

226

* 1: Position moves right when content inserted at this location

227

*/

228

type PositionAssociation = -1 | 0 | 1;

229

```

230

231

**Usage Examples:**

232

233

```typescript

234

import * as Y from "yjs";

235

236

const doc = new Y.Doc();

237

const ytext = doc.getText("document");

238

ytext.insert(0, "Hello");

239

240

// Create positions with different associations at index 5

241

const leftAssoc = Y.createRelativePositionFromTypeIndex(ytext, 5, -1);

242

const defaultAssoc = Y.createRelativePositionFromTypeIndex(ytext, 5, 0);

243

const rightAssoc = Y.createRelativePositionFromTypeIndex(ytext, 5, 1);

244

245

// Insert text at position 5

246

ytext.insert(5, " World");

247

248

// Check where positions ended up

249

const leftAbs = Y.createAbsolutePositionFromRelativePosition(leftAssoc, doc);

250

const defaultAbs = Y.createAbsolutePositionFromRelativePosition(defaultAssoc, doc);

251

const rightAbs = Y.createAbsolutePositionFromRelativePosition(rightAssoc, doc);

252

253

console.log("Left association:", leftAbs?.index); // 5 (before inserted content)

254

console.log("Default association:", defaultAbs?.index); // 5 or 11 (implementation dependent)

255

console.log("Right association:", rightAbs?.index); // 11 (after inserted content)

256

```

257

258

### Advanced Position Tracking

259

260

**Cursor Tracking:**

261

262

```typescript

263

import * as Y from "yjs";

264

265

class CursorTracker {

266

private doc: Y.Doc;

267

private ytext: Y.Text;

268

private positions: Map<string, Y.RelativePosition>;

269

270

constructor(doc: Y.Doc, textName: string) {

271

this.doc = doc;

272

this.ytext = doc.getText(textName);

273

this.positions = new Map();

274

}

275

276

setCursor(userId: string, index: number) {

277

const relPos = Y.createRelativePositionFromTypeIndex(this.ytext, index);

278

this.positions.set(userId, relPos);

279

}

280

281

getCursor(userId: string): number | null {

282

const relPos = this.positions.get(userId);

283

if (!relPos) return null;

284

285

const absPos = Y.createAbsolutePositionFromRelativePosition(relPos, this.doc);

286

return absPos?.index ?? null;

287

}

288

289

getAllCursors(): Map<string, number> {

290

const cursors = new Map<string, number>();

291

292

this.positions.forEach((relPos, userId) => {

293

const absPos = Y.createAbsolutePositionFromRelativePosition(relPos, this.doc);

294

if (absPos) {

295

cursors.set(userId, absPos.index);

296

}

297

});

298

299

return cursors;

300

}

301

}

302

303

// Usage

304

const doc = new Y.Doc();

305

const tracker = new CursorTracker(doc, "document");

306

307

tracker.setCursor("alice", 10);

308

tracker.setCursor("bob", 20);

309

310

// Cursors automatically adjust as document changes

311

const ytext = doc.getText("document");

312

ytext.insert(0, "Prefix ");

313

314

console.log("Alice cursor:", tracker.getCursor("alice")); // Adjusted position

315

console.log("Bob cursor:", tracker.getCursor("bob")); // Adjusted position

316

```

317

318

**Selection Ranges:**

319

320

```typescript

321

import * as Y from "yjs";

322

323

interface SelectionRange {

324

start: Y.RelativePosition;

325

end: Y.RelativePosition;

326

userId: string;

327

}

328

329

class SelectionTracker {

330

private doc: Y.Doc;

331

private selections: Map<string, SelectionRange>;

332

333

constructor(doc: Y.Doc) {

334

this.doc = doc;

335

this.selections = new Map();

336

}

337

338

setSelection(userId: string, type: Y.AbstractType<any>, startIndex: number, endIndex: number) {

339

const start = Y.createRelativePositionFromTypeIndex(type, startIndex);

340

const end = Y.createRelativePositionFromTypeIndex(type, endIndex);

341

342

this.selections.set(userId, { start, end, userId });

343

}

344

345

getSelection(userId: string): { start: number; end: number } | null {

346

const selection = this.selections.get(userId);

347

if (!selection) return null;

348

349

const startAbs = Y.createAbsolutePositionFromRelativePosition(selection.start, this.doc);

350

const endAbs = Y.createAbsolutePositionFromRelativePosition(selection.end, this.doc);

351

352

if (!startAbs || !endAbs) return null;

353

354

return {

355

start: startAbs.index,

356

end: endAbs.index

357

};

358

}

359

}

360

```

361

362

**Position Persistence:**

363

364

```typescript

365

import * as Y from "yjs";

366

367

// Save positions to storage

368

function savePositions(positions: Map<string, Y.RelativePosition>): string {

369

const serialized = Array.from(positions.entries()).map(([key, pos]) => ({

370

key,

371

position: Y.relativePositionToJSON(pos)

372

}));

373

374

return JSON.stringify(serialized);

375

}

376

377

// Restore positions from storage

378

function loadPositions(data: string): Map<string, Y.RelativePosition> {

379

const serialized = JSON.parse(data);

380

const positions = new Map<string, Y.RelativePosition>();

381

382

serialized.forEach(({ key, position }) => {

383

const relPos = Y.createRelativePositionFromJSON(position);

384

positions.set(key, relPos);

385

});

386

387

return positions;

388

}

389

390

// Usage

391

const doc = new Y.Doc();

392

const ytext = doc.getText("document");

393

ytext.insert(0, "Sample text");

394

395

const positions = new Map();

396

positions.set("cursor1", Y.createRelativePositionFromTypeIndex(ytext, 7));

397

positions.set("cursor2", Y.createRelativePositionFromTypeIndex(ytext, 12));

398

399

// Save to storage

400

const saved = savePositions(positions);

401

localStorage.setItem("positions", saved);

402

403

// Later: restore from storage

404

const restored = loadPositions(localStorage.getItem("positions")!);

405

406

// Positions remain valid across sessions

407

restored.forEach((relPos, key) => {

408

const absPos = Y.createAbsolutePositionFromRelativePosition(relPos, doc);

409

console.log(`${key} at index:`, absPos?.index);

410

});

411

```