@onlyworlds/sdk 4.56.0 → 4.57.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.
package/AGENTS.md CHANGED
@@ -30,8 +30,10 @@ against live data). The v1 surface (`OnlyWorldsClient`, the `ElementType` enum)
30
30
  - Offline or stale edits: pass `{ ifMatch: el.change_seq }` to `patch`/`editLinks` (since 4.9.0).
31
31
  If the element moved, nothing is written and `OwPreconditionFailedError` (412) carries
32
32
  `current`; the server never merges, the client decides.
33
- - Polling: `changesHead()` (since 4.15.0) gives `{head, cursor}` without a page; walk only
34
- when `head` moved past the head you last applied.
33
+ - Polling: `changesHead()` (since 4.15.0) gives `{head, cursor, epoch, unchanged}` without a
34
+ page; walk only when `head` or `epoch` moved past what you last applied. Since 4.57.0 it
35
+ sends the last ETag as `If-None-Match`: a 304 (refunded by keel) comes back as the held
36
+ values with `unchanged: true`. The client never sleeps or picks an interval.
35
37
  - World-meta changes do NOT appear in `/changes` — poll `GET /world` separately.
36
38
  - Extension fields: `x_<toolname>_*` is the sanctioned namespace for tool-specific state;
37
39
  unknown unprefixed fields 422. Extensions are capped at **64 KB per element** (422,
package/CHANGELOG.md CHANGED
@@ -5,6 +5,18 @@ earlier history lives in git log only.
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [4.57.0] - 2026-10-22
9
+
10
+ ### Added
11
+
12
+ - **The conditional head poll** (keel #518, live 2026-10-22; Orrery's `revision-poll.md`, decision 3): `changesHead()` keeps the last head read's `ETag` per client and sends it back as `If-None-Match` on the next one, exactly as received (keel's 200 sends a weak `W/"821:-"`; nothing is quoted, parsed or compared). keel's tag is `"<head>:<epoch>"`, so a view change moves it as a new change does. When nothing moved, keel answers 304 Not Modified with no body, and `changesHead()` returns the `head`, `cursor` and `epoch` of the last 200 with `unchanged: true`; every 200 says `unchanged: false`. keel refunds a 304 against the request budget, so a poll that mostly finds nothing costs little. A 409 `resync_required` from `changesHead()`, `changes()` or `changesAll()` clears the held tag, so the next head read is a full 200 with `epoch_reason`. A 304 while no tag is held is an `OwApiError` with status 304, never a made-up head. The client still never polls or sleeps on its own: the interval is the caller's.
13
+ - **The public page on `OwWorldMeta`** (keel #471, PR 173, live 2026-10-22), both read-only: `slug` (`string | null`), the page's address, onlyworlds.com/w/<slug>, null while the page is off, and also a keyless `?world=` value; `public_page` (the new `OwWorldPublicPage`), the page as its owner set it: `on`, `palette` (midnight, daylight, parchment, brass, dusk, or null for the house's), `hero` (one plain line), `sections` (the types shown in order, empty for all), `pin` (an element id or null) and `index` (always false for now), every key present with the page off. Both are set by the owner on the world page; `patchWorld()` strips them, so a body read from `getWorld()` stays writable.
14
+ - **`request()` gains two options**: `ifNoneMatch` (a tag sent as `If-None-Match` as given) and `onEtag(etag, status)` (called with each response's `ETag` and status before the body is read; with it given, a 304 returns null instead of throwing). Every other path is unchanged.
15
+
16
+ ### Changed
17
+
18
+ - **`changesHead()`'s return type gains `unchanged: boolean`**, beside `head`, `cursor`, `epoch` and the optional `epoch_reason`.
19
+
8
20
  ## [4.56.0] - 2026-10-22
9
21
 
10
22
  ### Added
package/README.md CHANGED
@@ -180,7 +180,7 @@ for await (const change of client.changesAll(cursor)) {
180
180
  }
181
181
  ```
182
182
 
183
- To poll for news cheaply, `client.changesHead()` returns `{ head, cursor }` without reading any element: walk `changesAll` only when `head` moved past the head you last applied.
183
+ To poll for news cheaply, `client.changesHead()` returns `{ head, cursor, epoch, unchanged }` without reading any element: walk `changesAll` only when `head` or `epoch` moved past what you last applied. Since 4.57.0 the client sends the last answer's ETag back, so when nothing moved keel answers 304 (refunded against your request budget) and `unchanged` is true. Sleep on it; the interval is yours.
184
184
 
185
185
  Edits to the world itself (its name, calendar, `public_read`) don't appear in the change feed. Read `client.getWorld()` and compare `updated_at`.
186
186
 
package/dist/index.d.ts CHANGED
@@ -1947,11 +1947,38 @@ interface OwWorldMeta {
1947
1947
  agent_narrator_rule?: boolean;
1948
1948
  /** The owner's "this Character is me": a Character id in this world, or null (null too, to a reader it is hidden from: keel #421). */
1949
1949
  owner_character?: string | null;
1950
+ /**
1951
+ * The world's public page address, onlyworlds.com/w/<slug> (keel #471): null when the world has
1952
+ * no page (a slug exists only while the page is on). Also a keyless `?world=` value. Set by the
1953
+ * owner on the world page; read-only on the wire, and patchWorld strips it.
1954
+ */
1955
+ slug?: string | null;
1956
+ /**
1957
+ * The world's public page as its owner set it (keel #471): `on`, and the levers. Every key is
1958
+ * present with the page off. Set by the owner on the world page; read-only on the wire, and
1959
+ * patchWorld strips it.
1960
+ */
1961
+ public_page?: OwWorldPublicPage;
1950
1962
  created_at?: string | null;
1951
1963
  /** Moves when the world's own fields change, not on element writes. */
1952
1964
  updated_at?: string | null;
1953
1965
  [field: string]: unknown;
1954
1966
  }
1967
+ /** OwWorldMeta.public_page (keel #471), as keel's `World` component types it: all six keys, always. */
1968
+ interface OwWorldPublicPage {
1969
+ /** Whether the page is on. */
1970
+ on: boolean;
1971
+ /** The page's palette, or null for the house's. */
1972
+ palette: 'midnight' | 'daylight' | 'parchment' | 'brass' | 'dusk' | null;
1973
+ /** One plain line (at most 120 characters). */
1974
+ hero: string;
1975
+ /** The element types shown, in their order; empty for all, in the house order. */
1976
+ sections: string[];
1977
+ /** One element's id, or null. */
1978
+ pin: string | null;
1979
+ /** Always false for now: search engines stay off. */
1980
+ index: boolean;
1981
+ }
1955
1982
  /** List envelope: cursor-paginated. */
1956
1983
  interface OwPage<T = OwElement> {
1957
1984
  data: T[];
@@ -3180,6 +3207,8 @@ declare class OwV2Client {
3180
3207
  private readonly agent;
3181
3208
  /** The name sent as X-OW-Client on element writes (keel #520), checked at construction. */
3182
3209
  private readonly clientName;
3210
+ /** The conditional head poll's state (keel #518), per instance. */
3211
+ private readonly headPoll;
3183
3212
  constructor(config?: OwClientConfig);
3184
3213
  /** GET /health -- unauthenticated liveness pulse. */
3185
3214
  health(): Promise<unknown>;
@@ -3189,7 +3218,10 @@ declare class OwV2Client {
3189
3218
  * do not bump change_seq. Poll getWorld().updated_at for meta freshness.
3190
3219
  */
3191
3220
  getWorld(): Promise<OwWorldMeta>;
3192
- /** PATCH /world -- partial world-meta update. */
3221
+ /**
3222
+ * PATCH /world -- partial world-meta update. The element write's server-managed keys are
3223
+ * stripped, and so are `slug` and `public_page` (keel #471: set on the world page, not here).
3224
+ */
3193
3225
  patchWorld(partial: Record<string, unknown>): Promise<OwWorldMeta>;
3194
3226
  /**
3195
3227
  * GET /me -- who this world key is: the world, read or write scope, the kind (owner,
@@ -3353,11 +3385,25 @@ declare class OwV2Client {
3353
3385
  * says why the epoch last moved, `role` or `view` (see OwChangesPage.epoch_reason).
3354
3386
  * keel returns the owner's cursor as a number; it is handed back as a
3355
3387
  * string, like every cursor here.
3388
+ *
3389
+ * Conditional (keel #518, 4.57.0): keel tags the answer with an ETag that covers the head and
3390
+ * the epoch, so a view change moves it too. The client keeps the last tag and sends it as
3391
+ * If-None-Match on its next head read. When nothing moved, keel answers 304 with no body, and
3392
+ * this returns the `head`, `cursor` and `epoch` from the last 200 with `unchanged: true`
3393
+ * (no `epoch_reason`); every 200 says `unchanged: false`. keel refunds a 304 against the
3394
+ * request budget, so a poll that mostly sees nothing costs little. A poller: read the head,
3395
+ * sleep when `unchanged`, walk changesAll when `head` or `epoch` moved past what you applied.
3396
+ * The interval is yours; the client never sleeps or polls on its own. The tag is kept per
3397
+ * client instance and sent back exactly as received (`W/` and quotes included), never
3398
+ * compared; a 409 `resync_required` from this or from changes/changesAll clears it, so the
3399
+ * next head read is a full 200 (with `epoch_reason`). A 304 while no tag is held is an
3400
+ * OwApiError (status 304): the client never makes up a head.
3356
3401
  */
3357
3402
  changesHead(): Promise<{
3358
3403
  head: number;
3359
3404
  cursor: string;
3360
3405
  epoch: number | null;
3406
+ unchanged: boolean;
3361
3407
  epoch_reason?: OwResyncReason | null;
3362
3408
  }>;
3363
3409
  /**
@@ -3790,6 +3836,13 @@ declare class OwV2Client {
3790
3836
  /** One of the seven element writes keel reads X-OW-Client on (keel #520: create, put, patch, delete, links, bulk, adopt): where the client's name goes. */
3791
3837
  elementWrite?: boolean;
3792
3838
  ifMatch?: number;
3839
+ /** A tag to send as `If-None-Match` exactly as given (keel #518, the head read): unlike ifMatch, nothing is quoted or checked. */
3840
+ ifNoneMatch?: string;
3841
+ /**
3842
+ * Called with every response's `ETag` header (as received, null when absent) and status, before
3843
+ * the body is read. With it given, a 304 Not Modified returns null instead of throwing.
3844
+ */
3845
+ onEtag?: (etag: string | null, status: number) => void;
3793
3846
  }): Promise<T>;
3794
3847
  }
3795
3848
  /**
@@ -4830,4 +4883,4 @@ declare class TokenResource {
4830
4883
  getEncryptionInfo(): Promise<EncryptionInfo>;
4831
4884
  }
4832
4885
 
4833
- export { AGENT_DOORS, ASK_LIMITS, type AccessKeyResponse, CALENDAR_FIELD, CALENDAR_SUPERTYPE, type CalendarConfig, type CalendarEra, type CalendarIntercalary, type CalendarMonths, type CalendarWeek, ELEMENT_FAMILIES, ELEMENT_ICONS, ELEMENT_LABELS, ELEMENT_SECTIONS, ELEMENT_TYPES, type ElementFamily, type ElementType, type EncryptionInfo, type EraDomain, FAMILY_COLORS, FAMILY_ORDER, FIELD_ACRONYMS, FIELD_SCHEMA, FILE_EXTENSIONS, type FieldInfo, type FieldType, GameTier, type ListParams, ONLYWORLDS_VERSION, type OwAccountWorld, type OwAgentJoined, type OwAllowance, OwApiError, type OwAskAnswer, type OwAskContext, type OwAuthErrorCode, type OwBulkItem, type OwBulkItemResult, type OwBulkResponse, type OwCalendarElement, type OwChange, type OwChangeDelete, type OwChangeUpsert, type OwChangesPage, type OwClientConfig, type OwCreatedWorldLink, type OwDeleteReason, type OwDraught, type OwDraughtComponent, type OwDraughtItem, type OwDraughtPolicy, type OwElement, type OwElementBase, type OwErrorBody, type OwFileAddedBy, type OwFileDownload, type OwFileKind, type OwFileRow, type OwFileTicket, type OwHiddenRow, type OwJoinPreview, type OwJoined, type OwJoinedMembership, type OwKeyKind, type OwLinkEdit, type OwLookupElement, type OwLookupHit, type OwLookupLink, type OwLookupOptions, type OwLookupReason, type OwMe, type OwMediaTicket, type OwMember, type OwMemberRole, type OwMintedKey, OwNetworkError, type OwPage, type OwPhotoOptions, type OwPhotoPlan, type OwPlayerJoined, OwPreconditionFailedError, type OwPreparedPhoto, type OwReadWorldFile, type OwReadWorldFileOptions, type OwRemovalTicket, type OwRemovedImage, type OwRenderOptions, type OwResyncReason, type OwSeat, type OwSeatCreated, type OwSignInBegin, type OwSignInBegun, type OwSignInComplete, OwSignInError, type OwSignInReason, type OwSignInResult, type OwSignInStorage, type OwSignedIn, type OwSortAddToRow, type OwSortAnswer, type OwSortAskRow, type OwSortLinkRow, type OwSortMakeRow, type OwSortNote, type OwSortRef, type OwSortRow, type OwUploadedFile, type OwUploadedImage, OwV2Client, type OwWorldFileEntry, OwWorldFileError, type OwWorldFileReason, type OwWorldFileRow, type OwWorldFiles, type OwWorldIndex, type OwWorldLink, type OwWorldMeta, type OwWriteDoor, PHOTO_MAX_BYTES, PHOTO_TYPES, PUBLIC_FIELDS, type ParseError, type ParseOptions, type ParseResult, ROOM_PAIRS, type RevokeAllSessionsResponse, type RevokeSessionResponse, SPATIAL_TYPES, type SectionInfo, type StepBreakdown, type TokenConsumeParams, type TokenConsumeResponse, TokenResource, type TokenStatus, type TokenTransport, WORLD_FILE_MAX_BYTES, WORLD_FILE_MAX_ENTRIES, WORLD_FILE_MEDIA_TYPE, type WorldCalendar, absYearForEraYear, absoluteYear, approxYearLength, base64url, breakdownStep, buildAskContext, buildWorldIndex, detectKeyKind, displayYear, elementColor, eraForStep, errorFromResponse, familyOf, fieldLabel, fileKindOf, floorDiv, floorMod, formatDuration, formatMidLabel, formatStep, formatYearLabel, getElementIcon, getElementLabel, isAgentDoor, isDemoKey, isKnownOf, isLeapYear, isParseError, isRoomElement, kindCanWrite, lookupWorld, monthStartSteps, parseDate, parseErrorEnvelope, pinExpectation, pkceChallenge, planPhoto, preparePhoto, readCalendarConfig, readCalendarValue, readWorldFile, renderElement, resolveCalendarLens, resolveEraDomains, simpleCaseFold, validateCalendarConfig, weekdayForStep, worldCalendars, writeWorldFile, yearLength, yearStartStep };
4886
+ export { AGENT_DOORS, ASK_LIMITS, type AccessKeyResponse, CALENDAR_FIELD, CALENDAR_SUPERTYPE, type CalendarConfig, type CalendarEra, type CalendarIntercalary, type CalendarMonths, type CalendarWeek, ELEMENT_FAMILIES, ELEMENT_ICONS, ELEMENT_LABELS, ELEMENT_SECTIONS, ELEMENT_TYPES, type ElementFamily, type ElementType, type EncryptionInfo, type EraDomain, FAMILY_COLORS, FAMILY_ORDER, FIELD_ACRONYMS, FIELD_SCHEMA, FILE_EXTENSIONS, type FieldInfo, type FieldType, GameTier, type ListParams, ONLYWORLDS_VERSION, type OwAccountWorld, type OwAgentJoined, type OwAllowance, OwApiError, type OwAskAnswer, type OwAskContext, type OwAuthErrorCode, type OwBulkItem, type OwBulkItemResult, type OwBulkResponse, type OwCalendarElement, type OwChange, type OwChangeDelete, type OwChangeUpsert, type OwChangesPage, type OwClientConfig, type OwCreatedWorldLink, type OwDeleteReason, type OwDraught, type OwDraughtComponent, type OwDraughtItem, type OwDraughtPolicy, type OwElement, type OwElementBase, type OwErrorBody, type OwFileAddedBy, type OwFileDownload, type OwFileKind, type OwFileRow, type OwFileTicket, type OwHiddenRow, type OwJoinPreview, type OwJoined, type OwJoinedMembership, type OwKeyKind, type OwLinkEdit, type OwLookupElement, type OwLookupHit, type OwLookupLink, type OwLookupOptions, type OwLookupReason, type OwMe, type OwMediaTicket, type OwMember, type OwMemberRole, type OwMintedKey, OwNetworkError, type OwPage, type OwPhotoOptions, type OwPhotoPlan, type OwPlayerJoined, OwPreconditionFailedError, type OwPreparedPhoto, type OwReadWorldFile, type OwReadWorldFileOptions, type OwRemovalTicket, type OwRemovedImage, type OwRenderOptions, type OwResyncReason, type OwSeat, type OwSeatCreated, type OwSignInBegin, type OwSignInBegun, type OwSignInComplete, OwSignInError, type OwSignInReason, type OwSignInResult, type OwSignInStorage, type OwSignedIn, type OwSortAddToRow, type OwSortAnswer, type OwSortAskRow, type OwSortLinkRow, type OwSortMakeRow, type OwSortNote, type OwSortRef, type OwSortRow, type OwUploadedFile, type OwUploadedImage, OwV2Client, type OwWorldFileEntry, OwWorldFileError, type OwWorldFileReason, type OwWorldFileRow, type OwWorldFiles, type OwWorldIndex, type OwWorldLink, type OwWorldMeta, type OwWorldPublicPage, type OwWriteDoor, PHOTO_MAX_BYTES, PHOTO_TYPES, PUBLIC_FIELDS, type ParseError, type ParseOptions, type ParseResult, ROOM_PAIRS, type RevokeAllSessionsResponse, type RevokeSessionResponse, SPATIAL_TYPES, type SectionInfo, type StepBreakdown, type TokenConsumeParams, type TokenConsumeResponse, TokenResource, type TokenStatus, type TokenTransport, WORLD_FILE_MAX_BYTES, WORLD_FILE_MAX_ENTRIES, WORLD_FILE_MEDIA_TYPE, type WorldCalendar, absYearForEraYear, absoluteYear, approxYearLength, base64url, breakdownStep, buildAskContext, buildWorldIndex, detectKeyKind, displayYear, elementColor, eraForStep, errorFromResponse, familyOf, fieldLabel, fileKindOf, floorDiv, floorMod, formatDuration, formatMidLabel, formatStep, formatYearLabel, getElementIcon, getElementLabel, isAgentDoor, isDemoKey, isKnownOf, isLeapYear, isParseError, isRoomElement, kindCanWrite, lookupWorld, monthStartSteps, parseDate, parseErrorEnvelope, pinExpectation, pkceChallenge, planPhoto, preparePhoto, readCalendarConfig, readCalendarValue, readWorldFile, renderElement, resolveCalendarLens, resolveEraDomains, simpleCaseFold, validateCalendarConfig, weekdayForStep, worldCalendars, writeWorldFile, yearLength, yearStartStep };
package/dist/index.js CHANGED
@@ -504,6 +504,8 @@ var DEFAULT_PAGE_SIZE = 100;
504
504
  var DEFAULT_CHANGES_PAGE_SIZE = 100;
505
505
  var OwV2Client = class {
506
506
  constructor(config = {}) {
507
+ /** The conditional head poll's state (keel #518), per instance. */
508
+ this.headPoll = new HeadPollState();
507
509
  only("new OwV2Client", config, CLIENT_CONFIG_KEYS);
508
510
  this.clientName = clientNameOf(config.client);
509
511
  this.apiKey = config.apiKey ?? "";
@@ -531,9 +533,14 @@ var OwV2Client = class {
531
533
  async getWorld() {
532
534
  return this.request("GET", "/world/");
533
535
  }
534
- /** PATCH /world -- partial world-meta update. */
536
+ /**
537
+ * PATCH /world -- partial world-meta update. The element write's server-managed keys are
538
+ * stripped, and so are `slug` and `public_page` (keel #471: set on the world page, not here).
539
+ */
535
540
  async patchWorld(partial) {
536
- return this.request("PATCH", "/world/", { body: sanitizePayload(partial) });
541
+ const body = sanitizePayload(partial);
542
+ for (const field of WORLD_READ_ONLY_FIELDS) delete body[field];
543
+ return this.request("PATCH", "/world/", { body });
537
544
  }
538
545
  // -- Who am I, and who is here ---------------------------------------------
539
546
  /**
@@ -779,7 +786,7 @@ var OwV2Client = class {
779
786
  async changes(opts = {}) {
780
787
  only("changes", opts, ["since", "limit", "after"]);
781
788
  const query = buildQuery({ since: opts.since, limit: opts.limit ?? this.changesPageSize, after: opts.after });
782
- const page = await this.request("GET", "/changes/", { query });
789
+ const page = await this.headPoll.feedRead(() => this.request("GET", "/changes/", { query }));
783
790
  return { ...page, cursor: String(page.cursor) };
784
791
  }
785
792
  /**
@@ -794,10 +801,41 @@ var OwV2Client = class {
794
801
  * says why the epoch last moved, `role` or `view` (see OwChangesPage.epoch_reason).
795
802
  * keel returns the owner's cursor as a number; it is handed back as a
796
803
  * string, like every cursor here.
804
+ *
805
+ * Conditional (keel #518, 4.57.0): keel tags the answer with an ETag that covers the head and
806
+ * the epoch, so a view change moves it too. The client keeps the last tag and sends it as
807
+ * If-None-Match on its next head read. When nothing moved, keel answers 304 with no body, and
808
+ * this returns the `head`, `cursor` and `epoch` from the last 200 with `unchanged: true`
809
+ * (no `epoch_reason`); every 200 says `unchanged: false`. keel refunds a 304 against the
810
+ * request budget, so a poll that mostly sees nothing costs little. A poller: read the head,
811
+ * sleep when `unchanged`, walk changesAll when `head` or `epoch` moved past what you applied.
812
+ * The interval is yours; the client never sleeps or polls on its own. The tag is kept per
813
+ * client instance and sent back exactly as received (`W/` and quotes included), never
814
+ * compared; a 409 `resync_required` from this or from changes/changesAll clears it, so the
815
+ * next head read is a full 200 (with `epoch_reason`). A 304 while no tag is held is an
816
+ * OwApiError (status 304): the client never makes up a head.
797
817
  */
798
818
  async changesHead() {
799
- const page = await this.request("GET", "/changes/", { query: buildQuery({ head: "true" }) });
800
- const out = { head: page.head, cursor: String(page.cursor), epoch: page.epoch ?? null };
819
+ const seen = { status: 0, etag: null };
820
+ const poll = this.headPoll;
821
+ const page = await poll.feedRead(() => this.request("GET", "/changes/", {
822
+ query: buildQuery({ head: "true" }),
823
+ ifNoneMatch: poll.tag,
824
+ onEtag: (etag, status) => {
825
+ seen.etag = etag;
826
+ seen.status = status;
827
+ }
828
+ }));
829
+ if (seen.status === 304) {
830
+ if (!poll.held) {
831
+ throw new OwApiError(304, null, "GET /changes?head=true answered 304 Not Modified while nothing is held: no head read has answered 200 on this client, so there is no head to return", null, null);
832
+ }
833
+ return { ...poll.held, unchanged: true };
834
+ }
835
+ const tip = { head: page.head, cursor: String(page.cursor), epoch: page.epoch ?? null };
836
+ poll.tag = seen.etag || void 0;
837
+ poll.held = tip;
838
+ const out = { ...tip, unchanged: false };
801
839
  if (page.epoch_reason !== void 0) out.epoch_reason = page.epoch_reason;
802
840
  return out;
803
841
  }
@@ -1514,6 +1552,7 @@ var OwV2Client = class {
1514
1552
  if (opts.body !== void 0) headers["Content-Type"] = "application/json";
1515
1553
  if (opts.idempotencyKey) headers["Idempotency-Key"] = opts.idempotencyKey;
1516
1554
  if (opts.ifMatch !== void 0) headers["If-Match"] = `"${opts.ifMatch}"`;
1555
+ if (opts.ifNoneMatch !== void 0) headers["If-None-Match"] = opts.ifNoneMatch;
1517
1556
  if (this.agent && opts.write) headers["X-OW-Agent"] = "1";
1518
1557
  if (opts.elementWrite) headers["X-OW-Client"] = this.clientName;
1519
1558
  let res;
@@ -1526,6 +1565,10 @@ var OwV2Client = class {
1526
1565
  } catch (cause) {
1527
1566
  throw new OwNetworkError(`OnlyWorlds request failed: ${method} ${path}`, cause);
1528
1567
  }
1568
+ if (opts.onEtag) {
1569
+ opts.onEtag(res.headers?.get?.("ETag") ?? null, res.status);
1570
+ if (res.status === 304) return null;
1571
+ }
1529
1572
  if (!res.ok) throw await errorFromResponse(res);
1530
1573
  if (res.status === 204 || opts.allowEmpty) {
1531
1574
  const text = await res.text();
@@ -1538,7 +1581,22 @@ var OwV2Client = class {
1538
1581
  return parsed;
1539
1582
  }
1540
1583
  };
1584
+ var HeadPollState = class {
1585
+ /** A read of /changes: a 409 `resync_required` drops the tag and the held head, then is thrown as is. */
1586
+ async feedRead(read) {
1587
+ try {
1588
+ return await read();
1589
+ } catch (err) {
1590
+ if (err instanceof OwApiError && err.isResyncRequired) {
1591
+ this.tag = void 0;
1592
+ this.held = void 0;
1593
+ }
1594
+ throw err;
1595
+ }
1596
+ }
1597
+ };
1541
1598
  var READ_ONLY_FIELDS = ["world", "type", "created_at", "updated_at", "change_seq", "known_of"];
1599
+ var WORLD_READ_ONLY_FIELDS = ["slug", "public_page"];
1542
1600
  function sanitizePayload(payload) {
1543
1601
  if (!payload || typeof payload !== "object") return payload;
1544
1602
  const rest = { ...payload };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlyworlds/sdk",
3
- "version": "4.56.0",
3
+ "version": "4.57.0",
4
4
  "description": "TypeScript SDK for the OnlyWorlds API - build world-building applications with type safety",
5
5
  "type": "module",
6
6
  "exports": {