@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 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, writeJSON } from "../utils/utils.js";
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 writeJSON(this._sink, path, alias, "application/json");
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
  );
@@ -20,7 +20,8 @@ import {
20
20
  decodeUriComponent,
21
21
  streamCollector,
22
22
  writeJSON,
23
- readJSON,
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 obj = await readJSON(this._sink, path);
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(obj);
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 {any} versions
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
- await writeJSON(this._sink, path, versions, "application/json");
210
- this._log.info(
211
- `map:put - Successfully wrote version meta file to sink - Pathname: ${path}`,
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
- const e = new HttpError.BadGateway();
263
- end({ labels: { success: false, status: e.status, type: "map" } });
264
- throw e;
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(incoming, versions);
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();
@@ -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
- readJSON,
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(async ({ pkg, eikJson }) => {
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
- await writeJSON(this._sink, eikJsonPath, content, "application/json");
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 obj = await readJSON(this._sink, path);
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(obj);
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 {any} versions
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
- await writeJSON(this._sink, path, versions, "application/json");
197
- this._log.info(
198
- `pkg:put - Successfully wrote version meta file to sink - Pathname: ${path}`,
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
- await this._writeVersions(incoming, versions);
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._writeEikJson(incoming, eikJson);
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();
@@ -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.77",
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.1.34",
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<InstanceType<typeof Versions>>;
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 {any} versions
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: any): Promise<void>;
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<void>;
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<InstanceType<typeof Versions>>;
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 {any} versions
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: any): Promise<void>;
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";
@@ -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
  */