Skip to content
This repository was archived by the owner on Jun 4, 2026. It is now read-only.

Commit 3ff3b66

Browse files
Chrisclaude
authored andcommitted
feat: add --batch flag to push for atomic bulk uploads
boxel push now supports --batch (and --batch-size <n>, default 10). Definitions (.gts) upload individually in dependency order so the realm indexer sees FieldDefs before the CardDefs that contain them. Instances (.json) batch through /_atomic in N-at-a-time groups. Meaningful speedup when pushing dozens of files to a fresh workspace, and reduces the UI-flashing that happens when each card indexes on its own POST. Builds on the upload routing from the previous commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 93c5d2a commit 3ff3b66

2 files changed

Lines changed: 68 additions & 1 deletion

File tree

src/commands/push.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { RealmSyncBase, validateMatrixEnvVars, isProtectedFile, type SyncOptions } from '../lib/realm-sync-base.js';
22
import { CheckpointManager, type CheckpointChange } from '../lib/checkpoint-manager.js';
3+
import { uploadWithBatching, type FileToUpload } from '../lib/batch-upload.js';
34
import * as fs from 'fs';
45
import * as path from 'path';
56
import * as crypto from 'crypto';
@@ -34,6 +35,8 @@ function saveManifest(localDir: string, manifest: SyncManifest): void {
3435
interface PushOptions extends SyncOptions {
3536
deleteRemote?: boolean;
3637
force?: boolean;
38+
batch?: boolean;
39+
batchSize?: number;
3740
}
3841

3942
class RealmPusher extends RealmSyncBase {
@@ -120,6 +123,64 @@ class RealmPusher extends RealmSyncBase {
120123

121124
if (filesToUpload.size === 0) {
122125
console.log('No files to upload - everything is up to date');
126+
} else if (this.pushOptions.batch) {
127+
// Batch upload mode: .gts files individually (in dependency order), .json via /_atomic
128+
const batchSize = this.pushOptions.batchSize ?? 10;
129+
130+
// Determine operation type: 'add' for new files, 'update' for existing
131+
const remoteFiles = await this.getRemoteFileList();
132+
const allFiles: FileToUpload[] = Array.from(filesToUpload.entries()).map(([relativePath, localPath]) => ({
133+
relativePath,
134+
localPath,
135+
operation: remoteFiles.has(relativePath) ? 'update' as const : 'add' as const,
136+
}));
137+
138+
// Separate definitions from instances
139+
const { sortDefinitionsFirst } = await import('../lib/batch-upload.js');
140+
const sorted = sortDefinitionsFirst(allFiles);
141+
const definitions = sorted.filter(f => f.relativePath.endsWith('.gts'));
142+
const instances = sorted.filter(f => !f.relativePath.endsWith('.gts'));
143+
144+
// Upload .gts files individually in dependency order
145+
if (definitions.length > 0) {
146+
console.log(`Uploading ${definitions.length} definition(s) in dependency order...`);
147+
for (const file of definitions) {
148+
try {
149+
await this.uploadFile(file.relativePath, file.localPath);
150+
newManifest.files[file.relativePath] = computeFileHash(file.localPath);
151+
} catch (error) {
152+
this.hasError = true;
153+
console.error(`Error uploading ${file.relativePath}:`, error);
154+
}
155+
}
156+
}
157+
158+
// Batch upload .json files via /_atomic
159+
if (instances.length > 0) {
160+
console.log(`Batch uploading ${instances.length} instance(s) (${batchSize} per batch)...`);
161+
const jwt = await this.realmAuthClient.getJWT();
162+
const result = await uploadWithBatching(instances, this.options.workspaceUrl, jwt, {
163+
batchSize,
164+
definitionsFirst: false, // already separated
165+
dryRun: this.options.dryRun,
166+
});
167+
168+
// Update manifest for successfully uploaded files
169+
if (result.uploaded > 0) {
170+
for (const file of instances) {
171+
if (fs.existsSync(file.localPath)) {
172+
newManifest.files[file.relativePath] = computeFileHash(file.localPath);
173+
}
174+
}
175+
}
176+
177+
if (result.failed > 0) {
178+
this.hasError = true;
179+
for (const err of result.errors) {
180+
console.error(`Error uploading ${err.path}: ${err.error}`);
181+
}
182+
}
183+
}
123184
} else {
124185
console.log(`Uploading ${filesToUpload.size} file(s)...`);
125186

@@ -194,6 +255,8 @@ export interface PushCommandOptions {
194255
delete?: boolean;
195256
dryRun?: boolean;
196257
force?: boolean;
258+
batch?: boolean;
259+
batchSize?: number;
197260
}
198261

199262
export async function pushCommand(
@@ -217,6 +280,8 @@ export async function pushCommand(
217280
deleteRemote: options.delete,
218281
dryRun: options.dryRun,
219282
force: options.force,
283+
batch: options.batch,
284+
batchSize: options.batchSize,
220285
},
221286
matrixUrl,
222287
username,

src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ program
6565
.option('--delete', 'Delete remote files that do not exist locally')
6666
.option('--dry-run', 'Show what would be done without making changes')
6767
.option('--force', 'Upload all files, even if unchanged')
68-
.action(async (localDir: string, workspaceUrl: string, options: { delete?: boolean; dryRun?: boolean; force?: boolean }) => {
68+
.option('--batch', 'Use atomic batch upload for faster bulk operations (10 files per batch)')
69+
.option('--batch-size <n>', 'Files per batch when using --batch (default: 10)', parseInt)
70+
.action(async (localDir: string, workspaceUrl: string, options: { delete?: boolean; dryRun?: boolean; force?: boolean; batch?: boolean; batchSize?: number }) => {
6971
await pushCommand(localDir, workspaceUrl, options);
7072
});
7173

0 commit comments

Comments
 (0)