s3db.js 9.2.1 → 9.2.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "s3db.js",
3
- "version": "9.2.1",
3
+ "version": "9.2.2",
4
4
  "description": "Use AWS S3, the world's most reliable document storage, as a database with this ORM.",
5
5
  "main": "dist/s3db.cjs.js",
6
6
  "module": "dist/s3db.es.js",
@@ -17,10 +17,10 @@ class AsyncEventEmitter extends EventEmitter {
17
17
  return false;
18
18
  }
19
19
 
20
- setImmediate(() => {
20
+ setImmediate(async () => {
21
21
  for (const listener of listeners) {
22
22
  try {
23
- listener(...args);
23
+ await listener(...args);
24
24
  } catch (error) {
25
25
  if (event !== 'error') {
26
26
  this.emit('error', error);
@@ -135,7 +135,8 @@ export class Resource extends AsyncEventEmitter {
135
135
  idSize = 22,
136
136
  versioningEnabled = false,
137
137
  events = {},
138
- asyncEvents = true
138
+ asyncEvents = true,
139
+ asyncPartitions = true
139
140
  } = config;
140
141
 
141
142
  // Set instance properties
@@ -177,6 +178,7 @@ export class Resource extends AsyncEventEmitter {
177
178
  autoDecrypt,
178
179
  allNestedObjectsOptional,
179
180
  asyncEvents,
181
+ asyncPartitions,
180
182
  };
181
183
 
182
184
  // Initialize hooks system
@@ -817,14 +819,42 @@ export class Resource extends AsyncEventEmitter {
817
819
  // Get the inserted object
818
820
  const insertedObject = await this.get(finalId);
819
821
 
820
- // Execute afterInsert hooks
821
- const finalResult = await this.executeHooks('afterInsert', insertedObject);
822
-
823
- // Emit insert event
824
- this.emit('insert', finalResult);
825
-
826
- // Return the final object
827
- return finalResult;
822
+ // Handle partition indexing based on asyncPartitions config
823
+ if (this.config.asyncPartitions && this.config.partitions && Object.keys(this.config.partitions).length > 0) {
824
+ // Async mode: create partition indexes in background
825
+ setImmediate(() => {
826
+ this.createPartitionReferences(insertedObject).catch(err => {
827
+ this.emit('partitionIndexError', {
828
+ operation: 'insert',
829
+ id: finalId,
830
+ error: err,
831
+ message: err.message
832
+ });
833
+ });
834
+ });
835
+
836
+ // Execute other afterInsert hooks synchronously (excluding partition hook)
837
+ const nonPartitionHooks = this.hooks.afterInsert.filter(hook =>
838
+ !hook.toString().includes('createPartitionReferences')
839
+ );
840
+ let finalResult = insertedObject;
841
+ for (const hook of nonPartitionHooks) {
842
+ finalResult = await hook(finalResult);
843
+ }
844
+
845
+ // Emit insert event
846
+ this.emit('insert', finalResult);
847
+ return finalResult;
848
+ } else {
849
+ // Sync mode: execute all hooks including partition creation
850
+ const finalResult = await this.executeHooks('afterInsert', insertedObject);
851
+
852
+ // Emit insert event
853
+ this.emit('insert', finalResult);
854
+
855
+ // Return the final object
856
+ return finalResult;
857
+ }
828
858
  }
829
859
 
830
860
  /**
@@ -1087,13 +1117,46 @@ export class Resource extends AsyncEventEmitter {
1087
1117
  body: finalBody,
1088
1118
  behavior: this.behavior
1089
1119
  });
1090
- const finalResult = await this.executeHooks('afterUpdate', updatedData);
1091
- this.emit('update', {
1092
- ...updatedData,
1093
- $before: { ...originalData },
1094
- $after: { ...finalResult }
1095
- });
1096
- return finalResult;
1120
+
1121
+ // Handle partition updates based on asyncPartitions config
1122
+ if (this.config.asyncPartitions && this.config.partitions && Object.keys(this.config.partitions).length > 0) {
1123
+ // Async mode: update partition indexes in background
1124
+ setImmediate(() => {
1125
+ this.handlePartitionReferenceUpdates(originalData, updatedData).catch(err => {
1126
+ this.emit('partitionIndexError', {
1127
+ operation: 'update',
1128
+ id,
1129
+ error: err,
1130
+ message: err.message
1131
+ });
1132
+ });
1133
+ });
1134
+
1135
+ // Execute other afterUpdate hooks synchronously (excluding partition hook)
1136
+ const nonPartitionHooks = this.hooks.afterUpdate.filter(hook =>
1137
+ !hook.toString().includes('handlePartitionReferenceUpdates')
1138
+ );
1139
+ let finalResult = updatedData;
1140
+ for (const hook of nonPartitionHooks) {
1141
+ finalResult = await hook(finalResult);
1142
+ }
1143
+
1144
+ this.emit('update', {
1145
+ ...updatedData,
1146
+ $before: { ...originalData },
1147
+ $after: { ...finalResult }
1148
+ });
1149
+ return finalResult;
1150
+ } else {
1151
+ // Sync mode: execute all hooks including partition updates
1152
+ const finalResult = await this.executeHooks('afterUpdate', updatedData);
1153
+ this.emit('update', {
1154
+ ...updatedData,
1155
+ $before: { ...originalData },
1156
+ $after: { ...finalResult }
1157
+ });
1158
+ return finalResult;
1159
+ }
1097
1160
  }
1098
1161
 
1099
1162
  /**
@@ -1149,8 +1212,34 @@ export class Resource extends AsyncEventEmitter {
1149
1212
  id
1150
1213
  });
1151
1214
 
1152
- const afterDeleteData = await this.executeHooks('afterDelete', objectData);
1153
- return response;
1215
+ // Handle partition cleanup based on asyncPartitions config
1216
+ if (this.config.asyncPartitions && this.config.partitions && Object.keys(this.config.partitions).length > 0) {
1217
+ // Async mode: delete partition indexes in background
1218
+ setImmediate(() => {
1219
+ this.deletePartitionReferences(objectData).catch(err => {
1220
+ this.emit('partitionIndexError', {
1221
+ operation: 'delete',
1222
+ id,
1223
+ error: err,
1224
+ message: err.message
1225
+ });
1226
+ });
1227
+ });
1228
+
1229
+ // Execute other afterDelete hooks synchronously (excluding partition hook)
1230
+ const nonPartitionHooks = this.hooks.afterDelete.filter(hook =>
1231
+ !hook.toString().includes('deletePartitionReferences')
1232
+ );
1233
+ let afterDeleteData = objectData;
1234
+ for (const hook of nonPartitionHooks) {
1235
+ afterDeleteData = await hook(afterDeleteData);
1236
+ }
1237
+ return response;
1238
+ } else {
1239
+ // Sync mode: execute all hooks including partition deletion
1240
+ const afterDeleteData = await this.executeHooks('afterDelete', objectData);
1241
+ return response;
1242
+ }
1154
1243
  }
1155
1244
 
1156
1245
  /**
@@ -1943,21 +2032,36 @@ export class Resource extends AsyncEventEmitter {
1943
2032
  return;
1944
2033
  }
1945
2034
 
1946
- // Create reference in each partition
1947
- for (const [partitionName, partition] of Object.entries(partitions)) {
2035
+ // Create all partition references in parallel
2036
+ const promises = Object.entries(partitions).map(async ([partitionName, partition]) => {
1948
2037
  const partitionKey = this.getPartitionKey({ partitionName, id: data.id, data });
1949
2038
  if (partitionKey) {
1950
2039
  // Save only version as metadata, never object attributes
1951
2040
  const partitionMetadata = {
1952
2041
  _v: String(this.version)
1953
2042
  };
1954
- await this.client.putObject({
2043
+ return this.client.putObject({
1955
2044
  key: partitionKey,
1956
2045
  metadata: partitionMetadata,
1957
2046
  body: '',
1958
2047
  contentType: undefined,
1959
2048
  });
1960
2049
  }
2050
+ return null;
2051
+ });
2052
+
2053
+ // Wait for all partition references to be created
2054
+ const results = await Promise.allSettled(promises);
2055
+
2056
+ // Check for any failures
2057
+ const failures = results.filter(r => r.status === 'rejected');
2058
+ if (failures.length > 0) {
2059
+ // Emit warning but don't throw - partitions are secondary indexes
2060
+ this.emit('partitionIndexWarning', {
2061
+ operation: 'create',
2062
+ id: data.id,
2063
+ failures: failures.map(f => f.reason)
2064
+ });
1961
2065
  }
1962
2066
  }
1963
2067
 
@@ -2077,33 +2181,41 @@ export class Resource extends AsyncEventEmitter {
2077
2181
  if (!partitions || Object.keys(partitions).length === 0) {
2078
2182
  return;
2079
2183
  }
2080
- for (const [partitionName, partition] of Object.entries(partitions)) {
2184
+
2185
+ // Update all partitions in parallel
2186
+ const updatePromises = Object.entries(partitions).map(async ([partitionName, partition]) => {
2081
2187
  const [ok, err] = await tryFn(() => this.handlePartitionReferenceUpdate(partitionName, partition, oldData, newData));
2082
2188
  if (!ok) {
2083
2189
  // console.warn(`Failed to update partition references for ${partitionName}:`, err.message);
2190
+ return { partitionName, error: err };
2084
2191
  }
2085
- }
2192
+ return { partitionName, success: true };
2193
+ });
2194
+
2195
+ await Promise.allSettled(updatePromises);
2196
+
2197
+ // Aggressive cleanup: remove stale partition keys in parallel
2086
2198
  const id = newData.id || oldData.id;
2087
- for (const [partitionName, partition] of Object.entries(partitions)) {
2199
+ const cleanupPromises = Object.entries(partitions).map(async ([partitionName, partition]) => {
2088
2200
  const prefix = `resource=${this.name}/partition=${partitionName}`;
2089
- let allKeys = [];
2090
2201
  const [okKeys, errKeys, keys] = await tryFn(() => this.client.getAllKeys({ prefix }));
2091
- if (okKeys) {
2092
- allKeys = keys;
2093
- } else {
2202
+ if (!okKeys) {
2094
2203
  // console.warn(`Aggressive cleanup: could not list keys for partition ${partitionName}:`, errKeys.message);
2095
- continue;
2204
+ return;
2096
2205
  }
2206
+
2097
2207
  const validKey = this.getPartitionKey({ partitionName, id, data: newData });
2098
- for (const key of allKeys) {
2099
- if (key.endsWith(`/id=${id}`) && key !== validKey) {
2100
- const [okDel, errDel] = await tryFn(() => this.client.deleteObject(key));
2101
- if (!okDel) {
2102
- // console.warn(`Aggressive cleanup: could not delete stale partition key ${key}:`, errDel.message);
2103
- }
2208
+ const staleKeys = keys.filter(key => key.endsWith(`/id=${id}`) && key !== validKey);
2209
+
2210
+ if (staleKeys.length > 0) {
2211
+ const [okDel, errDel] = await tryFn(() => this.client.deleteObjects(staleKeys));
2212
+ if (!okDel) {
2213
+ // console.warn(`Aggressive cleanup: could not delete stale partition keys:`, errDel.message);
2104
2214
  }
2105
2215
  }
2106
- }
2216
+ });
2217
+
2218
+ await Promise.allSettled(cleanupPromises);
2107
2219
  }
2108
2220
 
2109
2221
  /**