tangled
alpha
login
or
join now
atscan.net
/
plcbundle-go
1
fork
atom
[DEPRECATED] Go implementation of plcbundle
1
fork
atom
overview
issues
pulls
pipelines
cmd stream
tree.fail
4 months ago
0fd76d16
f8b7c98e
+343
-252
5 changed files
expand all
collapse all
unified
split
cmd
plcbundle
commands
backfill.go
common.go
stream.go
main.go
internal
sync
syncer.go
-148
cmd/plcbundle/commands/backfill.go
···
1
1
-
package commands
2
2
-
3
3
-
import (
4
4
-
"context"
5
5
-
"fmt"
6
6
-
"os"
7
7
-
8
8
-
flag "github.com/spf13/pflag"
9
9
-
)
10
10
-
11
11
-
// BackfillCommand handles the backfill subcommand
12
12
-
func BackfillCommand(args []string) error {
13
13
-
fs := flag.NewFlagSet("backfill", flag.ExitOnError)
14
14
-
plcURL := fs.String("plc", "https://plc.directory", "PLC directory URL")
15
15
-
startFrom := fs.Int("start", 1, "bundle number to start from")
16
16
-
endAt := fs.Int("end", 0, "bundle number to end at (0 = until caught up)")
17
17
-
verbose := fs.Bool("verbose", false, "verbose sync logging")
18
18
-
19
19
-
if err := fs.Parse(args); err != nil {
20
20
-
return err
21
21
-
}
22
22
-
23
23
-
mgr, dir, err := getManager(*plcURL)
24
24
-
if err != nil {
25
25
-
return err
26
26
-
}
27
27
-
defer mgr.Close()
28
28
-
29
29
-
fmt.Fprintf(os.Stderr, "Starting backfill from: %s\n", dir)
30
30
-
fmt.Fprintf(os.Stderr, "Starting from bundle: %06d\n", *startFrom)
31
31
-
if *endAt > 0 {
32
32
-
fmt.Fprintf(os.Stderr, "Ending at bundle: %06d\n", *endAt)
33
33
-
} else {
34
34
-
fmt.Fprintf(os.Stderr, "Ending: when caught up\n")
35
35
-
}
36
36
-
fmt.Fprintf(os.Stderr, "\n")
37
37
-
38
38
-
ctx := context.Background()
39
39
-
40
40
-
currentBundle := *startFrom
41
41
-
processedCount := 0
42
42
-
fetchedCount := 0
43
43
-
loadedCount := 0
44
44
-
operationCount := 0
45
45
-
46
46
-
for {
47
47
-
if *endAt > 0 && currentBundle > *endAt {
48
48
-
break
49
49
-
}
50
50
-
51
51
-
fmt.Fprintf(os.Stderr, "Processing bundle %06d... ", currentBundle)
52
52
-
53
53
-
// Try to load from disk first
54
54
-
bundle, err := mgr.LoadBundle(ctx, currentBundle)
55
55
-
56
56
-
if err != nil {
57
57
-
// Bundle doesn't exist, fetch it
58
58
-
fmt.Fprintf(os.Stderr, "fetching... ")
59
59
-
60
60
-
bundle, err = mgr.FetchNextBundle(ctx, !*verbose)
61
61
-
if err != nil {
62
62
-
if isEndOfDataError(err) {
63
63
-
fmt.Fprintf(os.Stderr, "\n✓ Caught up! No more complete bundles available.\n")
64
64
-
break
65
65
-
}
66
66
-
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
67
67
-
break
68
68
-
}
69
69
-
70
70
-
if _, err := mgr.SaveBundle(ctx, bundle, !*verbose); err != nil {
71
71
-
return fmt.Errorf("error saving: %w", err)
72
72
-
}
73
73
-
74
74
-
fetchedCount++
75
75
-
fmt.Fprintf(os.Stderr, "saved... ")
76
76
-
} else {
77
77
-
loadedCount++
78
78
-
}
79
79
-
80
80
-
// Output operations to stdout (JSONL)
81
81
-
for _, op := range bundle.Operations {
82
82
-
if len(op.RawJSON) > 0 {
83
83
-
fmt.Println(string(op.RawJSON))
84
84
-
}
85
85
-
}
86
86
-
87
87
-
operationCount += len(bundle.Operations)
88
88
-
processedCount++
89
89
-
90
90
-
fmt.Fprintf(os.Stderr, "✓ (%d ops, %d DIDs)\n", len(bundle.Operations), bundle.DIDCount)
91
91
-
92
92
-
currentBundle++
93
93
-
94
94
-
// Progress summary every 100 bundles
95
95
-
if processedCount%100 == 0 {
96
96
-
fmt.Fprintf(os.Stderr, "\n--- Progress: %d bundles processed (%d fetched, %d loaded) ---\n",
97
97
-
processedCount, fetchedCount, loadedCount)
98
98
-
fmt.Fprintf(os.Stderr, " Total operations: %d\n\n", operationCount)
99
99
-
}
100
100
-
}
101
101
-
102
102
-
// Final summary
103
103
-
fmt.Fprintf(os.Stderr, "\n✓ Backfill complete\n")
104
104
-
fmt.Fprintf(os.Stderr, " Bundles processed: %d\n", processedCount)
105
105
-
fmt.Fprintf(os.Stderr, " Newly fetched: %d\n", fetchedCount)
106
106
-
fmt.Fprintf(os.Stderr, " Loaded from disk: %d\n", loadedCount)
107
107
-
fmt.Fprintf(os.Stderr, " Total operations: %d\n", operationCount)
108
108
-
fmt.Fprintf(os.Stderr, " Range: %06d - %06d\n", *startFrom, currentBundle-1)
109
109
-
110
110
-
return nil
111
111
-
}
112
112
-
113
113
-
// isEndOfDataError checks if error indicates end of available data
114
114
-
func isEndOfDataError(err error) bool {
115
115
-
if err == nil {
116
116
-
return false
117
117
-
}
118
118
-
119
119
-
errMsg := err.Error()
120
120
-
return containsAny(errMsg,
121
121
-
"insufficient operations",
122
122
-
"no more operations available",
123
123
-
"reached latest data")
124
124
-
}
125
125
-
126
126
-
// Helper functions
127
127
-
128
128
-
func containsAny(s string, substrs ...string) bool {
129
129
-
for _, substr := range substrs {
130
130
-
if contains(s, substr) {
131
131
-
return true
132
132
-
}
133
133
-
}
134
134
-
return false
135
135
-
}
136
136
-
137
137
-
func contains(s, substr string) bool {
138
138
-
return len(s) >= len(substr) && indexOf(s, substr) >= 0
139
139
-
}
140
140
-
141
141
-
func indexOf(s, substr string) int {
142
142
-
for i := 0; i <= len(s)-len(substr); i++ {
143
143
-
if s[i:i+len(substr)] == substr {
144
144
-
return i
145
145
-
}
146
146
-
}
147
147
-
return -1
148
148
-
}
+2
cmd/plcbundle/commands/common.go
···
40
40
LoadOperation(ctx context.Context, bundleNum, position int) (*plcclient.PLCOperation, error)
41
41
CloneFromRemote(ctx context.Context, opts internalsync.CloneOptions) (*internalsync.CloneResult, error)
42
42
ResolveDID(ctx context.Context, did string) (*bundle.ResolveDIDResult, error)
43
43
+
RunSyncOnce(ctx context.Context, config *internalsync.SyncLoopConfig, verbose bool) (int, error)
44
44
+
RunSyncLoop(ctx context.Context, config *internalsync.SyncLoopConfig) error
43
45
}
44
46
45
47
// PLCOperationWithLocation wraps operation with location info
+253
cmd/plcbundle/commands/stream.go
···
1
1
+
// repo/cmd/plcbundle/commands/stream.go
2
2
+
package commands
3
3
+
4
4
+
import (
5
5
+
"context"
6
6
+
"fmt"
7
7
+
"os"
8
8
+
"time"
9
9
+
10
10
+
"github.com/goccy/go-json"
11
11
+
"github.com/spf13/cobra"
12
12
+
internalsync "tangled.org/atscan.net/plcbundle/internal/sync"
13
13
+
)
14
14
+
15
15
+
func NewStreamCommand() *cobra.Command {
16
16
+
var (
17
17
+
all bool
18
18
+
rangeStr string
19
19
+
sync bool
20
20
+
plcURL string
21
21
+
)
22
22
+
23
23
+
cmd := &cobra.Command{
24
24
+
Use: "stream [flags]",
25
25
+
Short: "Stream operations to stdout (JSONL)",
26
26
+
Long: `Stream operations to stdout in JSONL format
27
27
+
28
28
+
Outputs PLC operations as newline-delimited JSON to stdout.
29
29
+
Progress and status messages go to stderr.
30
30
+
31
31
+
By default, streams only existing bundles. Use --sync to also fetch
32
32
+
new bundles from PLC directory until caught up.`,
33
33
+
34
34
+
Example: ` # Stream all existing bundles
35
35
+
plcbundle stream --all
36
36
+
37
37
+
# Stream and sync new bundles
38
38
+
plcbundle stream --all --sync
39
39
+
40
40
+
# Stream specific range (existing only)
41
41
+
plcbundle stream --range 1-100
42
42
+
43
43
+
# Stream from specific PLC directory
44
44
+
plcbundle stream --all --sync --plc https://plc.directory
45
45
+
46
46
+
# Pipe to file
47
47
+
plcbundle stream --all > operations.jsonl
48
48
+
49
49
+
# Process with jq
50
50
+
plcbundle stream --all | jq -r .did | sort | uniq -c
51
51
+
52
52
+
# Sync and filter with detector
53
53
+
plcbundle stream --all --sync | plcbundle detector filter spam`,
54
54
+
55
55
+
RunE: func(cmd *cobra.Command, args []string) error {
56
56
+
verbose, _ := cmd.Root().PersistentFlags().GetBool("verbose")
57
57
+
quiet, _ := cmd.Root().PersistentFlags().GetBool("quiet")
58
58
+
59
59
+
// Setup PLC client only if syncing
60
60
+
var effectivePLCURL string
61
61
+
if sync {
62
62
+
effectivePLCURL = plcURL
63
63
+
}
64
64
+
65
65
+
mgr, dir, err := getManagerFromCommand(cmd, effectivePLCURL)
66
66
+
if err != nil {
67
67
+
return err
68
68
+
}
69
69
+
defer mgr.Close()
70
70
+
71
71
+
if !quiet {
72
72
+
fmt.Fprintf(os.Stderr, "Streaming from: %s\n", dir)
73
73
+
}
74
74
+
75
75
+
// Determine bundle range
76
76
+
var start, end int
77
77
+
78
78
+
if all {
79
79
+
index := mgr.GetIndex()
80
80
+
bundles := index.GetBundles()
81
81
+
82
82
+
if len(bundles) == 0 {
83
83
+
if sync {
84
84
+
// No bundles but sync enabled - start from 1
85
85
+
start = 1
86
86
+
end = 0 // Will be updated after sync
87
87
+
} else {
88
88
+
if !quiet {
89
89
+
fmt.Fprintf(os.Stderr, "No bundles available (use --sync to fetch)\n")
90
90
+
}
91
91
+
return nil
92
92
+
}
93
93
+
} else {
94
94
+
start = bundles[0].BundleNumber
95
95
+
end = bundles[len(bundles)-1].BundleNumber
96
96
+
}
97
97
+
98
98
+
} else if rangeStr != "" {
99
99
+
var err error
100
100
+
start, end, err = parseBundleRange(rangeStr)
101
101
+
if err != nil {
102
102
+
return err
103
103
+
}
104
104
+
105
105
+
} else {
106
106
+
return fmt.Errorf("either --all or --range required")
107
107
+
}
108
108
+
109
109
+
if !quiet {
110
110
+
if sync {
111
111
+
fmt.Fprintf(os.Stderr, "Mode: stream existing + sync new bundles\n")
112
112
+
} else {
113
113
+
fmt.Fprintf(os.Stderr, "Mode: stream existing only\n")
114
114
+
}
115
115
+
fmt.Fprintf(os.Stderr, "\n")
116
116
+
}
117
117
+
118
118
+
return streamBundles(cmd.Context(), mgr, start, end, sync, verbose, quiet)
119
119
+
},
120
120
+
}
121
121
+
122
122
+
cmd.Flags().BoolVar(&all, "all", false, "Stream all bundles")
123
123
+
cmd.Flags().StringVarP(&rangeStr, "range", "r", "", "Stream bundle range (e.g., '1-100')")
124
124
+
cmd.Flags().BoolVar(&sync, "sync", false, "Also fetch new bundles from PLC (until caught up)")
125
125
+
cmd.Flags().StringVar(&plcURL, "plc", "https://plc.directory", "PLC directory URL (for --sync)")
126
126
+
127
127
+
return cmd
128
128
+
}
129
129
+
130
130
+
func streamBundles(ctx context.Context, mgr BundleManager, start, end int, doSync bool, verbose bool, quiet bool) error {
131
131
+
operationCount := 0
132
132
+
133
133
+
// Phase 1: Stream existing bundles
134
134
+
existingCount := 0
135
135
+
if end > 0 {
136
136
+
existingCount = streamExistingBundles(ctx, mgr, start, end, &operationCount, verbose, quiet)
137
137
+
}
138
138
+
139
139
+
// Phase 2: Sync and stream new bundles (if enabled)
140
140
+
fetchedCount := 0
141
141
+
if doSync {
142
142
+
if !quiet {
143
143
+
fmt.Fprintf(os.Stderr, "\nSyncing new bundles from PLC...\n")
144
144
+
}
145
145
+
146
146
+
logger := &streamLogger{quiet: quiet}
147
147
+
config := &internalsync.SyncLoopConfig{
148
148
+
MaxBundles: 0,
149
149
+
Verbose: verbose,
150
150
+
Logger: logger,
151
151
+
OnBundleSynced: func(bundleNum, synced, mempoolCount int, duration, indexTime time.Duration) {
152
152
+
// Stream the bundle we just created
153
153
+
if bundle, err := mgr.LoadBundle(ctx, bundleNum); err == nil {
154
154
+
for _, op := range bundle.Operations {
155
155
+
if len(op.RawJSON) > 0 {
156
156
+
fmt.Println(string(op.RawJSON))
157
157
+
} else {
158
158
+
data, _ := json.Marshal(op)
159
159
+
fmt.Println(string(data))
160
160
+
}
161
161
+
}
162
162
+
operationCount += len(bundle.Operations)
163
163
+
}
164
164
+
},
165
165
+
}
166
166
+
167
167
+
var err error
168
168
+
fetchedCount, err = mgr.RunSyncOnce(ctx, config, verbose)
169
169
+
if err != nil {
170
170
+
return err
171
171
+
}
172
172
+
}
173
173
+
174
174
+
// Summary
175
175
+
if !quiet {
176
176
+
fmt.Fprintf(os.Stderr, "\n✓ Stream complete\n")
177
177
+
178
178
+
if doSync && fetchedCount > 0 {
179
179
+
fmt.Fprintf(os.Stderr, " Bundles: %d (%d existing + %d synced)\n",
180
180
+
existingCount+fetchedCount, existingCount, fetchedCount)
181
181
+
} else if doSync {
182
182
+
fmt.Fprintf(os.Stderr, " Bundles: %d (already up to date)\n", existingCount)
183
183
+
} else {
184
184
+
fmt.Fprintf(os.Stderr, " Bundles: %d\n", existingCount)
185
185
+
}
186
186
+
187
187
+
fmt.Fprintf(os.Stderr, " Operations: %d\n", operationCount)
188
188
+
}
189
189
+
190
190
+
return nil
191
191
+
}
192
192
+
193
193
+
func streamExistingBundles(ctx context.Context, mgr BundleManager, start, end int, operationCount *int, verbose bool, quiet bool) int {
194
194
+
processedCount := 0
195
195
+
196
196
+
for bundleNum := start; bundleNum <= end; bundleNum++ {
197
197
+
select {
198
198
+
case <-ctx.Done():
199
199
+
return processedCount
200
200
+
default:
201
201
+
}
202
202
+
203
203
+
bundle, err := mgr.LoadBundle(ctx, bundleNum)
204
204
+
if err != nil {
205
205
+
if verbose {
206
206
+
fmt.Fprintf(os.Stderr, "Bundle %06d: not found (skipped)\n", bundleNum)
207
207
+
}
208
208
+
continue
209
209
+
}
210
210
+
211
211
+
// Output operations to stdout (JSONL)
212
212
+
for _, op := range bundle.Operations {
213
213
+
if len(op.RawJSON) > 0 {
214
214
+
fmt.Println(string(op.RawJSON))
215
215
+
} else {
216
216
+
data, _ := json.Marshal(op)
217
217
+
fmt.Println(string(data))
218
218
+
}
219
219
+
}
220
220
+
221
221
+
*operationCount += len(bundle.Operations)
222
222
+
processedCount++
223
223
+
224
224
+
if verbose {
225
225
+
fmt.Fprintf(os.Stderr, "Bundle %06d: ✓ (%d ops)\n", bundleNum, len(bundle.Operations))
226
226
+
} else if !quiet && processedCount%100 == 0 {
227
227
+
fmt.Fprintf(os.Stderr, "Streamed: %d bundles, %d ops\r", processedCount, *operationCount)
228
228
+
}
229
229
+
}
230
230
+
231
231
+
if !quiet && !verbose && processedCount > 0 {
232
232
+
fmt.Fprintf(os.Stderr, "Existing: %d bundles, %d ops\n", processedCount, *operationCount)
233
233
+
}
234
234
+
235
235
+
return processedCount
236
236
+
}
237
237
+
238
238
+
type streamLogger struct {
239
239
+
quiet bool
240
240
+
verbose bool
241
241
+
}
242
242
+
243
243
+
func (l *streamLogger) Printf(format string, v ...interface{}) {
244
244
+
if !l.quiet {
245
245
+
fmt.Fprintf(os.Stderr, format+"\n", v...)
246
246
+
}
247
247
+
}
248
248
+
249
249
+
func (l *streamLogger) Println(v ...interface{}) {
250
250
+
if !l.quiet {
251
251
+
fmt.Fprintln(os.Stderr, v...)
252
252
+
}
253
253
+
}
+2
-2
cmd/plcbundle/main.go
···
47
47
cmd.AddCommand(commands.NewSyncCommand())
48
48
cmd.AddCommand(commands.NewCloneCommand())
49
49
/*cmd.AddCommand(commands.NewPullCommand())
50
50
-
cmd.AddCommand(commands.NewExportCommand())
50
50
+
cmd.AddCommand(commands.NewExportCommand())*/
51
51
cmd.AddCommand(commands.NewStreamCommand())
52
52
-
cmd.AddCommand(commands.NewGetCommand())
52
52
+
/*cmd.AddCommand(commands.NewGetCommand())
53
53
cmd.AddCommand(commands.NewRollbackCommand())*/
54
54
55
55
// Status & info (root level)
+86
-102
internal/sync/syncer.go
···
3
3
4
4
import (
5
5
"context"
6
6
-
"fmt"
7
6
"time"
8
7
9
8
"tangled.org/atscan.net/plcbundle/internal/types"
···
36
35
SaveMempool() error
37
36
}
38
37
38
38
+
// SyncOnce performs a single sync cycle - fetches until caught up
39
39
+
func SyncOnce(ctx context.Context, mgr SyncManager, config *SyncLoopConfig, verbose bool) (int, error) {
40
40
+
cycleStart := time.Now()
41
41
+
startMempool := mgr.GetMempoolCount()
42
42
+
43
43
+
fetchedCount := 0
44
44
+
var totalIndexTime time.Duration
45
45
+
46
46
+
// Keep fetching until caught up (detect by checking if state changes)
47
47
+
for {
48
48
+
// Track state before fetch
49
49
+
bundleBefore := mgr.GetLastBundleNumber()
50
50
+
mempoolBefore := mgr.GetMempoolCount()
51
51
+
52
52
+
// Attempt to fetch and save next bundle
53
53
+
bundleNum, indexTime, err := mgr.FetchAndSaveNextBundle(ctx, !verbose)
54
54
+
55
55
+
// Check if we made any progress
56
56
+
bundleAfter := mgr.GetLastBundleNumber()
57
57
+
mempoolAfter := mgr.GetMempoolCount()
58
58
+
59
59
+
madeProgress := bundleAfter > bundleBefore || mempoolAfter > mempoolBefore
60
60
+
61
61
+
if err != nil {
62
62
+
// If no progress and got error → caught up
63
63
+
if !madeProgress {
64
64
+
break
65
65
+
}
66
66
+
67
67
+
// We added to mempool but couldn't complete bundle yet
68
68
+
// This is fine, just stop here
69
69
+
break
70
70
+
}
71
71
+
72
72
+
// Success
73
73
+
fetchedCount++
74
74
+
totalIndexTime += indexTime
75
75
+
76
76
+
// Callback if provided
77
77
+
if config.OnBundleSynced != nil {
78
78
+
config.OnBundleSynced(bundleNum, fetchedCount, mempoolAfter, time.Since(cycleStart), totalIndexTime)
79
79
+
}
80
80
+
81
81
+
// Small delay between bundles
82
82
+
time.Sleep(500 * time.Millisecond)
83
83
+
84
84
+
// Check if we're still making progress
85
85
+
if !madeProgress {
86
86
+
break
87
87
+
}
88
88
+
}
89
89
+
90
90
+
// Summary output
91
91
+
if config.Logger != nil {
92
92
+
mempoolAfter := mgr.GetMempoolCount()
93
93
+
addedOps := mempoolAfter - startMempool
94
94
+
duration := time.Since(cycleStart)
95
95
+
currentBundle := mgr.GetLastBundleNumber()
96
96
+
97
97
+
if fetchedCount > 0 {
98
98
+
if totalIndexTime > 10*time.Millisecond {
99
99
+
config.Logger.Printf("[Sync] ✓ Bundle %06d | Synced: %d | Mempool: %d (+%d) | %s (index: %s)",
100
100
+
currentBundle, fetchedCount, mempoolAfter, addedOps,
101
101
+
duration.Round(time.Millisecond), totalIndexTime.Round(time.Millisecond))
102
102
+
} else {
103
103
+
config.Logger.Printf("[Sync] ✓ Bundle %06d | Synced: %d | Mempool: %d (+%d) | %s",
104
104
+
currentBundle, fetchedCount, mempoolAfter, addedOps, duration.Round(time.Millisecond))
105
105
+
}
106
106
+
} else if addedOps > 0 {
107
107
+
// No bundles but added to mempool
108
108
+
config.Logger.Printf("[Sync] ✓ Bundle %06d | Mempool: %d (+%d) | %s",
109
109
+
currentBundle, mempoolAfter, addedOps, duration.Round(time.Millisecond))
110
110
+
} else {
111
111
+
// Already up to date
112
112
+
config.Logger.Printf("[Sync] ✓ Bundle %06d | Up to date | %s",
113
113
+
currentBundle, duration.Round(time.Millisecond))
114
114
+
}
115
115
+
}
116
116
+
117
117
+
return fetchedCount, nil
118
118
+
}
119
119
+
39
120
// RunSyncLoop performs continuous syncing
40
121
func RunSyncLoop(ctx context.Context, mgr SyncManager, config *SyncLoopConfig) error {
41
122
if config == nil {
···
48
129
49
130
bundlesSynced := 0
50
131
51
51
-
// Initial sync - always show detailed progress
52
52
-
if config.Logger != nil {
132
132
+
// Initial sync
133
133
+
if config.Logger != nil && config.MaxBundles != 1 {
53
134
config.Logger.Printf("[Sync] Initial sync starting...")
54
135
}
55
136
56
56
-
synced, err := SyncOnce(ctx, mgr, config, true) // Force verbose for initial
137
137
+
synced, err := SyncOnce(ctx, mgr, config, config.Verbose)
57
138
if err != nil {
58
139
return err
59
140
}
···
83
164
return ctx.Err()
84
165
85
166
case <-ticker.C:
167
167
+
// Each tick, do one sync cycle (which fetches until caught up)
86
168
synced, err := SyncOnce(ctx, mgr, config, config.Verbose)
87
169
if err != nil {
88
170
if config.Logger != nil {
···
103
185
}
104
186
}
105
187
}
106
106
-
107
107
-
// SyncOnce performs a single sync cycle (exported for single-shot syncing)
108
108
-
func SyncOnce(ctx context.Context, mgr SyncManager, config *SyncLoopConfig, verbose bool) (int, error) {
109
109
-
cycleStart := time.Now()
110
110
-
111
111
-
startBundle := mgr.GetLastBundleNumber() + 1
112
112
-
mempoolBefore := mgr.GetMempoolCount()
113
113
-
fetchedCount := 0
114
114
-
var totalIndexTime time.Duration
115
115
-
116
116
-
// Keep fetching until caught up
117
117
-
for {
118
118
-
// quiet = !verbose
119
119
-
bundleNum, indexTime, err := mgr.FetchAndSaveNextBundle(ctx, !verbose)
120
120
-
if err != nil {
121
121
-
if isEndOfDataError(err) {
122
122
-
break
123
123
-
}
124
124
-
return fetchedCount, fmt.Errorf("fetch failed: %w", err)
125
125
-
}
126
126
-
127
127
-
fetchedCount++
128
128
-
totalIndexTime += indexTime
129
129
-
130
130
-
// Callback if provided
131
131
-
if config.OnBundleSynced != nil {
132
132
-
mempoolAfter := mgr.GetMempoolCount()
133
133
-
config.OnBundleSynced(bundleNum, fetchedCount, mempoolAfter, time.Since(cycleStart), totalIndexTime)
134
134
-
}
135
135
-
136
136
-
time.Sleep(500 * time.Millisecond)
137
137
-
}
138
138
-
139
139
-
// Summary output
140
140
-
if config.Logger != nil {
141
141
-
mempoolAfter := mgr.GetMempoolCount()
142
142
-
addedOps := mempoolAfter - mempoolBefore
143
143
-
duration := time.Since(cycleStart)
144
144
-
145
145
-
currentBundle := startBundle + fetchedCount - 1
146
146
-
if fetchedCount == 0 {
147
147
-
currentBundle = startBundle - 1
148
148
-
}
149
149
-
150
150
-
if fetchedCount > 0 {
151
151
-
if totalIndexTime > 10*time.Millisecond {
152
152
-
config.Logger.Printf("[Sync] ✓ Bundle %06d | Synced: %d | Mempool: %d (+%d) | %s (index: %s)",
153
153
-
currentBundle, fetchedCount, mempoolAfter, addedOps,
154
154
-
duration.Round(time.Millisecond), totalIndexTime.Round(time.Millisecond))
155
155
-
} else {
156
156
-
config.Logger.Printf("[Sync] ✓ Bundle %06d | Synced: %d | Mempool: %d (+%d) | %s",
157
157
-
currentBundle, fetchedCount, mempoolAfter, addedOps, duration.Round(time.Millisecond))
158
158
-
}
159
159
-
} else {
160
160
-
config.Logger.Printf("[Sync] ✓ Bundle %06d | Up to date | Mempool: %d (+%d) | %s",
161
161
-
currentBundle, mempoolAfter, addedOps, duration.Round(time.Millisecond))
162
162
-
}
163
163
-
}
164
164
-
165
165
-
return fetchedCount, nil
166
166
-
}
167
167
-
168
168
-
// isEndOfDataError checks if error indicates end of available data
169
169
-
func isEndOfDataError(err error) bool {
170
170
-
if err == nil {
171
171
-
return false
172
172
-
}
173
173
-
174
174
-
errMsg := err.Error()
175
175
-
return containsAny(errMsg,
176
176
-
"insufficient operations",
177
177
-
"no more operations available",
178
178
-
"reached latest data",
179
179
-
"caught up to latest")
180
180
-
}
181
181
-
182
182
-
// Helper functions
183
183
-
func containsAny(s string, substrs ...string) bool {
184
184
-
for _, substr := range substrs {
185
185
-
if contains(s, substr) {
186
186
-
return true
187
187
-
}
188
188
-
}
189
189
-
return false
190
190
-
}
191
191
-
192
192
-
func contains(s, substr string) bool {
193
193
-
return len(s) >= len(substr) && indexOf(s, substr) >= 0
194
194
-
}
195
195
-
196
196
-
func indexOf(s, substr string) int {
197
197
-
for i := 0; i <= len(s)-len(substr); i++ {
198
198
-
if s[i:i+len(substr)] == substr {
199
199
-
return i
200
200
-
}
201
201
-
}
202
202
-
return -1
203
203
-
}