@eik/core 2.1.77 → 2.1.79
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/CHANGELOG.md +14 -0
- package/lib/handlers/alias.put.js +12 -2
- package/lib/handlers/map.put.js +61 -17
- package/lib/handlers/pkg.put.js +112 -27
- package/lib/utils/utils.js +77 -0
- package/package.json +2 -2
- package/types/handlers/map.put.d.ts +13 -4
- package/types/handlers/pkg.put.d.ts +19 -5
- package/types/utils/utils.d.ts +27 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [2.1.79](https://github.com/eik-lib/core/compare/v2.1.78...v2.1.79) (2026-08-19)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* use atomic sink writes to prevent concurrent upload races ([#638](https://github.com/eik-lib/core/issues/638)) ([cf29a68](https://github.com/eik-lib/core/commit/cf29a6838622360f8c80bc83c86d07536d580f33)), closes [#1](https://github.com/eik-lib/core/issues/1) [#2](https://github.com/eik-lib/core/issues/2) [#3](https://github.com/eik-lib/core/issues/3) [#6](https://github.com/eik-lib/core/issues/6)
|
|
7
|
+
|
|
8
|
+
## [2.1.78](https://github.com/eik-lib/core/compare/v2.1.77...v2.1.78) (2026-08-17)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* include server-written eik.json in the package asset list ([#637](https://github.com/eik-lib/core/issues/637)) ([42ea8c6](https://github.com/eik-lib/core/commit/42ea8c6d328f2e35c2ba8618f4c3e74d48454ed2))
|
|
14
|
+
|
|
1
15
|
## [2.1.77](https://github.com/eik-lib/core/compare/v2.1.76...v2.1.77) (2026-08-17)
|
|
2
16
|
|
|
3
17
|
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
createFilePathToAlias,
|
|
9
9
|
createFilePathToAliasOrigin,
|
|
10
10
|
} from "../utils/path-builders-fs.js";
|
|
11
|
-
import { decodeUriComponent,
|
|
11
|
+
import { decodeUriComponent, writeJSONWithOptions } from "../utils/utils.js";
|
|
12
12
|
import { createURIToAlias } from "../utils/path-builders-uri.js";
|
|
13
13
|
import MultipartParser from "../multipart/parser.js";
|
|
14
14
|
import HttpIncoming from "../classes/http-incoming.js";
|
|
@@ -109,8 +109,18 @@ const AliasPut = class AliasPut {
|
|
|
109
109
|
);
|
|
110
110
|
|
|
111
111
|
try {
|
|
112
|
-
await
|
|
112
|
+
await writeJSONWithOptions(
|
|
113
|
+
this._sink,
|
|
114
|
+
path,
|
|
115
|
+
alias,
|
|
116
|
+
"application/json",
|
|
117
|
+
{ ifNotExists: true },
|
|
118
|
+
);
|
|
113
119
|
} catch (error) {
|
|
120
|
+
const err = /** @type {any} */ (error);
|
|
121
|
+
if (err.code === "ALREADY_EXISTS") {
|
|
122
|
+
throw new HttpError.Conflict();
|
|
123
|
+
}
|
|
114
124
|
this._log.error(
|
|
115
125
|
`alias:put - Failed writing alias to sink - Pathname: ${path}`,
|
|
116
126
|
);
|
package/lib/handlers/map.put.js
CHANGED
|
@@ -20,7 +20,8 @@ import {
|
|
|
20
20
|
decodeUriComponent,
|
|
21
21
|
streamCollector,
|
|
22
22
|
writeJSON,
|
|
23
|
-
|
|
23
|
+
writeJSONWithOptions,
|
|
24
|
+
readJSONWithGeneration,
|
|
24
25
|
} from "../utils/utils.js";
|
|
25
26
|
|
|
26
27
|
/**
|
|
@@ -172,7 +173,7 @@ const MapPut = class MapPut {
|
|
|
172
173
|
|
|
173
174
|
/**
|
|
174
175
|
* @param {any} incoming
|
|
175
|
-
* @returns {Promise<InstanceType<typeof Versions
|
|
176
|
+
* @returns {Promise<{ versions: InstanceType<typeof Versions>, generation: string }>}
|
|
176
177
|
*/
|
|
177
178
|
async _readVersions(incoming) {
|
|
178
179
|
const path = createFilePathToVersion(incoming);
|
|
@@ -186,30 +187,65 @@ const MapPut = class MapPut {
|
|
|
186
187
|
this._log.info(
|
|
187
188
|
`map:put - Version meta file did not exist in sink - Create new - Pathname: ${path}`,
|
|
188
189
|
);
|
|
189
|
-
return new Versions(incoming);
|
|
190
|
+
return { versions: new Versions(incoming), generation: "" };
|
|
190
191
|
}
|
|
191
192
|
|
|
192
193
|
// File exists. Any failure here must propagate rather than be
|
|
193
194
|
// swallowed — silently falling back to an empty Versions object
|
|
194
195
|
// would cause _writeVersions to overwrite the index and erase the
|
|
195
196
|
// entire version history.
|
|
196
|
-
const
|
|
197
|
+
const { data, generation } = await readJSONWithGeneration(this._sink, path);
|
|
197
198
|
this._log.info(
|
|
198
199
|
`map:put - Successfully read version meta file from sink - Pathname: ${path}`,
|
|
199
200
|
);
|
|
200
|
-
return new Versions(
|
|
201
|
+
return { versions: new Versions(data), generation };
|
|
201
202
|
}
|
|
202
203
|
|
|
203
204
|
/**
|
|
205
|
+
* Writes versions.json with compare-and-swap semantics when a generation
|
|
206
|
+
* token is available. On conflict the read-modify-write cycle is retried.
|
|
207
|
+
*
|
|
204
208
|
* @param {any} incoming
|
|
205
|
-
* @param {
|
|
209
|
+
* @param {InstanceType<typeof Versions>} versions
|
|
210
|
+
* @param {string} generation
|
|
211
|
+
* @param {string} pVersion
|
|
212
|
+
* @param {string} integrity
|
|
206
213
|
*/
|
|
207
|
-
async _writeVersions(incoming, versions) {
|
|
214
|
+
async _writeVersions(incoming, versions, generation, pVersion, integrity) {
|
|
208
215
|
const path = createFilePathToVersion(incoming);
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
216
|
+
const maxRetries = 3;
|
|
217
|
+
/** @type {{ ifGenerationMatch?: string | number }} */
|
|
218
|
+
let options = generation ? { ifGenerationMatch: generation } : {};
|
|
219
|
+
let currentVersions = versions;
|
|
220
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
221
|
+
try {
|
|
222
|
+
await writeJSONWithOptions(
|
|
223
|
+
this._sink,
|
|
224
|
+
path,
|
|
225
|
+
currentVersions,
|
|
226
|
+
"application/json",
|
|
227
|
+
options,
|
|
228
|
+
);
|
|
229
|
+
this._log.info(
|
|
230
|
+
`map:put - Successfully wrote version meta file to sink - Pathname: ${path}`,
|
|
231
|
+
);
|
|
232
|
+
return;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
const err = /** @type {any} */ (error);
|
|
235
|
+
if (err.code === "CONFLICT" && attempt < maxRetries) {
|
|
236
|
+
this._log.info(
|
|
237
|
+
`map:put - versions.json conflict on attempt ${attempt + 1}, retrying`,
|
|
238
|
+
);
|
|
239
|
+
const { versions: fresh, generation: newGen } =
|
|
240
|
+
await this._readVersions(incoming);
|
|
241
|
+
fresh.setVersion(pVersion, integrity);
|
|
242
|
+
currentVersions = fresh;
|
|
243
|
+
options = newGen ? { ifGenerationMatch: newGen } : {};
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
213
249
|
}
|
|
214
250
|
|
|
215
251
|
/**
|
|
@@ -258,11 +294,13 @@ const MapPut = class MapPut {
|
|
|
258
294
|
org,
|
|
259
295
|
});
|
|
260
296
|
|
|
261
|
-
const versions = await this._readVersions(incoming).catch(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
297
|
+
const { versions, generation } = await this._readVersions(incoming).catch(
|
|
298
|
+
() => {
|
|
299
|
+
const e = new HttpError.BadGateway();
|
|
300
|
+
end({ labels: { success: false, status: e.status, type: "map" } });
|
|
301
|
+
throw e;
|
|
302
|
+
},
|
|
303
|
+
);
|
|
266
304
|
|
|
267
305
|
if (!versions.check(pVersion)) {
|
|
268
306
|
this._log.info(
|
|
@@ -277,7 +315,13 @@ const MapPut = class MapPut {
|
|
|
277
315
|
versions.setVersion(pVersion, integrity);
|
|
278
316
|
|
|
279
317
|
try {
|
|
280
|
-
await this._writeVersions(
|
|
318
|
+
await this._writeVersions(
|
|
319
|
+
incoming,
|
|
320
|
+
versions,
|
|
321
|
+
generation,
|
|
322
|
+
pVersion,
|
|
323
|
+
integrity,
|
|
324
|
+
);
|
|
281
325
|
// eslint-disable-next-line no-unused-vars
|
|
282
326
|
} catch (error) {
|
|
283
327
|
const e = new HttpError.BadGateway();
|
package/lib/handlers/pkg.put.js
CHANGED
|
@@ -3,6 +3,7 @@ import originalUrl from "original-url";
|
|
|
3
3
|
import HttpError from "http-errors";
|
|
4
4
|
import Metrics from "@metrics/client";
|
|
5
5
|
import abslog from "abslog";
|
|
6
|
+
import ssri from "ssri";
|
|
6
7
|
|
|
7
8
|
import {
|
|
8
9
|
createFilePathToPackage,
|
|
@@ -12,7 +13,8 @@ import {
|
|
|
12
13
|
import {
|
|
13
14
|
decodeUriComponent,
|
|
14
15
|
writeJSON,
|
|
15
|
-
|
|
16
|
+
writeJSONWithOptions,
|
|
17
|
+
readJSONWithGeneration,
|
|
16
18
|
readEikJson,
|
|
17
19
|
} from "../utils/utils.js";
|
|
18
20
|
import { createURIPathToPkgLog } from "../utils/path-builders-uri.js";
|
|
@@ -22,6 +24,7 @@ import HttpOutgoing from "../classes/http-outgoing.js";
|
|
|
22
24
|
import Versions from "../classes/versions.js";
|
|
23
25
|
import Package from "../classes/package.js";
|
|
24
26
|
import Author from "../classes/author.js";
|
|
27
|
+
import Asset from "../classes/asset.js";
|
|
25
28
|
import config from "../utils/defaults.js";
|
|
26
29
|
|
|
27
30
|
/**
|
|
@@ -97,14 +100,7 @@ const PkgPut = class PkgPut {
|
|
|
97
100
|
});
|
|
98
101
|
return { pkg, eikJson };
|
|
99
102
|
})
|
|
100
|
-
.then(
|
|
101
|
-
const path = createFilePathToPackage(pkg);
|
|
102
|
-
await writeJSON(this._sink, path, pkg, "application/json");
|
|
103
|
-
|
|
104
|
-
this._log.info(
|
|
105
|
-
`pkg:put - Successfully wrote package meta file to sink - Pathname: ${path}`,
|
|
106
|
-
);
|
|
107
|
-
|
|
103
|
+
.then(({ pkg, eikJson }) => {
|
|
108
104
|
resolve({ pkg, eikJson });
|
|
109
105
|
})
|
|
110
106
|
.catch((error) => {
|
|
@@ -119,8 +115,12 @@ const PkgPut = class PkgPut {
|
|
|
119
115
|
* server-authoritative values, ensuring the file is always server-written
|
|
120
116
|
* regardless of what the client's tar contained.
|
|
121
117
|
*
|
|
118
|
+
* Returns an Asset representing the written file so the caller can include
|
|
119
|
+
* it in the package's asset list.
|
|
120
|
+
*
|
|
122
121
|
* @param {any} incoming
|
|
123
122
|
* @param {Record<string,any>|null} capturedEikJson
|
|
123
|
+
* @returns {Promise<InstanceType<typeof Asset>>}
|
|
124
124
|
*/
|
|
125
125
|
async _writeEikJson(incoming, capturedEikJson) {
|
|
126
126
|
const content = {
|
|
@@ -130,15 +130,34 @@ const PkgPut = class PkgPut {
|
|
|
130
130
|
type: incoming.type,
|
|
131
131
|
};
|
|
132
132
|
const eikJsonPath = createFilePathToEikJson(incoming);
|
|
133
|
-
|
|
133
|
+
const buffer = await writeJSONWithOptions(
|
|
134
|
+
this._sink,
|
|
135
|
+
eikJsonPath,
|
|
136
|
+
content,
|
|
137
|
+
"application/json",
|
|
138
|
+
{ ifNotExists: true },
|
|
139
|
+
);
|
|
134
140
|
this._log.info(
|
|
135
141
|
`pkg:put - Successfully wrote eik.json to sink - Pathname: ${eikJsonPath}`,
|
|
136
142
|
);
|
|
143
|
+
|
|
144
|
+
const asset = new Asset({
|
|
145
|
+
pathname: "eik.json",
|
|
146
|
+
version: incoming.version,
|
|
147
|
+
name: incoming.name,
|
|
148
|
+
type: incoming.type,
|
|
149
|
+
org: incoming.org,
|
|
150
|
+
});
|
|
151
|
+
asset.integrity = ssri
|
|
152
|
+
.fromData(buffer, { algorithms: ["sha512"] })
|
|
153
|
+
.toString();
|
|
154
|
+
asset.size = buffer.length;
|
|
155
|
+
return asset;
|
|
137
156
|
}
|
|
138
157
|
|
|
139
158
|
/**
|
|
140
159
|
* @param {any} incoming
|
|
141
|
-
* @returns {Promise<InstanceType<typeof Versions
|
|
160
|
+
* @returns {Promise<{ versions: InstanceType<typeof Versions>, generation: string }>}
|
|
142
161
|
*/
|
|
143
162
|
async _readVersions(incoming) {
|
|
144
163
|
const path = createFilePathToVersion(incoming);
|
|
@@ -152,18 +171,18 @@ const PkgPut = class PkgPut {
|
|
|
152
171
|
this._log.info(
|
|
153
172
|
`pkg:put - Version meta file did not exist in sink - Create new - Pathname: ${path}`,
|
|
154
173
|
);
|
|
155
|
-
return new Versions(incoming);
|
|
174
|
+
return { versions: new Versions(incoming), generation: "" };
|
|
156
175
|
}
|
|
157
176
|
|
|
158
177
|
// File exists. Any failure here (GCS error, corrupt JSON, network
|
|
159
178
|
// timeout) must propagate rather than be swallowed — silently falling
|
|
160
179
|
// back to an empty Versions object would cause _writeVersions to
|
|
161
180
|
// overwrite the index and erase the entire version history.
|
|
162
|
-
const
|
|
181
|
+
const { data, generation } = await readJSONWithGeneration(this._sink, path);
|
|
163
182
|
this._log.info(
|
|
164
183
|
`pkg:put - Successfully read version meta file from sink - Pathname: ${path}`,
|
|
165
184
|
);
|
|
166
|
-
return new Versions(
|
|
185
|
+
return { versions: new Versions(data), generation };
|
|
167
186
|
}
|
|
168
187
|
|
|
169
188
|
/**
|
|
@@ -188,15 +207,50 @@ const PkgPut = class PkgPut {
|
|
|
188
207
|
}
|
|
189
208
|
|
|
190
209
|
/**
|
|
210
|
+
* Writes versions.json with compare-and-swap semantics when a generation
|
|
211
|
+
* token is available. On conflict the read-modify-write cycle is retried.
|
|
212
|
+
*
|
|
191
213
|
* @param {any} incoming
|
|
192
|
-
* @param {
|
|
214
|
+
* @param {InstanceType<typeof Versions>} versions
|
|
215
|
+
* @param {string} generation
|
|
216
|
+
* @param {string} pVersion
|
|
217
|
+
* @param {string} integrity
|
|
193
218
|
*/
|
|
194
|
-
async _writeVersions(incoming, versions) {
|
|
219
|
+
async _writeVersions(incoming, versions, generation, pVersion, integrity) {
|
|
195
220
|
const path = createFilePathToVersion(incoming);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
221
|
+
const maxRetries = 3;
|
|
222
|
+
/** @type {{ ifGenerationMatch?: string | number }} */
|
|
223
|
+
let options = generation ? { ifGenerationMatch: generation } : {};
|
|
224
|
+
let currentVersions = versions;
|
|
225
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
226
|
+
try {
|
|
227
|
+
await writeJSONWithOptions(
|
|
228
|
+
this._sink,
|
|
229
|
+
path,
|
|
230
|
+
currentVersions,
|
|
231
|
+
"application/json",
|
|
232
|
+
options,
|
|
233
|
+
);
|
|
234
|
+
this._log.info(
|
|
235
|
+
`pkg:put - Successfully wrote version meta file to sink - Pathname: ${path}`,
|
|
236
|
+
);
|
|
237
|
+
return;
|
|
238
|
+
} catch (error) {
|
|
239
|
+
const err = /** @type {any} */ (error);
|
|
240
|
+
if (err.code === "CONFLICT" && attempt < maxRetries) {
|
|
241
|
+
this._log.info(
|
|
242
|
+
`pkg:put - versions.json conflict on attempt ${attempt + 1}, retrying`,
|
|
243
|
+
);
|
|
244
|
+
const { versions: fresh, generation: newGen } =
|
|
245
|
+
await this._readVersions(incoming);
|
|
246
|
+
fresh.setVersion(pVersion, integrity);
|
|
247
|
+
currentVersions = fresh;
|
|
248
|
+
options = newGen ? { ifGenerationMatch: newGen } : {};
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
200
254
|
}
|
|
201
255
|
|
|
202
256
|
/**
|
|
@@ -258,17 +312,42 @@ const PkgPut = class PkgPut {
|
|
|
258
312
|
throw e;
|
|
259
313
|
}
|
|
260
314
|
|
|
261
|
-
const versions = await this._readVersions(incoming).catch(
|
|
315
|
+
const { versions, generation } = await this._readVersions(incoming).catch(
|
|
316
|
+
() => {
|
|
317
|
+
const e = new HttpError.BadGateway();
|
|
318
|
+
end({ labels: { success: false, status: e.status, type } });
|
|
319
|
+
throw e;
|
|
320
|
+
},
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
const { pkg, eikJson } = await this._parser(incoming);
|
|
324
|
+
|
|
325
|
+
// Write eik.json before package metadata so the asset list is complete
|
|
326
|
+
// when {version}.package.json is written to the sink.
|
|
327
|
+
let eikJsonAsset;
|
|
328
|
+
try {
|
|
329
|
+
eikJsonAsset = await this._writeEikJson(incoming, eikJson);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
const err = /** @type {any} */ (error);
|
|
332
|
+
if (err.code === "ALREADY_EXISTS") {
|
|
333
|
+
// Another replica committed this version while this upload was in
|
|
334
|
+
// flight. Treat it as a conflict identical to the early existence check.
|
|
335
|
+
const e = new HttpError.Conflict();
|
|
336
|
+
end({ labels: { success: false, status: e.status, type } });
|
|
337
|
+
throw e;
|
|
338
|
+
}
|
|
262
339
|
const e = new HttpError.BadGateway();
|
|
263
340
|
end({ labels: { success: false, status: e.status, type } });
|
|
264
341
|
throw e;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const { pkg, eikJson } = await this._parser(incoming);
|
|
268
|
-
versions.setVersion(pVersion, pkg.integrity);
|
|
342
|
+
}
|
|
343
|
+
pkg.setAsset(eikJsonAsset);
|
|
269
344
|
|
|
270
345
|
try {
|
|
271
|
-
|
|
346
|
+
const pkgMetaPath = createFilePathToPackage(pkg);
|
|
347
|
+
await writeJSON(this._sink, pkgMetaPath, pkg, "application/json");
|
|
348
|
+
this._log.info(
|
|
349
|
+
`pkg:put - Successfully wrote package meta file to sink - Pathname: ${pkgMetaPath}`,
|
|
350
|
+
);
|
|
272
351
|
// eslint-disable-next-line no-unused-vars
|
|
273
352
|
} catch (error) {
|
|
274
353
|
const e = new HttpError.BadGateway();
|
|
@@ -277,7 +356,13 @@ const PkgPut = class PkgPut {
|
|
|
277
356
|
}
|
|
278
357
|
|
|
279
358
|
try {
|
|
280
|
-
await this.
|
|
359
|
+
await this._writeVersions(
|
|
360
|
+
incoming,
|
|
361
|
+
versions,
|
|
362
|
+
generation,
|
|
363
|
+
pVersion,
|
|
364
|
+
pkg.integrity,
|
|
365
|
+
);
|
|
281
366
|
// eslint-disable-next-line no-unused-vars
|
|
282
367
|
} catch (error) {
|
|
283
368
|
const e = new HttpError.BadGateway();
|
package/lib/utils/utils.js
CHANGED
|
@@ -107,9 +107,86 @@ const decodeUriComponent = (value) => {
|
|
|
107
107
|
return decodeURIComponent(value);
|
|
108
108
|
};
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Like readJSON but also returns the generation token from the ReadFile,
|
|
112
|
+
* which can be passed back to writeJSONWithOptions as ifGenerationMatch
|
|
113
|
+
* for compare-and-swap semantics.
|
|
114
|
+
*
|
|
115
|
+
* @param {any} sink
|
|
116
|
+
* @param {string} path
|
|
117
|
+
* @returns {Promise<{ data: any, generation: string }>}
|
|
118
|
+
*/
|
|
119
|
+
const readJSONWithGeneration = (sink, path) =>
|
|
120
|
+
// eslint-disable-next-line no-async-promise-executor
|
|
121
|
+
new Promise(async (resolve, reject) => {
|
|
122
|
+
try {
|
|
123
|
+
/** @type {any[]} */
|
|
124
|
+
const buffer = [];
|
|
125
|
+
const from = await sink.read(path);
|
|
126
|
+
|
|
127
|
+
const to = new Writable({
|
|
128
|
+
objectMode: false,
|
|
129
|
+
write(chunk, encoding, callback) {
|
|
130
|
+
buffer.push(chunk);
|
|
131
|
+
callback();
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
pipeline(from.stream, to, (error) => {
|
|
136
|
+
if (error) return reject(error);
|
|
137
|
+
const str = Buffer.concat(buffer).toString("utf8");
|
|
138
|
+
try {
|
|
139
|
+
const obj = JSON.parse(str);
|
|
140
|
+
return resolve({ data: obj, generation: from.generation ?? "" });
|
|
141
|
+
} catch (err) {
|
|
142
|
+
return reject(err);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
} catch (error) {
|
|
146
|
+
reject(error);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Like writeJSON but accepts sink write options (e.g. { ifNotExists, ifGenerationMatch })
|
|
152
|
+
* for atomic conditional writes.
|
|
153
|
+
*
|
|
154
|
+
* @param {any} sink
|
|
155
|
+
* @param {string} path
|
|
156
|
+
* @param {any} obj
|
|
157
|
+
* @param {string} contentType
|
|
158
|
+
* @param {{ ifNotExists?: boolean, ifGenerationMatch?: string | number }} [options]
|
|
159
|
+
*/
|
|
160
|
+
const writeJSONWithOptions = (sink, path, obj, contentType, options = {}) =>
|
|
161
|
+
// eslint-disable-next-line no-async-promise-executor
|
|
162
|
+
new Promise(async (resolve, reject) => {
|
|
163
|
+
try {
|
|
164
|
+
const buffer = Buffer.from(JSON.stringify(obj));
|
|
165
|
+
|
|
166
|
+
const from = new Readable({
|
|
167
|
+
objectMode: false,
|
|
168
|
+
read() {
|
|
169
|
+
this.push(buffer);
|
|
170
|
+
this.push(null);
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const to = await sink.write(path, contentType, options);
|
|
175
|
+
|
|
176
|
+
pipeline(from, to, (error) => {
|
|
177
|
+
if (error) return reject(error);
|
|
178
|
+
return resolve(buffer);
|
|
179
|
+
});
|
|
180
|
+
} catch (error) {
|
|
181
|
+
reject(error);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
110
185
|
export {
|
|
111
186
|
readJSON,
|
|
187
|
+
readJSONWithGeneration,
|
|
112
188
|
writeJSON,
|
|
189
|
+
writeJSONWithOptions,
|
|
113
190
|
streamCollector,
|
|
114
191
|
etagFromFsStat,
|
|
115
192
|
decodeUriComponent,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eik/core",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.79",
|
|
4
4
|
"description": "Core server package",
|
|
5
5
|
"main": "lib/main.js",
|
|
6
6
|
"types": "./types/main.d.ts",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"author": "",
|
|
30
30
|
"license": "MIT",
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@eik/common": "5.
|
|
32
|
+
"@eik/common": "5.2.0",
|
|
33
33
|
"@eik/sink": "1.2.7",
|
|
34
34
|
"@eik/sink-file-system": "2.0.40",
|
|
35
35
|
"@eik/sink-memory": "2.0.37",
|
|
@@ -42,14 +42,23 @@ declare const MapPut: {
|
|
|
42
42
|
}): Promise<string>;
|
|
43
43
|
/**
|
|
44
44
|
* @param {any} incoming
|
|
45
|
-
* @returns {Promise<InstanceType<typeof Versions
|
|
45
|
+
* @returns {Promise<{ versions: InstanceType<typeof Versions>, generation: string }>}
|
|
46
46
|
*/
|
|
47
|
-
_readVersions(incoming: any): Promise<
|
|
47
|
+
_readVersions(incoming: any): Promise<{
|
|
48
|
+
versions: InstanceType<typeof Versions>;
|
|
49
|
+
generation: string;
|
|
50
|
+
}>;
|
|
48
51
|
/**
|
|
52
|
+
* Writes versions.json with compare-and-swap semantics when a generation
|
|
53
|
+
* token is available. On conflict the read-modify-write cycle is retried.
|
|
54
|
+
*
|
|
49
55
|
* @param {any} incoming
|
|
50
|
-
* @param {
|
|
56
|
+
* @param {InstanceType<typeof Versions>} versions
|
|
57
|
+
* @param {string} generation
|
|
58
|
+
* @param {string} pVersion
|
|
59
|
+
* @param {string} integrity
|
|
51
60
|
*/
|
|
52
|
-
_writeVersions(incoming: any, versions:
|
|
61
|
+
_writeVersions(incoming: any, versions: InstanceType<typeof Versions>, generation: string, pVersion: string, integrity: string): Promise<void>;
|
|
53
62
|
/**
|
|
54
63
|
* @param {any} req
|
|
55
64
|
* @param {any} user
|
|
@@ -72,24 +72,37 @@ declare const PkgPut: {
|
|
|
72
72
|
* server-authoritative values, ensuring the file is always server-written
|
|
73
73
|
* regardless of what the client's tar contained.
|
|
74
74
|
*
|
|
75
|
+
* Returns an Asset representing the written file so the caller can include
|
|
76
|
+
* it in the package's asset list.
|
|
77
|
+
*
|
|
75
78
|
* @param {any} incoming
|
|
76
79
|
* @param {Record<string,any>|null} capturedEikJson
|
|
80
|
+
* @returns {Promise<InstanceType<typeof Asset>>}
|
|
77
81
|
*/
|
|
78
|
-
_writeEikJson(incoming: any, capturedEikJson: Record<string, any> | null): Promise<
|
|
82
|
+
_writeEikJson(incoming: any, capturedEikJson: Record<string, any> | null): Promise<InstanceType<typeof Asset>>;
|
|
79
83
|
/**
|
|
80
84
|
* @param {any} incoming
|
|
81
|
-
* @returns {Promise<InstanceType<typeof Versions
|
|
85
|
+
* @returns {Promise<{ versions: InstanceType<typeof Versions>, generation: string }>}
|
|
82
86
|
*/
|
|
83
|
-
_readVersions(incoming: any): Promise<
|
|
87
|
+
_readVersions(incoming: any): Promise<{
|
|
88
|
+
versions: InstanceType<typeof Versions>;
|
|
89
|
+
generation: string;
|
|
90
|
+
}>;
|
|
84
91
|
/**
|
|
85
92
|
* @param {any} incoming
|
|
86
93
|
*/
|
|
87
94
|
_readVersion(incoming: any): Promise<boolean>;
|
|
88
95
|
/**
|
|
96
|
+
* Writes versions.json with compare-and-swap semantics when a generation
|
|
97
|
+
* token is available. On conflict the read-modify-write cycle is retried.
|
|
98
|
+
*
|
|
89
99
|
* @param {any} incoming
|
|
90
|
-
* @param {
|
|
100
|
+
* @param {InstanceType<typeof Versions>} versions
|
|
101
|
+
* @param {string} generation
|
|
102
|
+
* @param {string} pVersion
|
|
103
|
+
* @param {string} integrity
|
|
91
104
|
*/
|
|
92
|
-
_writeVersions(incoming: any, versions:
|
|
105
|
+
_writeVersions(incoming: any, versions: InstanceType<typeof Versions>, generation: string, pVersion: string, integrity: string): Promise<void>;
|
|
93
106
|
/**
|
|
94
107
|
* @param {any} req
|
|
95
108
|
* @param {any} user
|
|
@@ -125,4 +138,5 @@ declare const PkgPut: {
|
|
|
125
138
|
};
|
|
126
139
|
import abslog from "abslog";
|
|
127
140
|
import Metrics from "@metrics/client";
|
|
141
|
+
import Asset from "../classes/asset.js";
|
|
128
142
|
import Versions from "../classes/versions.js";
|
package/types/utils/utils.d.ts
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
* @param {string} path
|
|
4
4
|
*/
|
|
5
5
|
export function readJSON(sink: any, path: string): Promise<any>;
|
|
6
|
+
/**
|
|
7
|
+
* Like readJSON but also returns the generation token from the ReadFile,
|
|
8
|
+
* which can be passed back to writeJSONWithOptions as ifGenerationMatch
|
|
9
|
+
* for compare-and-swap semantics.
|
|
10
|
+
*
|
|
11
|
+
* @param {any} sink
|
|
12
|
+
* @param {string} path
|
|
13
|
+
* @returns {Promise<{ data: any, generation: string }>}
|
|
14
|
+
*/
|
|
15
|
+
export function readJSONWithGeneration(sink: any, path: string): Promise<{
|
|
16
|
+
data: any;
|
|
17
|
+
generation: string;
|
|
18
|
+
}>;
|
|
6
19
|
/**
|
|
7
20
|
* @param {any} sink
|
|
8
21
|
* @param {string} path
|
|
@@ -10,6 +23,20 @@ export function readJSON(sink: any, path: string): Promise<any>;
|
|
|
10
23
|
* @param {string} contentType
|
|
11
24
|
*/
|
|
12
25
|
export function writeJSON(sink: any, path: string, obj: any, contentType: string): Promise<any>;
|
|
26
|
+
/**
|
|
27
|
+
* Like writeJSON but accepts sink write options (e.g. { ifNotExists, ifGenerationMatch })
|
|
28
|
+
* for atomic conditional writes.
|
|
29
|
+
*
|
|
30
|
+
* @param {any} sink
|
|
31
|
+
* @param {string} path
|
|
32
|
+
* @param {any} obj
|
|
33
|
+
* @param {string} contentType
|
|
34
|
+
* @param {{ ifNotExists?: boolean, ifGenerationMatch?: string | number }} [options]
|
|
35
|
+
*/
|
|
36
|
+
export function writeJSONWithOptions(sink: any, path: string, obj: any, contentType: string, options?: {
|
|
37
|
+
ifNotExists?: boolean;
|
|
38
|
+
ifGenerationMatch?: string | number;
|
|
39
|
+
}): Promise<any>;
|
|
13
40
|
/**
|
|
14
41
|
* @param {any} from
|
|
15
42
|
*/
|