@procedures/collections 0.28.0 → 0.29.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.
@@ -69,7 +69,8 @@ export function boundedList<T>(
69
69
  const { capacity, policy = "throw", onOverflow } = options;
70
70
 
71
71
  return (next: List<T>): List<T> & BoundedCollection => {
72
- const handleOverflow = (element: T): void => {
72
+ // An unshift puts the element at the front, so drop-oldest drops the last element for it
73
+ const handleOverflow = (element: T, dropFrom: "first" | "last" = "first"): void => {
73
74
  // Call overflow handler if provided
74
75
  if (onOverflow) {
75
76
  const context: OverflowContext<T> = {
@@ -87,9 +88,10 @@ export function boundedList<T>(
87
88
  throw new Error(`List is full (capacity: ${capacity})`);
88
89
 
89
90
  case "drop-oldest":
90
- // Remove first element
91
+ // Remove one element: the first, or the last for an unshift
91
92
  if (!next.isEmpty) {
92
- next.shift();
93
+ if (dropFrom === "last") next.pop();
94
+ else next.shift();
93
95
  }
94
96
  break;
95
97
 
@@ -113,6 +115,9 @@ export function boundedList<T>(
113
115
  }
114
116
  };
115
117
 
118
+ // After drop-oldest removed the first element, an index of the caller is one place lower
119
+ const shifted = (index: number): number => (policy === "drop-oldest" ? Math.max(0, index - 1) : index);
120
+
116
121
  return new Proxy(next, {
117
122
  get(target, prop, receiver) {
118
123
  // Capacity property
@@ -160,27 +165,26 @@ export function boundedList<T>(
160
165
  case "unshift":
161
166
  return function (element: T): void {
162
167
  if (target.size >= capacity && policy !== "grow") {
163
- handleOverflow(element);
168
+ // One element goes: the last one (before, the first and the last went: audit pass 2)
169
+ handleOverflow(element, "last");
164
170
  if (policy === "reject" || policy === "drop-newest") {
165
171
  return;
166
172
  }
167
- // For unshift with drop-oldest, we should drop from the end
168
- if (policy === "drop-oldest" && !target.isEmpty) {
169
- target.pop();
170
- }
171
173
  }
172
174
  (value as Function).call(target, element);
173
175
  };
174
176
 
175
177
  case "insert":
176
178
  return function (index: number, element: T): void {
179
+ let at = index;
177
180
  if (target.size >= capacity && policy !== "grow") {
178
181
  handleOverflow(element);
179
182
  if (policy === "reject" || policy === "drop-newest") {
180
183
  return;
181
184
  }
185
+ at = shifted(at);
182
186
  }
183
- (value as Function).call(target, index, element);
187
+ (value as Function).call(target, at, element);
184
188
  };
185
189
 
186
190
  case "addAll":
@@ -191,19 +195,23 @@ export function boundedList<T>(
191
195
 
192
196
  // Check if adding all elements would exceed capacity
193
197
  if (target.size + elementsArray.length > capacity && policy !== "grow") {
194
- // Add elements one by one with overflow handling
198
+ // Add elements one by one with overflow handling. An insert keeps the order of
199
+ // the elements, at an index that follows each drop.
195
200
  let added = false;
201
+ let at = args[0] as number;
196
202
  for (const element of elementsArray) {
197
203
  if (target.size >= capacity) {
198
204
  handleOverflow(element);
199
205
  if (policy === "reject" || policy === "drop-newest") {
200
206
  break;
201
207
  }
208
+ at = shifted(at);
202
209
  }
203
210
  if (prop === "addAll") {
204
211
  target.add(element);
205
212
  } else {
206
- target.insert(args[0] as number, element);
213
+ target.insert(at, element);
214
+ at++;
207
215
  }
208
216
  added = true;
209
217
  }
@@ -107,9 +107,13 @@ function createReadonly<C extends object>(
107
107
  errorMessage: string = "Cannot modify readonly collection"
108
108
  ): Middleware<C> {
109
109
  return (next: C): C => {
110
+ // The read methods, bound to the collection: one function for each name
111
+ const bound = new Map<PropertyKey, { value: Function; method: Function }>();
110
112
  return new Proxy(next, {
111
- get(target, prop, receiver) {
112
- const value = Reflect.get(target, prop, receiver);
113
+ get(target, prop) {
114
+ // A read runs on the collection itself, not on the proxy: a read that changes internal
115
+ // state (a LinkedHashMap with accessOrder moves the entry) does not hit the set trap
116
+ const value = Reflect.get(target, prop, target);
113
117
 
114
118
  // Block mutating methods
115
119
  if (typeof prop === "string" && MUTATING_METHODS.has(prop)) {
@@ -131,6 +135,14 @@ function createReadonly<C extends object>(
131
135
  };
132
136
  }
133
137
 
138
+ if (typeof value === "function") {
139
+ const known = bound.get(prop);
140
+ if (known !== undefined && known.value === value) return known.method;
141
+ const method = (value as Function).bind(target) as Function;
142
+ bound.set(prop, { value, method });
143
+ return method;
144
+ }
145
+
134
146
  return value;
135
147
  },
136
148