@onlyworlds/sdk 4.26.0 → 4.27.1
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 +19 -0
- package/dist/index.d.ts +13 -3
- package/dist/index.js +74 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ earlier history lives in git log only.
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [4.27.1] — 2026-10-16
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **A retried `create` with the same `idempotencyKey` now replays instead of failing.** `create` minted a
|
|
12
|
+
fresh id on every call, so a retry of an id-less element sent a different body and keel answered 409
|
|
13
|
+
`idempotency_error`. With a key and no id, the id is now derived from the key (a UUIDv5, the same id the
|
|
14
|
+
Python package mints), so the retry sends the same body and gets the first answer back. Without a key,
|
|
15
|
+
a fresh UUIDv7 as before; an id you pass is kept.
|
|
16
|
+
- `create`'s doc said a keyed retry "rewrites the same element"; keel's replay writes nothing. Found by
|
|
17
|
+
Quillon's parity audit of the Unity SDK (h37 #5764).
|
|
18
|
+
|
|
19
|
+
## [4.27.0] — 2026-10-16
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- `OwApiError.isIdempotencyInFlight` (keel #307, D105): a request with the same Idempotency-Key is still
|
|
23
|
+
running (409 `idempotency_in_flight`, `retryAfter` 1). Nothing was written by this one; send again with
|
|
24
|
+
the same key and body after `retryAfter` and it replays the first answer. `isIdempotencyConflict` stays
|
|
25
|
+
the different-body case only. The client still does not retry for you.
|
|
26
|
+
|
|
8
27
|
## [4.26.0] — 2026-10-15
|
|
9
28
|
|
|
10
29
|
### Added
|
package/dist/index.d.ts
CHANGED
|
@@ -2394,6 +2394,13 @@ declare class OwApiError extends Error {
|
|
|
2394
2394
|
* conflict: keel also answers 409 `id_conflict` (see isIdConflict).
|
|
2395
2395
|
*/
|
|
2396
2396
|
get isIdempotencyConflict(): boolean;
|
|
2397
|
+
/**
|
|
2398
|
+
* A request with this Idempotency-Key is still running (409 `idempotency_in_flight`, keel
|
|
2399
|
+
* #307, D105): two sends of one create or bulk overlapped. Nothing was written by this one.
|
|
2400
|
+
* Wait `retryAfter` seconds (keel sends 1) and send again with the SAME key and body: once
|
|
2401
|
+
* the first has answered, that retry replays its answer. Not a key conflict (see above).
|
|
2402
|
+
*/
|
|
2403
|
+
get isIdempotencyInFlight(): boolean;
|
|
2397
2404
|
/**
|
|
2398
2405
|
* The id is already taken (409 `id_conflict`): a create with an id that exists
|
|
2399
2406
|
* (keel D39), or a PUT whose id belongs to another world (D70). Retrying will not
|
|
@@ -2549,9 +2556,12 @@ declare class OwV2Client {
|
|
|
2549
2556
|
/** GET /{type}/{id}/ -- optional one-level stub expansion / sparse fields. */
|
|
2550
2557
|
get(type: ElementType | string, id: string, opts?: Pick<ListParams, 'expand' | 'fields'>): Promise<OwElement>;
|
|
2551
2558
|
/**
|
|
2552
|
-
* POST /{type}/ -- create one element, minting
|
|
2553
|
-
*
|
|
2554
|
-
*
|
|
2559
|
+
* POST /{type}/ -- create one element, minting an id on the client when you leave id out.
|
|
2560
|
+
* With an `idempotencyKey`, the minted id is derived from the key (a UUIDv5, the same one the
|
|
2561
|
+
* Python package mints), so a retry with the same key and element sends the same body and keel
|
|
2562
|
+
* replays its first answer: nothing is written twice, and the stored 201 comes back. Without a
|
|
2563
|
+
* key, a fresh UUIDv7. You may still supply your own id. A new create meant to write again
|
|
2564
|
+
* (an undo that re-creates) needs a new key, or none: within 24 h a reused key only replays.
|
|
2555
2565
|
* `name` is the one required field, and required means the key is present: `''` and
|
|
2556
2566
|
* `null` are accepted and stored as `''` (nameless Markers exist).
|
|
2557
2567
|
*/
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,61 @@
|
|
|
1
|
+
// src/v2/keyed-id.ts
|
|
2
|
+
var KEYED_IDS = "0199b6a0-6f0e-7c2a-9d1e-3b5c7a8e4f21";
|
|
3
|
+
function sha1(msg) {
|
|
4
|
+
const ml = msg.length;
|
|
5
|
+
const total = (ml + 8 >> 6) + 1 << 6;
|
|
6
|
+
const buf = new Uint8Array(total);
|
|
7
|
+
buf.set(msg);
|
|
8
|
+
buf[ml] = 128;
|
|
9
|
+
const bits = ml * 8;
|
|
10
|
+
const view = new DataView(buf.buffer);
|
|
11
|
+
view.setUint32(total - 8, Math.floor(bits / 2 ** 32));
|
|
12
|
+
view.setUint32(total - 4, bits >>> 0);
|
|
13
|
+
let h0 = 1732584193, h1 = 4023233417, h2 = 2562383102, h3 = 271733878, h4 = 3285377520;
|
|
14
|
+
const w = new Uint32Array(80);
|
|
15
|
+
for (let off = 0; off < total; off += 64) {
|
|
16
|
+
for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + i * 4);
|
|
17
|
+
for (let i = 16; i < 80; i++) {
|
|
18
|
+
const x = w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16];
|
|
19
|
+
w[i] = x << 1 | x >>> 31;
|
|
20
|
+
}
|
|
21
|
+
let a = h0, b = h1, c = h2, d = h3, e = h4;
|
|
22
|
+
for (let i = 0; i < 80; i++) {
|
|
23
|
+
const [f, k] = i < 20 ? [b & c | ~b & d, 1518500249] : i < 40 ? [b ^ c ^ d, 1859775393] : i < 60 ? [b & c | b & d | c & d, 2400959708] : [b ^ c ^ d, 3395469782];
|
|
24
|
+
const t = (a << 5 | a >>> 27) + f + e + k + w[i] >>> 0;
|
|
25
|
+
e = d;
|
|
26
|
+
d = c;
|
|
27
|
+
c = b << 30 | b >>> 2;
|
|
28
|
+
b = a;
|
|
29
|
+
a = t;
|
|
30
|
+
}
|
|
31
|
+
h0 = h0 + a >>> 0;
|
|
32
|
+
h1 = h1 + b >>> 0;
|
|
33
|
+
h2 = h2 + c >>> 0;
|
|
34
|
+
h3 = h3 + d >>> 0;
|
|
35
|
+
h4 = h4 + e >>> 0;
|
|
36
|
+
}
|
|
37
|
+
const out = new Uint8Array(20);
|
|
38
|
+
const ov = new DataView(out.buffer);
|
|
39
|
+
[h0, h1, h2, h3, h4].forEach((h, i) => ov.setUint32(i * 4, h));
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
var hexBytes = (hex) => Uint8Array.from(hex.replace(/-/g, "").match(/../g).map((b) => parseInt(b, 16)));
|
|
43
|
+
function uuid5(ns, name) {
|
|
44
|
+
const nameBytes = new TextEncoder().encode(name);
|
|
45
|
+
const input = new Uint8Array(16 + nameBytes.length);
|
|
46
|
+
input.set(hexBytes(ns));
|
|
47
|
+
input.set(nameBytes, 16);
|
|
48
|
+
const b = sha1(input).slice(0, 16);
|
|
49
|
+
b[6] = b[6] & 15 | 80;
|
|
50
|
+
b[8] = b[8] & 63 | 128;
|
|
51
|
+
const h = [...b].map((x) => x.toString(16).padStart(2, "0")).join("");
|
|
52
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
53
|
+
}
|
|
54
|
+
function keyedId(idempotencyKey, index) {
|
|
55
|
+
return uuid5(KEYED_IDS, `${idempotencyKey}
|
|
56
|
+
${index}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
1
59
|
// src/v2/errors.ts
|
|
2
60
|
var OwApiError = class extends Error {
|
|
3
61
|
constructor(status, code, message, docUrl, detail, type = null, param = null, retryAfter = null) {
|
|
@@ -26,6 +84,15 @@ var OwApiError = class extends Error {
|
|
|
26
84
|
get isIdempotencyConflict() {
|
|
27
85
|
return this.status === 409 && this.code === "idempotency_error";
|
|
28
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* A request with this Idempotency-Key is still running (409 `idempotency_in_flight`, keel
|
|
89
|
+
* #307, D105): two sends of one create or bulk overlapped. Nothing was written by this one.
|
|
90
|
+
* Wait `retryAfter` seconds (keel sends 1) and send again with the SAME key and body: once
|
|
91
|
+
* the first has answered, that retry replays its answer. Not a key conflict (see above).
|
|
92
|
+
*/
|
|
93
|
+
get isIdempotencyInFlight() {
|
|
94
|
+
return this.status === 409 && this.code === "idempotency_in_flight";
|
|
95
|
+
}
|
|
29
96
|
/**
|
|
30
97
|
* The id is already taken (409 `id_conflict`): a create with an id that exists
|
|
31
98
|
* (keel D39), or a PUT whose id belongs to another world (D70). Retrying will not
|
|
@@ -265,16 +332,19 @@ var OwV2Client = class {
|
|
|
265
332
|
return this.request("GET", `/${type}/${id}/`, { query });
|
|
266
333
|
}
|
|
267
334
|
/**
|
|
268
|
-
* POST /{type}/ -- create one element, minting
|
|
269
|
-
*
|
|
270
|
-
*
|
|
335
|
+
* POST /{type}/ -- create one element, minting an id on the client when you leave id out.
|
|
336
|
+
* With an `idempotencyKey`, the minted id is derived from the key (a UUIDv5, the same one the
|
|
337
|
+
* Python package mints), so a retry with the same key and element sends the same body and keel
|
|
338
|
+
* replays its first answer: nothing is written twice, and the stored 201 comes back. Without a
|
|
339
|
+
* key, a fresh UUIDv7. You may still supply your own id. A new create meant to write again
|
|
340
|
+
* (an undo that re-creates) needs a new key, or none: within 24 h a reused key only replays.
|
|
271
341
|
* `name` is the one required field, and required means the key is present: `''` and
|
|
272
342
|
* `null` are accepted and stored as `''` (nameless Markers exist).
|
|
273
343
|
*/
|
|
274
344
|
async create(type, element, opts = {}) {
|
|
275
345
|
const body = sanitizePayload(element);
|
|
276
346
|
if (body.id === void 0 || body.id === null || body.id === "") {
|
|
277
|
-
body.id = mintUuid();
|
|
347
|
+
body.id = opts.idempotencyKey ? keyedId(opts.idempotencyKey, 0) : mintUuid();
|
|
278
348
|
}
|
|
279
349
|
return this.request("POST", `/${type}/`, {
|
|
280
350
|
write: true,
|