s3db.js 9.2.0 → 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.
@@ -1,6 +1,5 @@
1
1
  import Plugin from "./plugin.class.js";
2
2
  import tryFn from "../concerns/try-fn.js";
3
- import { EventEmitter } from 'events';
4
3
 
5
4
  /**
6
5
  * SchedulerPlugin - Cron-based Task Scheduling System
@@ -1,6 +1,6 @@
1
1
  import { join } from "path";
2
- import EventEmitter from "events";
3
2
  import { createHash } from "crypto";
3
+ import AsyncEventEmitter from "./concerns/async-event-emitter.js";
4
4
  import { customAlphabet, urlAlphabet } from 'nanoid';
5
5
  import jsonStableStringify from "json-stable-stringify";
6
6
  import { PromisePool } from "@supercharge/promise-pool";
@@ -16,7 +16,7 @@ import { calculateTotalSize, calculateEffectiveLimit } from "./concerns/calculat
16
16
  import { mapAwsError, InvalidResourceItem, ResourceError, PartitionError } from "./errors.js";
17
17
 
18
18
 
19
- export class Resource extends EventEmitter {
19
+ export class Resource extends AsyncEventEmitter {
20
20
  /**
21
21
  * Create a new Resource instance
22
22
  * @param {Object} config - Resource configuration
@@ -40,6 +40,7 @@ export class Resource extends EventEmitter {
40
40
  * @param {number} [config.idSize=22] - Size for auto-generated IDs
41
41
  * @param {boolean} [config.versioningEnabled=false] - Enable versioning for this resource
42
42
  * @param {Object} [config.events={}] - Event listeners to automatically add
43
+ * @param {boolean} [config.asyncEvents=true] - Whether events should be emitted asynchronously
43
44
  * @example
44
45
  * const users = new Resource({
45
46
  * name: 'users',
@@ -133,7 +134,9 @@ export class Resource extends EventEmitter {
133
134
  idGenerator: customIdGenerator,
134
135
  idSize = 22,
135
136
  versioningEnabled = false,
136
- events = {}
137
+ events = {},
138
+ asyncEvents = true,
139
+ asyncPartitions = true
137
140
  } = config;
138
141
 
139
142
  // Set instance properties
@@ -145,6 +148,9 @@ export class Resource extends EventEmitter {
145
148
  this.parallelism = parallelism;
146
149
  this.passphrase = passphrase ?? 'secret';
147
150
  this.versioningEnabled = versioningEnabled;
151
+
152
+ // Configure async events mode
153
+ this.setAsyncMode(asyncEvents);
148
154
 
149
155
  // Configure ID generator
150
156
  this.idGenerator = this.configureIdGenerator(customIdGenerator, idSize);
@@ -171,6 +177,8 @@ export class Resource extends EventEmitter {
171
177
  partitions,
172
178
  autoDecrypt,
173
179
  allNestedObjectsOptional,
180
+ asyncEvents,
181
+ asyncPartitions,
174
182
  };
175
183
 
176
184
  // Initialize hooks system
@@ -811,14 +819,42 @@ export class Resource extends EventEmitter {
811
819
  // Get the inserted object
812
820
  const insertedObject = await this.get(finalId);
813
821
 
814
- // Execute afterInsert hooks
815
- const finalResult = await this.executeHooks('afterInsert', insertedObject);
816
-
817
- // Emit insert event
818
- this.emit('insert', finalResult);
819
-
820
- // Return the final object
821
- 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
+ }
822
858
  }
823
859
 
824
860
  /**
@@ -1081,13 +1117,46 @@ export class Resource extends EventEmitter {
1081
1117
  body: finalBody,
1082
1118
  behavior: this.behavior
1083
1119
  });
1084
- const finalResult = await this.executeHooks('afterUpdate', updatedData);
1085
- this.emit('update', {
1086
- ...updatedData,
1087
- $before: { ...originalData },
1088
- $after: { ...finalResult }
1089
- });
1090
- 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
+ }
1091
1160
  }
1092
1161
 
1093
1162
  /**
@@ -1143,8 +1212,34 @@ export class Resource extends EventEmitter {
1143
1212
  id
1144
1213
  });
1145
1214
 
1146
- const afterDeleteData = await this.executeHooks('afterDelete', objectData);
1147
- 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
+ }
1148
1243
  }
1149
1244
 
1150
1245
  /**
@@ -1937,21 +2032,36 @@ export class Resource extends EventEmitter {
1937
2032
  return;
1938
2033
  }
1939
2034
 
1940
- // Create reference in each partition
1941
- 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]) => {
1942
2037
  const partitionKey = this.getPartitionKey({ partitionName, id: data.id, data });
1943
2038
  if (partitionKey) {
1944
2039
  // Save only version as metadata, never object attributes
1945
2040
  const partitionMetadata = {
1946
2041
  _v: String(this.version)
1947
2042
  };
1948
- await this.client.putObject({
2043
+ return this.client.putObject({
1949
2044
  key: partitionKey,
1950
2045
  metadata: partitionMetadata,
1951
2046
  body: '',
1952
2047
  contentType: undefined,
1953
2048
  });
1954
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
+ });
1955
2065
  }
1956
2066
  }
1957
2067
 
@@ -2071,33 +2181,41 @@ export class Resource extends EventEmitter {
2071
2181
  if (!partitions || Object.keys(partitions).length === 0) {
2072
2182
  return;
2073
2183
  }
2074
- 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]) => {
2075
2187
  const [ok, err] = await tryFn(() => this.handlePartitionReferenceUpdate(partitionName, partition, oldData, newData));
2076
2188
  if (!ok) {
2077
2189
  // console.warn(`Failed to update partition references for ${partitionName}:`, err.message);
2190
+ return { partitionName, error: err };
2078
2191
  }
2079
- }
2192
+ return { partitionName, success: true };
2193
+ });
2194
+
2195
+ await Promise.allSettled(updatePromises);
2196
+
2197
+ // Aggressive cleanup: remove stale partition keys in parallel
2080
2198
  const id = newData.id || oldData.id;
2081
- for (const [partitionName, partition] of Object.entries(partitions)) {
2199
+ const cleanupPromises = Object.entries(partitions).map(async ([partitionName, partition]) => {
2082
2200
  const prefix = `resource=${this.name}/partition=${partitionName}`;
2083
- let allKeys = [];
2084
2201
  const [okKeys, errKeys, keys] = await tryFn(() => this.client.getAllKeys({ prefix }));
2085
- if (okKeys) {
2086
- allKeys = keys;
2087
- } else {
2202
+ if (!okKeys) {
2088
2203
  // console.warn(`Aggressive cleanup: could not list keys for partition ${partitionName}:`, errKeys.message);
2089
- continue;
2204
+ return;
2090
2205
  }
2206
+
2091
2207
  const validKey = this.getPartitionKey({ partitionName, id, data: newData });
2092
- for (const key of allKeys) {
2093
- if (key.endsWith(`/id=${id}`) && key !== validKey) {
2094
- const [okDel, errDel] = await tryFn(() => this.client.deleteObject(key));
2095
- if (!okDel) {
2096
- // console.warn(`Aggressive cleanup: could not delete stale partition key ${key}:`, errDel.message);
2097
- }
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);
2098
2214
  }
2099
2215
  }
2100
- }
2216
+ });
2217
+
2218
+ await Promise.allSettled(cleanupPromises);
2101
2219
  }
2102
2220
 
2103
2221
  /**
@@ -2452,9 +2570,6 @@ export class Resource extends EventEmitter {
2452
2570
  return filtered;
2453
2571
  }
2454
2572
 
2455
- emit(event, ...args) {
2456
- return super.emit(event, ...args);
2457
- }
2458
2573
 
2459
2574
  async replace(id, attributes) {
2460
2575
  await this.delete(id);