@abloatai/humans 0.59.2 → 0.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/Ablo.d.ts +2 -10
- package/dist/Ablo.js +0 -1
- package/dist/client.d.ts +1 -48
- package/dist/humans.d.ts +1 -1
- package/dist/local/BaseSyncedStore.d.ts +5 -5
- package/dist/local/BaseSyncedStore.js +7 -7
- package/dist/local/Database.d.ts +2 -2
- package/dist/local/LazyReferenceCollection.d.ts +1 -1
- package/dist/local/Model.js +46 -56
- package/dist/local/NetworkMonitor.js +2 -0
- package/dist/local/RuntimeContext.js +2 -0
- package/dist/local/SyncClient.d.ts +12 -36
- package/dist/local/SyncClient.js +50 -108
- package/dist/local/client/createModelOperations.d.ts +3 -27
- package/dist/local/client/createModelOperations.js +20 -21
- package/dist/local/client/options.d.ts +14 -39
- package/dist/local/client/reactiveEngine.d.ts +3 -9
- package/dist/local/client/reactiveEngine.js +6 -151
- package/dist/local/client/storeLifecycle.js +5 -1
- package/dist/local/fileUploads.d.ts +27 -0
- package/dist/local/fileUploads.js +55 -0
- package/dist/local/storeContract.d.ts +5 -5
- package/dist/local/stores/syncAction.d.ts +4 -4
- package/dist/local/sync/contextOnChange.js +1 -1
- package/dist/local/sync/createClaimStream.js +1 -1
- package/dist/local/sync/credentialLifecycle.d.ts +4 -5
- package/dist/local/sync/credentialLifecycle.js +4 -5
- package/dist/local/sync/deltaPipeline.js +12 -6
- package/dist/local/sync/schemas.d.ts +10 -10
- package/dist/local/sync/scopeGroups.d.ts +11 -0
- package/dist/local/sync/scopeGroups.js +75 -0
- package/dist/local/sync/wsFrameHandlers.d.ts +1 -1
- package/dist/local/transactions/localMutation.js +3 -3
- package/dist/local/transactions/mutations/MutationQueue.d.ts +4 -5
- package/dist/local/transactions/mutations/MutationQueue.js +25 -51
- package/dist/local/transactions/mutations/batchProcessing.js +23 -10
- package/dist/local/transactions/mutations/commitPayload.d.ts +9 -2
- package/dist/local/transactions/mutations/commitTransport.js +3 -1
- package/dist/local/transactions/mutations/executionSelection.d.ts +0 -1
- package/dist/local/transactions/mutations/executionSelection.js +9 -17
- package/dist/local/transactions/mutations/failureHandling.js +9 -0
- package/dist/local/transactions/mutations/localMutation.js +3 -3
- package/dist/local/transactions/mutations/queueCoalescing.js +8 -0
- package/dist/local/transactions/mutations/replayValidation.d.ts +15 -15
- package/dist/react/AbloProvider.d.ts +11 -86
- package/dist/react/AbloProvider.js +11 -163
- package/dist/react/ClientSideSuspense.d.ts +1 -1
- package/dist/react/DefaultFallback.d.ts +1 -1
- package/dist/react/createAbloReact.js +1 -1
- package/dist/react/useErrorListener.js +1 -1
- package/dist/react/useMutationFailureListener.js +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/surface.d.ts +3 -3
- package/dist/surface.js +1 -4
- package/package.json +3 -3
- package/src/Ablo.ts +5 -17
- package/src/client.ts +0 -51
- package/src/local/BaseSyncedStore.ts +14 -14
- package/src/local/Model.ts +45 -55
- package/src/local/NetworkMonitor.ts +2 -0
- package/src/local/RuntimeContext.ts +2 -0
- package/src/local/SyncClient.ts +73 -140
- package/src/local/client/createModelOperations.ts +30 -64
- package/src/local/client/options.ts +20 -43
- package/src/local/client/reactiveEngine.ts +7 -179
- package/src/local/client/storeLifecycle.ts +6 -1
- package/src/local/fileUploads.ts +97 -0
- package/src/local/storeContract.ts +5 -5
- package/src/local/sync/contextOnChange.ts +1 -1
- package/src/local/sync/createClaimStream.ts +1 -1
- package/src/local/sync/credentialLifecycle.ts +4 -5
- package/src/local/sync/deltaPipeline.ts +10 -6
- package/src/local/sync/scopeGroups.ts +91 -0
- package/src/local/sync/wsFrameHandlers.ts +0 -1
- package/src/local/transactions/localMutation.ts +3 -3
- package/src/local/transactions/mutations/MutationQueue.ts +24 -53
- package/src/local/transactions/mutations/batchProcessing.ts +25 -10
- package/src/local/transactions/mutations/commitPayload.ts +11 -1
- package/src/local/transactions/mutations/commitTransport.ts +2 -2
- package/src/local/transactions/mutations/executionSelection.ts +9 -15
- package/src/local/transactions/mutations/failureHandling.ts +10 -0
- package/src/local/transactions/mutations/localMutation.ts +3 -3
- package/src/local/transactions/mutations/queueCoalescing.ts +6 -0
- package/src/react/AbloProvider.tsx +17 -249
- package/src/react/useErrorListener.ts +1 -1
- package/src/react/useMutationFailureListener.ts +1 -1
- package/src/react.ts +1 -5
- package/src/surface.ts +1 -4
- package/dist/local/sync/participants.d.ts +0 -132
- package/dist/local/sync/participants.js +0 -342
- package/dist/local/transactions/mutations/pendingDrain.d.ts +0 -33
- package/dist/local/transactions/mutations/pendingDrain.js +0 -117
- package/src/local/sync/participants.ts +0 -564
- package/src/local/transactions/mutations/pendingDrain.ts +0 -169
package/src/local/Model.ts
CHANGED
|
@@ -416,7 +416,7 @@ export abstract class Model {
|
|
|
416
416
|
const original = this.getOriginalSnapshot();
|
|
417
417
|
for (const key of keys) {
|
|
418
418
|
if (key === 'id') continue;
|
|
419
|
-
const mod = modified
|
|
419
|
+
const mod = modified.get(key);
|
|
420
420
|
if (mod) {
|
|
421
421
|
out[key] = mod.old;
|
|
422
422
|
} else if (original && key in original) {
|
|
@@ -463,25 +463,23 @@ export abstract class Model {
|
|
|
463
463
|
const modelName = this.getModelName();
|
|
464
464
|
const properties = getActiveRegistry().getProperties(modelName);
|
|
465
465
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
errors.push(`${propName} is required`);
|
|
474
|
-
}
|
|
466
|
+
const json = this.toJSON();
|
|
467
|
+
for (const [propName, metadata] of properties) {
|
|
468
|
+
// Check required fields
|
|
469
|
+
if (!metadata.nullable && !metadata.optional) {
|
|
470
|
+
const value = json[propName];
|
|
471
|
+
if (value == null || value === '') {
|
|
472
|
+
errors.push(`${propName} is required`);
|
|
475
473
|
}
|
|
474
|
+
}
|
|
476
475
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}
|
|
476
|
+
// Run custom validation rules
|
|
477
|
+
const rules = this.validationRules[propName];
|
|
478
|
+
if (rules) {
|
|
479
|
+
const value = json[propName];
|
|
480
|
+
for (const rule of rules) {
|
|
481
|
+
const error = rule(value);
|
|
482
|
+
if (error) errors.push(error);
|
|
485
483
|
}
|
|
486
484
|
}
|
|
487
485
|
}
|
|
@@ -816,18 +814,16 @@ export abstract class Model {
|
|
|
816
814
|
result.archivedAt = this.archivedAt?.toISOString() ?? null;
|
|
817
815
|
}
|
|
818
816
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
result[propName] = value;
|
|
830
|
-
}
|
|
817
|
+
const self = this as Record<string, unknown>;
|
|
818
|
+
for (const [propName, metadata] of properties) {
|
|
819
|
+
// Skip certain types
|
|
820
|
+
if (metadata.type === 'ephemeralProperty') continue;
|
|
821
|
+
if (metadata.type === 'referenceModel') continue;
|
|
822
|
+
if (metadata.type === 'referenceCollection') continue;
|
|
823
|
+
|
|
824
|
+
const value = self[propName];
|
|
825
|
+
if (value !== undefined) {
|
|
826
|
+
result[propName] = value;
|
|
831
827
|
}
|
|
832
828
|
}
|
|
833
829
|
|
|
@@ -946,14 +942,12 @@ export abstract class Model {
|
|
|
946
942
|
const modelName = this.getModelName();
|
|
947
943
|
const properties = getActiveRegistry().getProperties(modelName);
|
|
948
944
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
collection.dispose();
|
|
956
|
-
}
|
|
945
|
+
const self = this as Record<string, unknown>;
|
|
946
|
+
for (const [propName, metadata] of properties) {
|
|
947
|
+
if (metadata.type === 'referenceCollection') {
|
|
948
|
+
const collection = self[propName] as Disposable | undefined;
|
|
949
|
+
if (collection) {
|
|
950
|
+
collection.dispose();
|
|
957
951
|
}
|
|
958
952
|
}
|
|
959
953
|
}
|
|
@@ -983,11 +977,9 @@ export abstract class Model {
|
|
|
983
977
|
const modelName = this.getModelName();
|
|
984
978
|
const properties = getActiveRegistry().getProperties(modelName);
|
|
985
979
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
snapshot[propName] = json[propName];
|
|
990
|
-
}
|
|
980
|
+
const json = this.toJSON();
|
|
981
|
+
for (const [propName] of properties) {
|
|
982
|
+
snapshot[propName] = json[propName];
|
|
991
983
|
}
|
|
992
984
|
|
|
993
985
|
return snapshot;
|
|
@@ -1034,19 +1026,17 @@ export abstract class Model {
|
|
|
1034
1026
|
|
|
1035
1027
|
const properties = getActiveRegistry().getProperties(this.getModelName());
|
|
1036
1028
|
const self = this as Record<string, unknown>;
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
continue;
|
|
1045
|
-
}
|
|
1046
|
-
// Reading through the observable getter is the point: it subscribes the
|
|
1047
|
-
// enclosing MobX reaction to this field.
|
|
1048
|
-
snapshot[propName] = self[propName];
|
|
1029
|
+
for (const [propName, metadata] of properties) {
|
|
1030
|
+
if (
|
|
1031
|
+
metadata.type === 'ephemeralProperty' ||
|
|
1032
|
+
metadata.type === 'referenceModel' ||
|
|
1033
|
+
metadata.type === 'referenceCollection'
|
|
1034
|
+
) {
|
|
1035
|
+
continue;
|
|
1049
1036
|
}
|
|
1037
|
+
// Reading through the observable getter is the point: it subscribes the
|
|
1038
|
+
// enclosing MobX reaction to this field.
|
|
1039
|
+
snapshot[propName] = self[propName];
|
|
1050
1040
|
}
|
|
1051
1041
|
|
|
1052
1042
|
for (const name of this.getDerivedGetterNames()) {
|
|
@@ -14,6 +14,8 @@ export class NetworkMonitor extends EventEmitter {
|
|
|
14
14
|
// Only `navigator.onLine === false` means offline. Node 18+ exposes a global
|
|
15
15
|
// `navigator` with `onLine === undefined`, so the naive `navigator.onLine`
|
|
16
16
|
// would seed `false` (offline) on every server client — start optimistic.
|
|
17
|
+
// DOM types say `onLine` is boolean, but Node exposes it as undefined.
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare
|
|
17
19
|
private isOnline = !(typeof navigator !== 'undefined' && navigator.onLine === false);
|
|
18
20
|
private lastOnlineCheck: Date = new Date();
|
|
19
21
|
|
|
@@ -106,6 +106,8 @@ export const browserOnlineStatus: OnlineStatusProvider = {
|
|
|
106
106
|
// signal. Don't use `!navigator.onLine`: Node 18+ exposes a global
|
|
107
107
|
// `navigator` whose `onLine` is `undefined`, which `!` would read as offline —
|
|
108
108
|
// wedging every Node/server client (agents, worker, MCP) into a false offline.
|
|
109
|
+
// DOM types say `onLine` is boolean, but Node exposes it as undefined.
|
|
110
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare
|
|
109
111
|
return !(typeof navigator !== 'undefined' && navigator.onLine === false);
|
|
110
112
|
},
|
|
111
113
|
};
|
package/src/local/SyncClient.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { deepEqual, snapshotJsonValue } from '@abloatai/transaction/utils/json';
|
|
|
18
18
|
import { LoadStrategy } from '@abloatai/transaction/types';
|
|
19
19
|
import { globalRuntime } from './context.js';
|
|
20
20
|
import type { RuntimeContext } from './RuntimeContext.js';
|
|
21
|
-
import {
|
|
21
|
+
import { AbloError, AbloValidationError } from '@abloatai/transaction/errors';
|
|
22
22
|
import { EventEmitter } from 'events';
|
|
23
23
|
import { NetworkMonitor } from './NetworkMonitor.js';
|
|
24
24
|
import {
|
|
@@ -51,9 +51,16 @@ import {
|
|
|
51
51
|
type SyncState,
|
|
52
52
|
} from './syncClientTypes.js';
|
|
53
53
|
import type { BootstrapSnapshot } from './syncClientTypes.js';
|
|
54
|
+
import {
|
|
55
|
+
batchUploadFiles,
|
|
56
|
+
uploadFile,
|
|
57
|
+
type BatchFileUploadOptions,
|
|
58
|
+
type FileUploadContext,
|
|
59
|
+
type FileUploadOptions,
|
|
60
|
+
} from './fileUploads.js';
|
|
54
61
|
|
|
55
62
|
export type { BootstrapSnapshot, RehydrationStats } from './syncClientTypes.js';
|
|
56
|
-
|
|
63
|
+
const ignoreSeparatelyObservedMutationFailure = (): undefined => undefined;
|
|
57
64
|
export class SyncClient extends EventEmitter {
|
|
58
65
|
private objectPool: InstanceCache;
|
|
59
66
|
private database: Database;
|
|
@@ -935,7 +942,8 @@ export class SyncClient extends EventEmitter {
|
|
|
935
942
|
model: Model,
|
|
936
943
|
poolAction: () => void,
|
|
937
944
|
writeOptions?: WriteOptions,
|
|
938
|
-
|
|
945
|
+
capturedChangesOverride?: Record<string, unknown>,
|
|
946
|
+
): Promise<void> | undefined {
|
|
939
947
|
// No-op UPDATE guard (O(1)). An update with no dirty fields would travel
|
|
940
948
|
// to the server, get dropped by `coalesceOperations` Rule 4 (empty input),
|
|
941
949
|
// and — if it was the only op — come back as `lastSyncId: 0`. That trips
|
|
@@ -949,24 +957,32 @@ export class SyncClient extends EventEmitter {
|
|
|
949
957
|
// is false → we fall through to the normal path rather than risk dropping a
|
|
950
958
|
// real write. Only a genuine Model with an empty dirty-set is skipped.
|
|
951
959
|
const hasChanges: unknown = model.hasChanges;
|
|
952
|
-
if (
|
|
953
|
-
|
|
960
|
+
if (
|
|
961
|
+
type === 'update' &&
|
|
962
|
+
hasChanges === false &&
|
|
963
|
+
capturedChangesOverride === undefined
|
|
964
|
+
) {
|
|
965
|
+
return Promise.resolve();
|
|
954
966
|
}
|
|
955
967
|
|
|
956
968
|
// Capture changes before the pool action runs. Pool operations —
|
|
957
969
|
// upsert in particular — can clear the model's local changes, so
|
|
958
970
|
// capturing first ensures they are never lost.
|
|
959
|
-
const capturedChanges =
|
|
960
|
-
|
|
971
|
+
const capturedChanges = capturedChangesOverride !== undefined
|
|
972
|
+
? Object.freeze({ ...capturedChangesOverride })
|
|
973
|
+
: type === 'update' || type === 'create'
|
|
974
|
+
? this.captureModelChanges(model)
|
|
975
|
+
: undefined;
|
|
961
976
|
|
|
962
977
|
poolAction();
|
|
963
|
-
this.stageMutation(type, model, capturedChanges, writeOptions);
|
|
978
|
+
const confirmation = this.stageMutation(type, model, capturedChanges, writeOptions);
|
|
964
979
|
this.notifyObservers({
|
|
965
980
|
type,
|
|
966
981
|
modelType: model.getModelName(),
|
|
967
982
|
model: type !== 'delete' ? model : undefined,
|
|
968
983
|
modelId: model.id,
|
|
969
984
|
});
|
|
985
|
+
return confirmation;
|
|
970
986
|
|
|
971
987
|
// QueryProcessor uses `models:changed` to invalidate caches. Coalesce
|
|
972
988
|
// to one event per microtask: a paste of 100 rows should re-run
|
|
@@ -1004,13 +1020,23 @@ export class SyncClient extends EventEmitter {
|
|
|
1004
1020
|
}
|
|
1005
1021
|
|
|
1006
1022
|
/** Add new model (CREATE) - works offline */
|
|
1007
|
-
add(model: Model, options?: WriteOptions): void {
|
|
1008
|
-
this.mutate('create', model, () => { this.objectPool.add(model, ModelScope.live); }, options);
|
|
1023
|
+
add(model: Model, options?: WriteOptions): Promise<void> | undefined {
|
|
1024
|
+
return this.mutate('create', model, () => { this.objectPool.add(model, ModelScope.live); }, options);
|
|
1009
1025
|
}
|
|
1010
1026
|
|
|
1011
1027
|
/** Update existing model (UPDATE) - works offline */
|
|
1012
|
-
update(
|
|
1013
|
-
|
|
1028
|
+
update(
|
|
1029
|
+
model: Model,
|
|
1030
|
+
options?: WriteOptions,
|
|
1031
|
+
capturedChanges?: Record<string, unknown>,
|
|
1032
|
+
): Promise<void> | undefined {
|
|
1033
|
+
return this.mutate(
|
|
1034
|
+
'update',
|
|
1035
|
+
model,
|
|
1036
|
+
() => { this.objectPool.upsert(model, ModelScope.live); },
|
|
1037
|
+
options,
|
|
1038
|
+
capturedChanges,
|
|
1039
|
+
);
|
|
1014
1040
|
}
|
|
1015
1041
|
|
|
1016
1042
|
/**
|
|
@@ -1038,7 +1064,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1038
1064
|
if (capturedChanges === undefined) return;
|
|
1039
1065
|
|
|
1040
1066
|
this.objectPool.upsert(model, ModelScope.live);
|
|
1041
|
-
this.stageMutation('update', model, capturedChanges);
|
|
1067
|
+
void this.stageMutation('update', model, capturedChanges);
|
|
1042
1068
|
this.notifyObservers({
|
|
1043
1069
|
type: 'update',
|
|
1044
1070
|
modelType: model.getModelName(),
|
|
@@ -1055,134 +1081,36 @@ export class SyncClient extends EventEmitter {
|
|
|
1055
1081
|
}
|
|
1056
1082
|
|
|
1057
1083
|
/** Delete model (DELETE) - works offline */
|
|
1058
|
-
delete(model: Model, options?: WriteOptions): void {
|
|
1084
|
+
delete(model: Model, options?: WriteOptions): Promise<void> | undefined {
|
|
1059
1085
|
// Clear pending mutations first to prevent "not found" errors on fast delete
|
|
1060
1086
|
this.mutationQueue.cancelTransactionsForModel(model.id);
|
|
1061
|
-
this.mutate('delete', model, () => this.objectPool.remove(model.id), options);
|
|
1087
|
+
return this.mutate('delete', model, () => this.objectPool.remove(model.id), options);
|
|
1062
1088
|
}
|
|
1063
1089
|
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
* the {@link MutationQueue}, and a model is built from the server's
|
|
1067
|
-
* response and added to the pool.
|
|
1068
|
-
*/
|
|
1069
|
-
async uploadFile(
|
|
1070
|
-
file: File,
|
|
1071
|
-
options: {
|
|
1072
|
-
id: string;
|
|
1073
|
-
attachableType: string;
|
|
1074
|
-
attachableId: string;
|
|
1075
|
-
metadata?: Record<string, unknown>;
|
|
1076
|
-
}
|
|
1077
|
-
): Promise<Model | null> {
|
|
1078
|
-
if (!this.userId || !this.organizationId) {
|
|
1079
|
-
throw new AbloAuthenticationError('Authentication required for file uploads', {
|
|
1080
|
-
code: 'file_upload_auth_required',
|
|
1081
|
-
});
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
try {
|
|
1085
|
-
// Use MutationQueue to handle the upload mutation
|
|
1086
|
-
const result = await this.mutationQueue.uploadAttachment(
|
|
1087
|
-
file,
|
|
1088
|
-
{
|
|
1089
|
-
id: options.id,
|
|
1090
|
-
attachableType: options.attachableType,
|
|
1091
|
-
attachableId: options.attachableId,
|
|
1092
|
-
metadata: options.metadata,
|
|
1093
|
-
},
|
|
1094
|
-
{
|
|
1095
|
-
userId: this.userId,
|
|
1096
|
-
organizationId: this.organizationId,
|
|
1097
|
-
}
|
|
1098
|
-
);
|
|
1099
|
-
|
|
1100
|
-
if (result) {
|
|
1101
|
-
// Create model from response using ModelRegistry (generic — no concrete class import)
|
|
1102
|
-
const model = this.objectPool.createFromData({
|
|
1103
|
-
id: options.id,
|
|
1104
|
-
...result,
|
|
1105
|
-
});
|
|
1106
|
-
|
|
1107
|
-
if (model) {
|
|
1108
|
-
this.objectPool.add(model, ModelScope.live);
|
|
1109
|
-
this.notifyObservers({
|
|
1110
|
-
type: 'create',
|
|
1111
|
-
modelType: model.getModelName(),
|
|
1112
|
-
model,
|
|
1113
|
-
});
|
|
1114
|
-
return model;
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
return null;
|
|
1119
|
-
} catch (error) {
|
|
1120
|
-
this.runtime.observability.captureMutationFailure({
|
|
1121
|
-
context: 'file-upload',
|
|
1122
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
1123
|
-
});
|
|
1124
|
-
throw error;
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
/**
|
|
1129
|
-
* Batch upload files — single GraphQL call + parallel S3 PUTs.
|
|
1130
|
-
*
|
|
1131
|
-
* Returns the raw `Model[]` built by the object pool (typename is
|
|
1132
|
-
* determined by the payload the server returns — currently always
|
|
1133
|
-
* `Attachment`). The SDK has no knowledge of app-specific model classes,
|
|
1134
|
-
* so it cannot honestly claim a narrower return type; consumers that
|
|
1135
|
-
* need an `Attachment[]` project through their own typed accessor
|
|
1136
|
-
* (e.g. `store.query.attachments.findMany({ where: { id: IN ids } })`)
|
|
1137
|
-
* after the upload resolves.
|
|
1138
|
-
*/
|
|
1139
|
-
async batchUploadFiles(
|
|
1140
|
-
files: File[],
|
|
1141
|
-
options: {
|
|
1142
|
-
ids: string[];
|
|
1143
|
-
attachableType: string;
|
|
1144
|
-
attachableId: string;
|
|
1145
|
-
metadata?: Record<string, unknown>;
|
|
1146
|
-
}
|
|
1147
|
-
): Promise<Model[]> {
|
|
1148
|
-
if (!this.userId || !this.organizationId) {
|
|
1149
|
-
throw new AbloAuthenticationError('Authentication required for file uploads', {
|
|
1150
|
-
code: 'file_upload_auth_required',
|
|
1151
|
-
});
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
const items = options.ids.map((id) => ({
|
|
1155
|
-
id,
|
|
1156
|
-
attachableType: options.attachableType,
|
|
1157
|
-
attachableId: options.attachableId,
|
|
1158
|
-
metadata: options.metadata,
|
|
1159
|
-
}));
|
|
1160
|
-
|
|
1161
|
-
const results = await this.mutationQueue.batchUploadAttachments(files, items, {
|
|
1090
|
+
private fileUploadContext(): FileUploadContext {
|
|
1091
|
+
return {
|
|
1162
1092
|
userId: this.userId,
|
|
1163
1093
|
organizationId: this.organizationId,
|
|
1164
|
-
|
|
1094
|
+
mutationQueue: this.mutationQueue,
|
|
1095
|
+
objectPool: this.objectPool,
|
|
1096
|
+
observability: this.runtime.observability,
|
|
1097
|
+
notifyCreated: (model) => {
|
|
1098
|
+
this.notifyObservers({ type: 'create', modelType: model.getModelName(), model });
|
|
1099
|
+
},
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1165
1102
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
if (model) {
|
|
1170
|
-
this.objectPool.add(model, ModelScope.live);
|
|
1171
|
-
this.notifyObservers({
|
|
1172
|
-
type: 'create',
|
|
1173
|
-
modelType: model.getModelName(),
|
|
1174
|
-
model,
|
|
1175
|
-
});
|
|
1176
|
-
models.push(model);
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1103
|
+
uploadFile(file: File, options: FileUploadOptions): Promise<Model | null> {
|
|
1104
|
+
return uploadFile(this.fileUploadContext(), file, options);
|
|
1105
|
+
}
|
|
1179
1106
|
|
|
1180
|
-
|
|
1107
|
+
batchUploadFiles(files: File[], options: BatchFileUploadOptions): Promise<Model[]> {
|
|
1108
|
+
return batchUploadFiles(this.fileUploadContext(), files, options);
|
|
1181
1109
|
}
|
|
1182
1110
|
|
|
1183
1111
|
/** Archive model (ARCHIVE) - works offline */
|
|
1184
|
-
archive(model: Model): void {
|
|
1185
|
-
this.mutate('archive', model, () => { this.objectPool.updateScope(model.id, ModelScope.archived); });
|
|
1112
|
+
archive(model: Model): Promise<void> | undefined {
|
|
1113
|
+
return this.mutate('archive', model, () => { this.objectPool.updateScope(model.id, ModelScope.archived); });
|
|
1186
1114
|
}
|
|
1187
1115
|
|
|
1188
1116
|
/**
|
|
@@ -1204,8 +1132,8 @@ export class SyncClient extends EventEmitter {
|
|
|
1204
1132
|
model: Model,
|
|
1205
1133
|
capturedChanges?: Record<string, unknown>,
|
|
1206
1134
|
writeOptions?: WriteOptions,
|
|
1207
|
-
): void {
|
|
1208
|
-
if (this.isDisposed) return;
|
|
1135
|
+
): Promise<void> | undefined {
|
|
1136
|
+
if (this.isDisposed) return Promise.resolve();
|
|
1209
1137
|
if (!this.userId || !this.organizationId) {
|
|
1210
1138
|
this.mutationQueue.deferMutation(type, model, capturedChanges, writeOptions);
|
|
1211
1139
|
return;
|
|
@@ -1218,6 +1146,13 @@ export class SyncClient extends EventEmitter {
|
|
|
1218
1146
|
capturedChanges,
|
|
1219
1147
|
writeOptions,
|
|
1220
1148
|
);
|
|
1149
|
+
const confirmation = staging.then(async (transaction) => {
|
|
1150
|
+
await transaction.confirmation;
|
|
1151
|
+
});
|
|
1152
|
+
// Most internal callers intentionally use fire-and-forget writes. Observe
|
|
1153
|
+
// their rejection without replacing the exact promise returned to model
|
|
1154
|
+
// operations that need authoritative per-transaction confirmation.
|
|
1155
|
+
void confirmation.catch(ignoreSeparatelyObservedMutationFailure);
|
|
1221
1156
|
const pending = staging.then(() => undefined).catch((error: Error) => {
|
|
1222
1157
|
this.runtime.observability.captureMutationFailure({
|
|
1223
1158
|
context: `stage-mutation-${type}`,
|
|
@@ -1228,6 +1163,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1228
1163
|
});
|
|
1229
1164
|
this.pendingStages.add(pending);
|
|
1230
1165
|
void pending.finally(() => this.pendingStages.delete(pending));
|
|
1166
|
+
return confirmation;
|
|
1231
1167
|
}
|
|
1232
1168
|
|
|
1233
1169
|
private scheduleSync(): void {
|
|
@@ -1468,14 +1404,11 @@ export class SyncClient extends EventEmitter {
|
|
|
1468
1404
|
markConnected(): void {
|
|
1469
1405
|
this.setConnectionState('connected');
|
|
1470
1406
|
// Browser online state may have marked the client connected before the
|
|
1471
|
-
// WebSocket itself was ready.
|
|
1472
|
-
//
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
1477
|
-
});
|
|
1478
|
-
});
|
|
1407
|
+
// WebSocket itself was ready. Kick the durable lanes through the staging
|
|
1408
|
+
// barrier: a model mutation enters the in-memory store before its journal
|
|
1409
|
+
// row finishes saving, so a direct reconnect drain can otherwise try to
|
|
1410
|
+
// seal a source record that does not exist yet. The pending drain also
|
|
1411
|
+
// starts the atomic commit lane, so one ordered entry point covers both.
|
|
1479
1412
|
void this.processPendingMutations();
|
|
1480
1413
|
}
|
|
1481
1414
|
|