Skip to content

Commit 9a8364e

Browse files
committed
feat(datastore): add per-model syncPageSize and maxRecordsToSync configuration (#7310)
Allow syncExpression() to accept an optional third parameter with per-model sync settings (syncPageSize, maxRecordsToSync) that override global defaults. Models without per-model config continue to use the global values. This lets users lower the page size for models with large records (e.g., exceeding AppSync's 1MB response limit) without penalizing sync performance for all other models.
1 parent d2ae770 commit 9a8364e

File tree

5 files changed

+217
-10
lines changed

5 files changed

+217
-10
lines changed

packages/datastore/__tests__/connectivityHandling.test.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,6 +1106,112 @@ describe('DataStore sync engine', () => {
11061106
}
11071107
`);
11081108
});
1109+
1110+
describe('per-model sync configuration', () => {
1111+
/**
1112+
* These tests verify that per-model syncPageSize and maxRecordsToSync
1113+
* overrides (passed as the 3rd argument to syncExpression) are correctly
1114+
* applied to sync queries, while models without overrides fall back to
1115+
* the global defaults.
1116+
*
1117+
* graphqlService.requests accumulates across the entire test lifecycle
1118+
* (initial sync from beforeEach, data generation, resync). We snapshot
1119+
* the request count before resyncWith() and only inspect requests made
1120+
* after that point.
1121+
*/
1122+
1123+
const getSyncRequestsAfter = (
1124+
tableName: string,
1125+
afterIndex: number,
1126+
) =>
1127+
graphqlService.requests.slice(afterIndex).filter(
1128+
r =>
1129+
r.operation === 'query' &&
1130+
r.type === 'sync' &&
1131+
r.tableName === tableName,
1132+
);
1133+
1134+
test('per-model syncPageSize overrides global default', async () => {
1135+
(DataStore as any).amplifyConfig.syncPageSize = 1000;
1136+
(DataStore as any).amplifyConfig.maxRecordsToSync = 10000;
1137+
1138+
const requestsBefore = graphqlService.requests.length;
1139+
await resyncWith([
1140+
syncExpression(Post, () => Predicates.ALL, {
1141+
syncPageSize: 50,
1142+
}),
1143+
]);
1144+
1145+
const postSyncRequests = getSyncRequestsAfter('Post', requestsBefore);
1146+
expect(postSyncRequests.length).toBeGreaterThan(0);
1147+
expect(postSyncRequests[0].variables.limit).toBe(50);
1148+
});
1149+
1150+
test('model without per-model config uses global syncPageSize', async () => {
1151+
(DataStore as any).amplifyConfig.syncPageSize = 500;
1152+
(DataStore as any).amplifyConfig.maxRecordsToSync = 10000;
1153+
1154+
const requestsBefore = graphqlService.requests.length;
1155+
await resyncWith([
1156+
syncExpression(Post, () => Predicates.ALL, {
1157+
syncPageSize: 50,
1158+
}),
1159+
]);
1160+
1161+
// Comment has no per-model config, should use global
1162+
const commentSyncRequests = getSyncRequestsAfter(
1163+
'Comment',
1164+
requestsBefore,
1165+
);
1166+
expect(commentSyncRequests.length).toBeGreaterThan(0);
1167+
expect(commentSyncRequests[0].variables.limit).toBe(500);
1168+
});
1169+
1170+
test('multiple models with different per-model syncPageSize', async () => {
1171+
(DataStore as any).amplifyConfig.syncPageSize = 1000;
1172+
(DataStore as any).amplifyConfig.maxRecordsToSync = 10000;
1173+
1174+
const requestsBefore = graphqlService.requests.length;
1175+
await resyncWith([
1176+
syncExpression(Post, () => Predicates.ALL, {
1177+
syncPageSize: 75,
1178+
}),
1179+
syncExpression(Model, () => Predicates.ALL, {
1180+
syncPageSize: 200,
1181+
}),
1182+
]);
1183+
1184+
const postSyncRequests = getSyncRequestsAfter('Post', requestsBefore);
1185+
expect(postSyncRequests.length).toBeGreaterThan(0);
1186+
expect(postSyncRequests[0].variables.limit).toBe(75);
1187+
1188+
const modelSyncRequests = getSyncRequestsAfter(
1189+
'Model',
1190+
requestsBefore,
1191+
);
1192+
expect(modelSyncRequests.length).toBeGreaterThan(0);
1193+
expect(modelSyncRequests[0].variables.limit).toBe(200);
1194+
});
1195+
1196+
test('per-model maxRecordsToSync caps limit below syncPageSize', async () => {
1197+
(DataStore as any).amplifyConfig.syncPageSize = 1000;
1198+
(DataStore as any).amplifyConfig.maxRecordsToSync = 10000;
1199+
1200+
const requestsBefore = graphqlService.requests.length;
1201+
await resyncWith([
1202+
syncExpression(Post, () => Predicates.ALL, {
1203+
syncPageSize: 1000,
1204+
maxRecordsToSync: 25,
1205+
}),
1206+
]);
1207+
1208+
const postSyncRequests = getSyncRequestsAfter('Post', requestsBefore);
1209+
expect(postSyncRequests.length).toBeGreaterThan(0);
1210+
// limit = Math.min(maxRecordsToSync - 0, syncPageSize) = Math.min(25, 1000) = 25
1211+
expect(postSyncRequests[0].variables.limit).toBe(25);
1212+
});
1213+
1214+
});
11091215
});
11101216

11111217
describe('error handling', () => {
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { syncExpression, PerModelSyncConfig } from '../src/types';
2+
import { Predicates } from '../src/predicates';
3+
4+
// Minimal fake model constructor — syncExpression only stores the reference,
5+
// it doesn't inspect it.
6+
const FakeModel: any = function FakeModel() {};
7+
FakeModel.copyOf = () => {};
8+
9+
describe('syncExpression', () => {
10+
test('returns syncConfig when provided', async () => {
11+
const config: PerModelSyncConfig = { syncPageSize: 50 };
12+
const result = await syncExpression(
13+
FakeModel,
14+
() => Predicates.ALL,
15+
config,
16+
);
17+
18+
expect(result.syncConfig).toEqual({ syncPageSize: 50 });
19+
expect(result.modelConstructor).toBe(FakeModel);
20+
});
21+
22+
test('returns syncConfig with both fields', async () => {
23+
const config: PerModelSyncConfig = {
24+
syncPageSize: 100,
25+
maxRecordsToSync: 500,
26+
};
27+
const result = await syncExpression(
28+
FakeModel,
29+
() => Predicates.ALL,
30+
config,
31+
);
32+
33+
expect(result.syncConfig).toEqual({
34+
syncPageSize: 100,
35+
maxRecordsToSync: 500,
36+
});
37+
});
38+
39+
test('syncConfig is undefined when omitted (backward compatible)', async () => {
40+
const result = await syncExpression(FakeModel, () => Predicates.ALL);
41+
42+
expect(result.syncConfig).toBeUndefined();
43+
expect(result.modelConstructor).toBe(FakeModel);
44+
expect(result.conditionProducer).toBeDefined();
45+
});
46+
47+
test('syncConfig is undefined when explicitly passed as undefined', async () => {
48+
const result = await syncExpression(
49+
FakeModel,
50+
() => Predicates.ALL,
51+
undefined,
52+
);
53+
54+
expect(result.syncConfig).toBeUndefined();
55+
});
56+
});

packages/datastore/src/datastore/datastore.ts

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import {
5050
NonModelTypeConstructor,
5151
ObserveQueryOptions,
5252
PaginationInput,
53+
PerModelSyncConfig,
5354
PersistentModel,
5455
PersistentModelConstructor,
5556
PersistentModelMetaData,
@@ -1551,7 +1552,10 @@ class DataStore {
15511552
aws_appsync_graphqlEndpoint,
15521553
);
15531554

1554-
this.syncPredicates = await this.processSyncExpressions();
1555+
const { syncPredicates, perModelSyncConfig } =
1556+
await this.processSyncExpressions();
1557+
this.syncPredicates = syncPredicates;
1558+
this.amplifyConfig.perModelSyncConfig = perModelSyncConfig;
15551559
this.sync = new SyncEngine(
15561560
schema,
15571561
namespaceResolver,
@@ -2675,22 +2679,40 @@ class DataStore {
26752679

26762680
/**
26772681
* Examines the configured `syncExpressions` and produces a WeakMap of
2678-
* SchemaModel -> predicate to use during sync.
2682+
* SchemaModel -> predicate to use during sync, and a WeakMap of
2683+
* SchemaModel -> per-model sync config.
26792684
*/
2680-
private async processSyncExpressions(): Promise<
2681-
WeakMap<SchemaModel, ModelPredicate<any> | null>
2682-
> {
2685+
private async processSyncExpressions(): Promise<{
2686+
syncPredicates: WeakMap<SchemaModel, ModelPredicate<any> | null>;
2687+
perModelSyncConfig: WeakMap<SchemaModel, PerModelSyncConfig>;
2688+
}> {
26832689
if (!this.syncExpressions || !this.syncExpressions.length) {
2684-
return new WeakMap<SchemaModel, ModelPredicate<any>>();
2690+
return {
2691+
syncPredicates: new WeakMap<SchemaModel, ModelPredicate<any>>(),
2692+
perModelSyncConfig: new WeakMap<SchemaModel, PerModelSyncConfig>(),
2693+
};
26852694
}
26862695

2696+
const perModelSyncConfig = new WeakMap<SchemaModel, PerModelSyncConfig>();
2697+
26872698
const syncPredicates = await Promise.all(
26882699
this.syncExpressions.map(
26892700
async (
26902701
syncExpression: SyncExpression,
26912702
): Promise<[SchemaModel, ModelPredicate<any> | null]> => {
2692-
const { modelConstructor, conditionProducer } = await syncExpression;
2693-
const modelDefinition = getModelDefinition(modelConstructor)!;
2703+
const { modelConstructor, conditionProducer, syncConfig } =
2704+
await syncExpression;
2705+
const modelDefinition = getModelDefinition(modelConstructor);
2706+
2707+
if (!modelDefinition) {
2708+
throw new Error(
2709+
`Invalid model provided to syncExpression: ${modelConstructor?.name}; unable to find model definition.`,
2710+
);
2711+
}
2712+
2713+
if (syncConfig) {
2714+
perModelSyncConfig.set(modelDefinition, syncConfig);
2715+
}
26942716

26952717
// conditionProducer is either a predicate, e.g. (c) => c.field.eq(1)
26962718
// OR a function/promise that returns a predicate
@@ -2714,7 +2736,10 @@ class DataStore {
27142736
),
27152737
);
27162738

2717-
return this.weakMapFromEntries(syncPredicates);
2739+
return {
2740+
syncPredicates: this.weakMapFromEntries(syncPredicates),
2741+
perModelSyncConfig,
2742+
};
27182743
}
27192744

27202745
private async unwrapPromise<T extends PersistentModel>(

packages/datastore/src/sync/processors/sync.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,11 @@ class SyncProcessor {
363363
start(
364364
typesLastSync: Map<SchemaModel, [string, number]>,
365365
): Observable<SyncModelPage> {
366-
const { maxRecordsToSync, syncPageSize } = this.amplifyConfig;
366+
const {
367+
maxRecordsToSync: defaultMaxRecordsToSync,
368+
syncPageSize: defaultSyncPageSize,
369+
perModelSyncConfig,
370+
} = this.amplifyConfig;
367371
const parentPromises = new Map<string, Promise<void>>();
368372
const observable = new Observable<SyncModelPage>(observer => {
369373
const sortedTypesLastSyncs = Object.values(this.schema.namespaces).reduce(
@@ -394,6 +398,12 @@ class SyncProcessor {
394398
let recordsReceived = 0;
395399
const filter = this.graphqlFilterFromPredicate(modelDefinition);
396400

401+
const modelSyncConfig = perModelSyncConfig?.get(modelDefinition);
402+
const syncPageSize =
403+
modelSyncConfig?.syncPageSize ?? defaultSyncPageSize;
404+
const maxRecordsToSync =
405+
modelSyncConfig?.maxRecordsToSync ?? defaultMaxRecordsToSync;
406+
397407
const parents = this.schema.namespaces[
398408
namespace
399409
].modelTopologicalOrdering!.get(modelDefinition.name);

packages/datastore/src/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,9 +1093,15 @@ export type ModelAuthModes = Record<
10931093
Record<ModelOperation, GraphQLAuthMode[]>
10941094
>;
10951095

1096+
export interface PerModelSyncConfig {
1097+
syncPageSize?: number;
1098+
maxRecordsToSync?: number;
1099+
}
1100+
10961101
export type SyncExpression = Promise<{
10971102
modelConstructor: any;
10981103
conditionProducer(c?: any): any;
1104+
syncConfig?: PerModelSyncConfig;
10991105
}>;
11001106

11011107
/*
@@ -1156,6 +1162,7 @@ type ConditionProducer<T extends PersistentModel, A extends Option<T>> = (
11561162
*
11571163
* @param modelConstructor The Model from the schema.
11581164
* @param conditionProducer A function that builds a condition object that can describe how to filter the model.
1165+
* @param syncConfig Optional per-model sync configuration (e.g., `syncPageSize`, `maxRecordsToSync`).
11591166
* @returns An sync expression object that can be attached to the DataStore `syncExpressions` configuration property.
11601167
*/
11611168
export async function syncExpression<
@@ -1164,13 +1171,16 @@ export async function syncExpression<
11641171
>(
11651172
modelConstructor: PersistentModelConstructor<T>,
11661173
conditionProducer: ConditionProducer<T, A>,
1174+
syncConfig?: PerModelSyncConfig,
11671175
): Promise<{
11681176
modelConstructor: PersistentModelConstructor<T>;
11691177
conditionProducer: ConditionProducer<T, A>;
1178+
syncConfig?: PerModelSyncConfig;
11701179
}> {
11711180
return {
11721181
modelConstructor,
11731182
conditionProducer,
1183+
syncConfig,
11741184
};
11751185
}
11761186

0 commit comments

Comments
 (0)