tangled
alpha
login
or
join now
atscan.net
/
plcbundle
A Transparent and Verifiable Way to Sync the AT Protocol's PLC Directory
14
fork
atom
overview
issues
2
pulls
pipelines
fix warnings
tree.fail
3 months ago
dc3ea2a9
a30138d8
1/1
build.yml
success
42s
+238
-147
13 changed files
expand all
collapse all
unified
split
bundle
manager.go
cmd
plcbundle
commands
detector.go
did.go
export.go
log.go
mempool.go
rollback.go
stream.go
internal
didindex
builder.go
lookup.go
manager.go
manager_test.go
types.go
+6
-8
bundle/manager.go
···
369
369
}
370
370
371
371
// loadBundleFromDisk loads a bundle from disk
372
372
-
func (m *Manager) loadBundleFromDisk(ctx context.Context, bundleNumber int) (*Bundle, error) {
372
372
+
func (m *Manager) loadBundleFromDisk(_ context.Context, bundleNumber int) (*Bundle, error) {
373
373
// Get metadata from index
374
374
meta, err := m.index.GetBundle(bundleNumber)
375
375
if err != nil {
···
1321
1321
// Find latest non-nullified location
1322
1322
var latestLoc *didindex.OpLocation
1323
1323
for i := range locations {
1324
1324
-
if locations[i].Nullified {
1324
1324
+
if locations[i].Nullified() {
1325
1325
continue
1326
1326
}
1327
1327
-
if latestLoc == nil ||
1328
1328
-
locations[i].Bundle > latestLoc.Bundle ||
1329
1329
-
(locations[i].Bundle == latestLoc.Bundle && locations[i].Position > latestLoc.Position) {
1327
1327
+
if latestLoc == nil || locations[i].IsAfter(*latestLoc) {
1330
1328
latestLoc = &locations[i]
1331
1329
}
1332
1330
}
···
1337
1335
1338
1336
// STEP 3: Load operation
1339
1337
opStart := time.Now()
1340
1340
-
op, err := m.LoadOperation(ctx, int(latestLoc.Bundle), int(latestLoc.Position))
1338
1338
+
op, err := m.LoadOperation(ctx, latestLoc.BundleInt(), latestLoc.PositionInt())
1341
1339
result.LoadOpTime = time.Since(opStart)
1342
1340
1343
1341
if err != nil {
1344
1342
return nil, fmt.Errorf("failed to load operation: %w", err)
1345
1343
}
1346
1344
1347
1347
-
result.BundleNumber = int(latestLoc.Bundle)
1348
1348
-
result.Position = int(latestLoc.Position)
1345
1345
+
result.BundleNumber = latestLoc.BundleInt()
1346
1346
+
result.Position = latestLoc.PositionInt()
1349
1347
1350
1348
// STEP 4: Resolve document
1351
1349
doc, err := plcclient.ResolveDIDDocument(did, []plcclient.PLCOperation{*op})
+1
-1
cmd/plcbundle/commands/detector.go
···
62
62
`)
63
63
}
64
64
65
65
-
func detectorList(args []string) error {
65
65
+
func detectorList(_ []string) error {
66
66
registry := detector.DefaultRegistry()
67
67
detectors := registry.List()
68
68
+3
-3
cmd/plcbundle/commands/did.go
···
795
795
return nil
796
796
}
797
797
798
798
-
func batchLookup(mgr BundleManager, dids []string, output *os.File, workers int) error {
798
798
+
func batchLookup(mgr BundleManager, dids []string, output *os.File, _ int) error {
799
799
progress := ui.NewProgressBar(len(dids))
800
800
ctx := context.Background()
801
801
···
855
855
return nil
856
856
}
857
857
858
858
-
func batchResolve(mgr BundleManager, dids []string, output *os.File, workers int) error {
858
858
+
func batchResolve(mgr BundleManager, dids []string, output *os.File, _ int) error {
859
859
progress := ui.NewProgressBar(len(dids))
860
860
ctx := context.Background()
861
861
···
900
900
return nil
901
901
}
902
902
903
903
-
func batchExport(mgr BundleManager, dids []string, output *os.File, workers int) error {
903
903
+
func batchExport(mgr BundleManager, dids []string, output *os.File, _ int) error {
904
904
progress := ui.NewProgressBar(len(dids))
905
905
ctx := context.Background()
906
906
+12
-8
cmd/plcbundle/commands/export.go
···
24
24
}
25
25
26
26
if !*all && *bundles == "" {
27
27
-
return fmt.Errorf("usage: plcbundle export --bundles <number|range> [options]\n" +
28
28
-
" or: plcbundle export --all [options]\n\n" +
29
29
-
"Examples:\n" +
30
30
-
" plcbundle export --bundles 42\n" +
31
31
-
" plcbundle export --bundles 1-100\n" +
32
32
-
" plcbundle export --all\n" +
33
33
-
" plcbundle export --all --count 50000\n" +
34
34
-
" plcbundle export --bundles 42 | jq .")
27
27
+
fmt.Fprint(os.Stderr, `usage: plcbundle export --bundles <number|range> [options]
28
28
+
or: plcbundle export --all [options]
29
29
+
30
30
+
Examples:
31
31
+
plcbundle export --bundles 42
32
32
+
plcbundle export --bundles 1-100
33
33
+
plcbundle export --all
34
34
+
plcbundle export --all --count 50000
35
35
+
plcbundle export --bundles 42 | jq .
36
36
+
37
37
+
`)
38
38
+
return fmt.Errorf("missing required flag: --bundles or --all")
35
39
}
36
40
37
41
mgr, _, err := getManager(&ManagerOptions{Cmd: nil})
+6
-6
cmd/plcbundle/commands/log.go
···
197
197
// Display bundles
198
198
for i, meta := range displayBundles {
199
199
if opts.oneline {
200
200
-
displayBundleOneLine(w, meta, opts.showHashes, useColor, colorBundleNum, colorHash, colorDate, colorAge, colorSize, colorReset)
200
200
+
displayBundleOneLine(w, meta, opts.showHashes, colorBundleNum, colorHash, colorDate, colorAge, colorSize, colorReset)
201
201
} else {
202
202
-
displayBundleDetailed(w, meta, opts.showHashes, useColor, colorBundleNum, colorHash, colorDate, colorAge, colorSize, colorDim, colorReset)
202
202
+
displayBundleDetailed(w, meta, opts.showHashes, colorBundleNum, colorHash, colorDate, colorAge, colorSize, colorDim, colorReset)
203
203
204
204
// Add separator between bundles (except last)
205
205
if i < len(displayBundles)-1 {
···
212
212
// Summary footer
213
213
if !opts.oneline && len(displayBundles) > 0 {
214
214
fmt.Fprintf(w, "\n")
215
215
-
displayLogSummary(w, allBundles, displayBundles, opts.last, useColor, colorHeader, colorReset)
215
215
+
displayLogSummary(w, allBundles, displayBundles, opts.last, colorHeader, colorReset)
216
216
}
217
217
}
218
218
219
219
-
func displayBundleOneLine(w io.Writer, meta *bundleindex.BundleMetadata, showHashes bool, useColor bool, colorBundle, colorHash, colorDate, colorAge, colorSize, colorReset string) {
219
219
+
func displayBundleOneLine(w io.Writer, meta *bundleindex.BundleMetadata, showHashes bool, colorBundle, colorHash, colorDate, colorAge, colorSize, colorReset string) {
220
220
age := time.Since(meta.EndTime)
221
221
ageStr := formatDurationShort(age)
222
222
···
236
236
colorSize, formatBytes(meta.CompressedSize), colorReset)
237
237
}
238
238
239
239
-
func displayBundleDetailed(w io.Writer, meta *bundleindex.BundleMetadata, showHashes bool, useColor bool, colorBundle, colorHash, colorDate, colorAge, colorSize, colorDim, colorReset string) {
239
239
+
func displayBundleDetailed(w io.Writer, meta *bundleindex.BundleMetadata, showHashes bool, colorBundle, colorHash, colorDate, colorAge, colorSize, colorDim, colorReset string) {
240
240
fmt.Fprintf(w, "%sBundle %06d%s\n", colorBundle, meta.BundleNumber, colorReset)
241
241
242
242
// Timestamp and age
···
282
282
}
283
283
}
284
284
285
285
-
func displayLogSummary(w io.Writer, allBundles, displayedBundles []*bundleindex.BundleMetadata, limit int, useColor bool, colorHeader, colorReset string) {
285
285
+
func displayLogSummary(w io.Writer, allBundles, displayedBundles []*bundleindex.BundleMetadata, limit int, colorHeader, colorReset string) {
286
286
first := displayedBundles[0]
287
287
last := displayedBundles[len(displayedBundles)-1]
288
288
+1
-1
cmd/plcbundle/commands/mempool.go
···
83
83
return cmd
84
84
}
85
85
86
86
-
func mempoolStatus(cmd *cobra.Command, args []string) error {
86
86
+
func mempoolStatus(cmd *cobra.Command, _ []string) error {
87
87
verbose, _ := cmd.Flags().GetBool("verbose")
88
88
if cmd.Parent() != nil {
89
89
// Called as subcommand, check parent's verbose flag
-17
cmd/plcbundle/commands/rollback.go
···
527
527
fmt.Printf(" plcbundle index build\n\n")
528
528
}
529
529
}
530
530
-
531
531
-
// Validation helpers
532
532
-
533
533
-
// validateRollbackSafety performs additional safety checks
534
534
-
func validateRollbackSafety(mgr BundleManager, plan *rollbackPlan) error {
535
535
-
// Check for chain integrity issues
536
536
-
if len(plan.toKeep) > 1 {
537
537
-
// Verify the target bundle exists and has valid hash
538
538
-
lastKeep := plan.toKeep[len(plan.toKeep)-1]
539
539
-
if lastKeep.Hash == "" {
540
540
-
return fmt.Errorf("target bundle %06d has no chain hash - may be corrupted",
541
541
-
lastKeep.BundleNumber)
542
542
-
}
543
543
-
}
544
544
-
545
545
-
return nil
546
546
-
}
+1
-2
cmd/plcbundle/commands/stream.go
···
230
230
}
231
231
232
232
type streamLogger struct {
233
233
-
quiet bool
234
234
-
verbose bool
233
233
+
quiet bool
235
234
}
236
235
237
236
func (l *streamLogger) Printf(format string, v ...interface{}) {
+31
-41
internal/didindex/builder.go
···
20
20
}
21
21
22
22
// add adds a location to the shard
23
23
-
func (sb *ShardBuilder) add(identifier string, bundle uint16, position uint16, nullified bool) {
23
23
+
func (sb *ShardBuilder) add(identifier string, loc OpLocation) {
24
24
sb.mu.Lock()
25
25
defer sb.mu.Unlock()
26
26
27
27
-
sb.entries[identifier] = append(sb.entries[identifier], OpLocation{
28
28
-
Bundle: bundle,
29
29
-
Position: position,
30
30
-
Nullified: nullified,
31
31
-
})
27
27
+
sb.entries[identifier] = append(sb.entries[identifier], loc)
28
28
+
}
29
29
+
30
30
+
// updateAndSaveConfig updates config with new values and saves atomically
31
31
+
func (dim *Manager) updateAndSaveConfig(totalDIDs int64, lastBundle int) error {
32
32
+
dim.config.TotalDIDs = totalDIDs
33
33
+
dim.config.LastBundle = lastBundle
34
34
+
dim.config.Version = DIDINDEX_VERSION
35
35
+
dim.config.Format = "binary_v4"
36
36
+
dim.config.UpdatedAt = time.Now().UTC()
37
37
+
38
38
+
return dim.saveIndexConfig()
32
39
}
33
40
34
41
// BuildIndexFromScratch builds index with controlled memory usage
···
92
99
93
100
shardNum := dim.calculateShard(identifier)
94
101
95
95
-
// Write entry: [24 bytes ID][2 bytes bundle][2 bytes pos][1 byte nullified]
96
96
-
entry := make([]byte, 29)
102
102
+
// Write entry: [24 bytes ID][4 bytes packed OpLocation]
103
103
+
entry := make([]byte, 28)
97
104
copy(entry[0:24], identifier)
98
98
-
binary.LittleEndian.PutUint16(entry[24:26], uint16(meta.BundleNumber))
99
99
-
binary.LittleEndian.PutUint16(entry[26:28], uint16(pos))
100
105
101
101
-
// Store nullified flag
102
102
-
if op.IsNullified() {
103
103
-
entry[28] = 1
104
104
-
} else {
105
105
-
entry[28] = 0
106
106
-
}
106
106
+
// Create packed OpLocation (includes nullified bit)
107
107
+
loc := NewOpLocation(uint16(meta.BundleNumber), uint16(pos), op.IsNullified())
108
108
+
binary.LittleEndian.PutUint32(entry[24:28], uint32(loc))
107
109
108
110
if _, err := tempShards[shardNum].Write(entry); err != nil {
109
111
dim.logger.Printf("Warning: failed to write to temp shard %02x: %v", shardNum, err)
···
135
137
totalDIDs += count
136
138
}
137
139
138
138
-
dim.config.TotalDIDs = totalDIDs
139
139
-
dim.config.LastBundle = bundles[len(bundles)-1].BundleNumber
140
140
-
141
141
-
if err := dim.saveIndexConfig(); err != nil {
140
140
+
if err := dim.updateAndSaveConfig(totalDIDs, bundles[len(bundles)-1].BundleNumber); err != nil {
142
141
return fmt.Errorf("failed to save config: %w", err)
143
142
}
144
143
···
165
164
return 0, nil
166
165
}
167
166
168
168
-
// Parse entries (29 bytes each)
169
169
-
entryCount := len(data) / 29
170
170
-
if len(data)%29 != 0 {
171
171
-
return 0, fmt.Errorf("corrupted temp shard: size not multiple of 29")
167
167
+
// Parse entries (28 bytes each)
168
168
+
entryCount := len(data) / 28
169
169
+
if len(data)%28 != 0 {
170
170
+
return 0, fmt.Errorf("corrupted temp shard: size not multiple of 28")
172
171
}
173
172
174
173
type tempEntry struct {
175
174
identifier string
176
176
-
bundle uint16
177
177
-
position uint16
178
178
-
nullified bool
175
175
+
location OpLocation // ← Single packed value
179
176
}
180
177
181
178
entries := make([]tempEntry, entryCount)
182
179
for i := 0; i < entryCount; i++ {
183
183
-
offset := i * 29
180
180
+
offset := i * 28 // ← 28 bytes
184
181
entries[i] = tempEntry{
185
182
identifier: string(data[offset : offset+24]),
186
186
-
bundle: binary.LittleEndian.Uint16(data[offset+24 : offset+26]),
187
187
-
position: binary.LittleEndian.Uint16(data[offset+26 : offset+28]),
188
188
-
nullified: data[offset+28] != 0,
183
183
+
location: OpLocation(binary.LittleEndian.Uint32(data[offset+24 : offset+28])),
189
184
}
190
185
}
191
186
···
200
195
// Group by DID
201
196
builder := newShardBuilder()
202
197
for _, entry := range entries {
203
203
-
builder.add(entry.identifier, entry.bundle, entry.position, entry.nullified)
198
198
+
builder.add(entry.identifier, entry.location)
204
199
}
205
200
206
201
// Free entries
···
240
235
shardOps[shardNum] = make(map[string][]OpLocation)
241
236
}
242
237
243
243
-
shardOps[shardNum][identifier] = append(shardOps[shardNum][identifier], OpLocation{
244
244
-
Bundle: uint16(bundle.BundleNumber),
245
245
-
Position: uint16(pos),
246
246
-
Nullified: op.IsNullified(),
247
247
-
})
238
238
+
loc := NewOpLocation(uint16(bundle.BundleNumber), uint16(pos), op.IsNullified())
239
239
+
shardOps[shardNum][identifier] = append(shardOps[shardNum][identifier], loc)
248
240
}
249
241
250
242
groupDuration := time.Since(groupStart)
···
349
341
// STEP 4: Update config
350
342
configStart := time.Now()
351
343
352
352
-
dim.config.TotalDIDs += deltaCount
353
353
-
dim.config.LastBundle = bundle.BundleNumber
354
354
-
355
355
-
if err := dim.saveIndexConfig(); err != nil {
344
344
+
newTotal := dim.config.TotalDIDs + deltaCount
345
345
+
if err := dim.updateAndSaveConfig(newTotal, bundle.BundleNumber); err != nil {
356
346
return fmt.Errorf("failed to save config: %w", err)
357
347
}
358
348
+12
-12
internal/didindex/lookup.go
···
34
34
// Filter nullified
35
35
var validLocations []OpLocation
36
36
for _, loc := range locations {
37
37
-
if !loc.Nullified {
37
37
+
if !loc.Nullified() {
38
38
validLocations = append(validLocations, loc)
39
39
}
40
40
}
···
46
46
47
47
if len(validLocations) == 1 {
48
48
loc := validLocations[0]
49
49
-
op, err := provider.LoadOperation(ctx, int(loc.Bundle), int(loc.Position))
49
49
+
op, err := provider.LoadOperation(ctx, loc.BundleInt(), loc.PositionInt())
50
50
if err != nil {
51
51
return nil, err
52
52
}
···
56
56
// For multiple operations: group by bundle to minimize bundle loads
57
57
bundleMap := make(map[uint16][]uint16)
58
58
for _, loc := range validLocations {
59
59
-
bundleMap[loc.Bundle] = append(bundleMap[loc.Bundle], loc.Position)
59
59
+
bundleMap[loc.Bundle()] = append(bundleMap[loc.Bundle()], loc.Position())
60
60
}
61
61
62
62
if dim.verbose {
···
133
133
// Group by bundle
134
134
bundleMap := make(map[uint16][]OpLocation)
135
135
for _, loc := range locations {
136
136
-
bundleMap[loc.Bundle] = append(bundleMap[loc.Bundle], loc)
136
136
+
bundleMap[loc.Bundle()] = append(bundleMap[loc.Bundle()], loc)
137
137
}
138
138
139
139
if dim.verbose {
···
149
149
}
150
150
151
151
for _, loc := range locs {
152
152
-
if int(loc.Position) >= len(bundle.Operations) {
152
152
+
if loc.PositionInt() >= len(bundle.Operations) {
153
153
continue
154
154
}
155
155
156
156
-
op := bundle.Operations[loc.Position]
156
156
+
op := bundle.Operations[loc.Position()]
157
157
results = append(results, OpLocationWithOperation{
158
158
Operation: op,
159
159
-
Bundle: int(loc.Bundle),
160
160
-
Position: int(loc.Position),
159
159
+
Bundle: loc.BundleInt(),
160
160
+
Position: loc.PositionInt(),
161
161
})
162
162
}
163
163
}
···
196
196
// Find latest non-nullified location
197
197
var latestLoc *OpLocation
198
198
for i := range locations {
199
199
-
if locations[i].Nullified {
199
199
+
if locations[i].Nullified() {
200
200
continue
201
201
}
202
202
203
203
if latestLoc == nil {
204
204
latestLoc = &locations[i]
205
205
} else {
206
206
-
if locations[i].Bundle > latestLoc.Bundle ||
207
207
-
(locations[i].Bundle == latestLoc.Bundle && locations[i].Position > latestLoc.Position) {
206
206
+
if locations[i].Bundle() > latestLoc.Bundle() ||
207
207
+
(locations[i].Bundle() == latestLoc.Bundle() && locations[i].Position() > latestLoc.Position()) {
208
208
latestLoc = &locations[i]
209
209
}
210
210
}
···
215
215
}
216
216
217
217
// Load ONLY the specific operation (efficient!)
218
218
-
return provider.LoadOperation(ctx, int(latestLoc.Bundle), int(latestLoc.Position))
218
218
+
return provider.LoadOperation(ctx, latestLoc.BundleInt(), latestLoc.PositionInt())
219
219
}
+40
-40
internal/didindex/manager.go
···
25
25
config, _ := loadIndexConfig(configPath)
26
26
if config == nil {
27
27
config = &Config{
28
28
-
Version: DIDINDEX_VERSION,
29
29
-
Format: "binary_v1",
28
28
+
Version: DIDINDEX_VERSION, // Will be 4
29
29
+
Format: "binary_v4", // Update format name
30
30
ShardCount: DID_SHARD_COUNT,
31
31
UpdatedAt: time.Now().UTC(),
32
32
}
33
33
+
} else if config.Version < DIDINDEX_VERSION {
34
34
+
// Auto-trigger rebuild on version mismatch
35
35
+
logger.Printf("DID index version outdated (v%d, need v%d) - rebuild required",
36
36
+
config.Version, DIDINDEX_VERSION)
33
37
}
34
38
35
39
return &Manager{
···
43
47
config: config,
44
48
logger: logger,
45
49
}
46
46
-
}
47
47
-
48
48
-
// Add helper to ensure directories when actually writing
49
49
-
func (dim *Manager) ensureDirectories() error {
50
50
-
return os.MkdirAll(dim.shardDir, 0755)
51
50
}
52
51
53
52
// Close unmaps all shards and cleans up
···
274
273
275
274
// searchShard performs optimized binary search using prefix index
276
275
func (dim *Manager) searchShard(shard *mmapShard, identifier string) []OpLocation {
277
277
-
if shard.data == nil || len(shard.data) < 1056 {
276
276
+
if len(shard.data) < 1056 {
278
277
return nil
279
278
}
280
279
···
449
448
// Read locations
450
449
locations := make([]OpLocation, count)
451
450
for i := 0; i < int(count); i++ {
452
452
-
if offset+5 > len(data) {
451
451
+
if offset+4 > len(data) { // ← 4 bytes now
453
452
return locations[:i]
454
453
}
455
454
456
456
-
bundle := binary.LittleEndian.Uint16(data[offset : offset+2])
457
457
-
position := binary.LittleEndian.Uint16(data[offset+2 : offset+4])
458
458
-
nullified := data[offset+4] != 0
455
455
+
// Read packed uint32
456
456
+
packed := binary.LittleEndian.Uint32(data[offset : offset+4])
457
457
+
locations[i] = OpLocation(packed)
459
458
460
460
-
locations[i] = OpLocation{
461
461
-
Bundle: bundle,
462
462
-
Position: position,
463
463
-
Nullified: nullified,
464
464
-
}
465
465
-
466
466
-
offset += 5
459
459
+
offset += 4 // ← 4 bytes
467
460
}
468
461
469
462
return locations
···
660
653
for i, id := range identifiers {
661
654
offsetTable[i] = uint32(currentOffset)
662
655
locations := builder.entries[id]
663
663
-
entrySize := DID_IDENTIFIER_LEN + 2 + (len(locations) * 5)
656
656
+
entrySize := DID_IDENTIFIER_LEN + 2 + (len(locations) * 4) // ← 4 bytes
664
657
currentOffset += entrySize
665
658
}
666
659
···
700
693
offset += 2
701
694
702
695
for _, loc := range locations {
703
703
-
binary.LittleEndian.PutUint16(buf[offset:offset+2], loc.Bundle)
704
704
-
binary.LittleEndian.PutUint16(buf[offset+2:offset+4], loc.Position)
705
705
-
706
706
-
if loc.Nullified {
707
707
-
buf[offset+4] = 1
708
708
-
} else {
709
709
-
buf[offset+4] = 0
710
710
-
}
711
711
-
712
712
-
offset += 5
696
696
+
// Write packed uint32 (global position + nullified bit)
697
697
+
binary.LittleEndian.PutUint32(buf[offset:offset+4], uint32(loc))
698
698
+
offset += 4 // ← 4 bytes per location
713
699
}
714
700
}
715
701
···
724
710
725
711
entryCount := binary.LittleEndian.Uint32(data[9:13])
726
712
727
727
-
var offsetTableStart int
728
728
-
offsetTableStart = 1056
713
713
+
offsetTableStart := 1056
729
714
730
715
// Start reading entries after offset table
731
716
offset := offsetTableStart + (int(entryCount) * 4)
···
745
730
746
731
// Read locations
747
732
locations := make([]OpLocation, locCount)
733
733
+
734
734
+
// Check version to determine format
735
735
+
version := binary.LittleEndian.Uint32(data[4:8])
736
736
+
748
737
for j := 0; j < int(locCount); j++ {
749
749
-
if offset+5 > len(data) {
750
750
-
break
751
751
-
}
738
738
+
if version >= 4 {
739
739
+
// New format: 4-byte packed uint32
740
740
+
if offset+4 > len(data) {
741
741
+
break
742
742
+
}
743
743
+
packed := binary.LittleEndian.Uint32(data[offset : offset+4])
744
744
+
locations[j] = OpLocation(packed)
745
745
+
offset += 4
746
746
+
} else {
747
747
+
// Old format: 5-byte separate fields (for migration)
748
748
+
if offset+5 > len(data) {
749
749
+
break
750
750
+
}
751
751
+
bundle := binary.LittleEndian.Uint16(data[offset : offset+2])
752
752
+
position := binary.LittleEndian.Uint16(data[offset+2 : offset+4])
753
753
+
nullified := data[offset+4] != 0
752
754
753
753
-
locations[j] = OpLocation{
754
754
-
Bundle: binary.LittleEndian.Uint16(data[offset : offset+2]),
755
755
-
Position: binary.LittleEndian.Uint16(data[offset+2 : offset+4]),
756
756
-
Nullified: data[offset+4] != 0,
755
755
+
// Convert to new format
756
756
+
locations[j] = NewOpLocation(bundle, position, nullified)
757
757
+
offset += 5
757
758
}
758
758
-
offset += 5
759
759
}
760
760
761
761
builder.entries[identifier] = locations
+58
internal/didindex/manager_test.go
···
1
1
+
package didindex_test
2
2
+
3
3
+
import (
4
4
+
"testing"
5
5
+
6
6
+
"tangled.org/atscan.net/plcbundle/internal/didindex"
7
7
+
)
8
8
+
9
9
+
func TestOpLocationPacking(t *testing.T) {
10
10
+
tests := []struct {
11
11
+
bundle uint16
12
12
+
position uint16
13
13
+
nullified bool
14
14
+
}{
15
15
+
{1, 0, false},
16
16
+
{1, 9999, false},
17
17
+
{100, 5000, true},
18
18
+
{65535, 9999, true}, // Max values
19
19
+
}
20
20
+
21
21
+
for _, tt := range tests {
22
22
+
loc := didindex.NewOpLocation(tt.bundle, tt.position, tt.nullified)
23
23
+
24
24
+
// Test unpacking
25
25
+
if loc.Bundle() != tt.bundle {
26
26
+
t.Errorf("Bundle mismatch: got %d, want %d", loc.Bundle(), tt.bundle)
27
27
+
}
28
28
+
if loc.Position() != tt.position {
29
29
+
t.Errorf("Position mismatch: got %d, want %d", loc.Position(), tt.position)
30
30
+
}
31
31
+
if loc.Nullified() != tt.nullified {
32
32
+
t.Errorf("Nullified mismatch: got %v, want %v", loc.Nullified(), tt.nullified)
33
33
+
}
34
34
+
35
35
+
// Test global position
36
36
+
expectedGlobal := uint32(tt.bundle)*10000 + uint32(tt.position)
37
37
+
if loc.GlobalPosition() != expectedGlobal {
38
38
+
t.Errorf("Global position mismatch: got %d, want %d",
39
39
+
loc.GlobalPosition(), expectedGlobal)
40
40
+
}
41
41
+
}
42
42
+
}
43
43
+
44
44
+
func TestOpLocationComparison(t *testing.T) {
45
45
+
loc1 := didindex.NewOpLocation(100, 50, false) // 1,000,050
46
46
+
loc2 := didindex.NewOpLocation(100, 51, false) // 1,000,051
47
47
+
loc3 := didindex.NewOpLocation(200, 30, false) // 2,000,030
48
48
+
49
49
+
if !loc1.Less(loc2) {
50
50
+
t.Error("Expected loc1 < loc2")
51
51
+
}
52
52
+
if !loc2.Less(loc3) {
53
53
+
t.Error("Expected loc2 < loc3")
54
54
+
}
55
55
+
if loc3.Less(loc1) {
56
56
+
t.Error("Expected loc3 > loc1")
57
57
+
}
58
58
+
}
+67
-8
internal/didindex/types.go
···
17
17
18
18
// Binary format constants
19
19
DIDINDEX_MAGIC = "PLCD"
20
20
-
DIDINDEX_VERSION = 3
20
20
+
DIDINDEX_VERSION = 4
21
21
+
22
22
+
// Format sizes
23
23
+
LOCATION_SIZE_V3 = 5 // Old: 2+2+1
24
24
+
LOCATION_SIZE_V4 = 4 // New: packed uint32
21
25
22
26
BUILD_BATCH_SIZE = 100 // Process 100 bundles at a time
23
27
···
63
67
LastBundle int `json:"last_bundle"`
64
68
}
65
69
66
66
-
// OpLocation represents exact location of an operation
67
67
-
type OpLocation struct {
68
68
-
Bundle uint16
69
69
-
Position uint16
70
70
-
Nullified bool
71
71
-
}
72
72
-
73
70
// ShardBuilder accumulates DID positions for a shard
74
71
type ShardBuilder struct {
75
72
entries map[string][]OpLocation
76
73
mu sync.Mutex
77
74
}
78
75
76
76
+
// OpLocation represents exact location of an operation
77
77
+
type OpLocation uint32
78
78
+
79
79
// OpLocationWithOperation contains an operation with its bundle/position
80
80
type OpLocationWithOperation struct {
81
81
Operation plcclient.PLCOperation
82
82
Bundle int
83
83
Position int
84
84
}
85
85
+
86
86
+
func NewOpLocation(bundle, position uint16, nullified bool) OpLocation {
87
87
+
globalPos := uint32(bundle)*10000 + uint32(position)
88
88
+
loc := globalPos << 1
89
89
+
if nullified {
90
90
+
loc |= 1
91
91
+
}
92
92
+
return OpLocation(loc)
93
93
+
}
94
94
+
95
95
+
// Getters
96
96
+
func (loc OpLocation) GlobalPosition() uint32 {
97
97
+
return uint32(loc) >> 1
98
98
+
}
99
99
+
100
100
+
func (loc OpLocation) Bundle() uint16 {
101
101
+
return uint16(loc.GlobalPosition() / 10000)
102
102
+
}
103
103
+
104
104
+
func (loc OpLocation) Position() uint16 {
105
105
+
return uint16(loc.GlobalPosition() % 10000)
106
106
+
}
107
107
+
108
108
+
func (loc OpLocation) Nullified() bool {
109
109
+
return (loc & 1) == 1
110
110
+
}
111
111
+
112
112
+
func (loc OpLocation) IsAfter(other OpLocation) bool {
113
113
+
// Compare global positions directly
114
114
+
return loc.GlobalPosition() > other.GlobalPosition()
115
115
+
}
116
116
+
117
117
+
func (loc OpLocation) IsBefore(other OpLocation) bool {
118
118
+
return loc.GlobalPosition() < other.GlobalPosition()
119
119
+
}
120
120
+
121
121
+
func (loc OpLocation) Equals(other OpLocation) bool {
122
122
+
// Compare entire packed value (including nullified bit)
123
123
+
return loc == other
124
124
+
}
125
125
+
126
126
+
func (loc OpLocation) PositionEquals(other OpLocation) bool {
127
127
+
// Compare only position (ignore nullified bit)
128
128
+
return loc.GlobalPosition() == other.GlobalPosition()
129
129
+
}
130
130
+
131
131
+
// Convenience conversions
132
132
+
func (loc OpLocation) BundleInt() int {
133
133
+
return int(loc.Bundle())
134
134
+
}
135
135
+
136
136
+
func (loc OpLocation) PositionInt() int {
137
137
+
return int(loc.Position())
138
138
+
}
139
139
+
140
140
+
// For sorting/comparison
141
141
+
func (loc OpLocation) Less(other OpLocation) bool {
142
142
+
return loc.GlobalPosition() < other.GlobalPosition()
143
143
+
}