You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
World calendars: OWCalendar reads a world's calendar (the atlas_calendar field Atlas writes on a calendar
Construct) and formats, breaks down and parses dates on the world's integer time line: eras, months, weeks, leap and
intercalary days. It is a port of the npm SDK's calendar kernel (@onlyworlds/sdk 4.23.0), checked against the same
test vectors, so a date reads the same in Unity as in Atlas and Lantern. OWCalendarLens.WorldCalendars and ResolveLens find the calendars a world holds. A missing or malformed calendar reads as null, never an error.
The door of every write (OnlyWorlds keel #299; the npm SDK's 4.26.0 has the same). Agent = true on OWClientConfig sends X-OW-Agent on every element write (create, patch, delete, link edit, bulk), never on a read,
and the server records those writes as agent-made. It is off by default and belongs only in a client an agent
drives.
Every element reads the server-managed ChangedBy (the membership whose write last changed it) and ChangedVia (that
write's door: seat, mcp, declared, api, v1, sync or server). A /changes delete reads DeletedBy and DeletedVia. OWWriteDoor.IsAgent(door) is true for seat, mcp and declared.
OWApiError.RetryAfter: the seconds to wait from the response's Retry-After header (a delay, or an HTTP date
turned into one), or null. The server sends it on 503 server_busy, 429 and 409 idempotency_in_flight. It is read
as the npm SDK reads it, and the client still never retries on its own.
Export an account world to a world folder: OWFolderExport.ExportAsync(client, folder) reads the world and all 22
element types (GETs only) and writes the folder the onlyworlds Python package's export_world writes, a port of its
rules: element files byte for byte, the collision rule's id-ascending names, time_current on disk, format_version"0.3.7". On Moppetopia's 310 elements it matched the package's output on the same wire bodies
with no file different. Each Map's image is kept as Atlas keeps local media (media/<map id>/base-image/, as served,
with its .meta.json sidecar); an image that will not fetch is a line in the report, never a failure. The folder
is built beside its destination and moved into place, so a cancel or an error leaves no half-written world, and it
must not exist or be empty. OWFolderExport.WorldForDisk is the world.json step on its own. The key and PIN go
into no file and no message. On Windows a folder too deep for the files a world needs is refused before the first
write: Unity's Mono does not take long paths.
Changed
Schema pin moved to v0.32.0-dist.24 (from dist.20): canonical unchanged. The element id is described as a
UUID, new ids minted as v7 and earlier v4 ids still valid. The generated models changed only in their stamps.
New ids are UUIDv7, as the standard now says and as the server, the npm SDK (4.2.0 on) and the Python package
mint them: CreateAsync and BulkAsync mint one with OWId.NewId() when an element has no id and no key is
given, where they used a v4 Guid. Ids this client mints a millisecond or more apart sort by creation; a world still
holds v4 ids, which stay valid, so order elements by created_at, never by id. A caller-supplied id is still sent as
given.
Faster folder reads: OWFolderReader.Read (and so OWFolderLoader.LoadInto) reads a world's files on up to 8
threads, then parses them one at a time in the same order as before. On a 20,570-element world it took about 2.0 s
instead of 3.1 s, with the same elements, order, types and skipped files. Parsing stays on one thread on purpose: under
Unity's Mono GC a parallel parse measured slower. WebGL players read one file at a time.
Fixed
The folder writer escapes strings as JSON.stringify does.OWFolderWriter.Serialize wrote U+2028, U+2029 and
U+0085 as escapes, in values and in keys, where the reference implementation and the Python package write them
raw, so a name holding one was a byte difference in every folder. It now matches the package's bytes on a body
holding them, the short escapes, the other controls and a surrogate pair. A lone surrogate is written as its escape.
OWFolderWriter.Write settles a collision id-ascending, as the spec's writer rule requires: a batch is written
lowest id first, so of two elements whose short filenames match (compared case-insensitively, on every filesystem)
the lower id keeps the short form whatever order they came in. Into an empty folder it now names files exactly as
the Python package does. Every body of a batch is checked before the first is written, and a refused batch writes
nothing. WriteElement still never renames an element already in the folder.
ChangesAsync pages the feed. It sent its cursor as cursor=, and the changes feed takes since, refusing any
other parameter with a 422. So OWSync.IncrementalAsync failed whenever a world had changed since its baseline. It
now sends since.
A wire check against the API's own document.OWWireContractTests records every request OWClient can make and
checks each against a snapshot of the server's published OpenAPI document: a path it serves, a method it takes
there, and only query parameters and headers it declares. It catches both bugs above, which had passed their own
tests since 0.1.0. tools/refresh_wire.py refreshes the snapshot (--check says whether the server's document has
moved).
EditLinksAsync reaches the server. Since 0.1.0 it posted to /{type}/{id}/editLinks/, a route the API never
served, so every call failed with 404. It now posts add and remove to /{type}/{id}/links/{field}. Its test had
asserted the wrong URL; it now asserts the server's.
The folder writer's round-trip tests run in any host: they find their fixture through the package's own resolved
path, so a project that lists the SDK in testables and references it by file: from outside its own folder runs
all three instead of skipping them.
CreateAsync sends Idempotency-Key only when you pass one, as the npm SDK does. Since 0.1.0 it sent the
element's id as the key on every create. The server keeps a keyed create's first answer for 24 hours and answers an
identical repeat with it, writing nothing, so re-creating a deleted element under its old id (an undo) got the old
201 back and the element stayed deleted. A retry without a key is safe when the element carries its own id: it
answers 409 id_conflict, and the element is there. An element without one gets a fresh id on every call, so an
unkeyed retry of it creates a second element, as in the npm SDK and the Python package; a caller who retries passes idempotencyKey, and the stored answer is replayed.
A keyed retry replays. With an idempotencyKey and no id, CreateAsync and BulkAsync minted a fresh id on
every call, so a retry sent a different body and the server answered 409 idempotency_error instead of its stored
answer. The id is now derived from the key and the element's position (OWId.Keyed, a UUIDv5 in the Python
package's namespace), so a retry sends the same body. The Python package and the npm SDK (4.27.1) derive the same
id, so a key mints one id in all three clients. Without a key, ids are minted as before.
OWSync.IncrementalAsync resumes from the server's own cursor, kept as is in the cache's new CursorToken,
where it built one from the last change_seq. The server answers a guest key's bare number with 409 resync_required, so a guest key's incremental sync failed on its first real change. A cache synced by an earlier
version has no token and resumes from its change_seq, as before. A 409 resync_required now rebuilds the cache
from the start, with a RebaselineReason saying why: a guest is never sent deletes, so only a rebuild drops what it
can no longer see.
The sync reads the feed's epoch (OWChangesPage.Epoch, kept as OWWorldCache.Epoch). A guest's view can
change with the head unmoved, so a cache at the head is current only when the epoch is unchanged too, and HasChangesAsync says so.
Each upsert is applied from the element it carries (OWChange.Element), where the sync fetched every changed
element again: one request per page instead of one per change. If the feed reports more pages but gives no cursor,
the cache keeps its change_seq and the last cursor the server gave, where it recorded the highest change_seq it
had read; a bulk import gives many changes one change_seq, so that number could claim changes never read. An
empty world's cache no longer rebuilds on every sync.
ChangesAsync sends limit: the client's PageSize, at most 1000, above which the feed answers 422. The
server's default page was 500; the npm SDK asks for 100.
OWApiError.IsRetryable reads the error code, where it was any 429 or 5xx. A 409 idempotency_in_flight (the
same key still running: send it again and it replays the first answer) now reads as retryable. A 429 quota_exceeded (a daily allowance, used up until midnight UTC) and a 503 ask_unavailable or media_unavailable
(a feature switched off) no longer do.
OWApiError.IsAuthError reads the error code, as the npm SDK does: invalid_credentials, key_revoked (and
the reserved world_gone), where it was any 401 or 403. A 403 such as not_author or owner_only comes with a
valid key, and the World Browser no longer tells you to check the key and PIN for one: it says the element is
someone else's, that only the owner can do this, or that the key's scope does not allow it. IsNotFound is a 404
with the code not_found, so a proxy's 404 is not mistaken for a missing element. New: IsNotAuthor, IsOwnerOnly, and IsResyncRequired.
OWKey.CanWrite(OWKeyKind.Account) is false, as the npm SDK's kindCanWrite says: an account key is for the
server's /account routes, where it mints world keys, and writes no world itself.
OWBulkSlot.Succeeded documents the rolled-back batch: after an atomic: true request with Errors true,
nothing was written, yet the slots that would have succeeded still say 201 with the ids they would have had. Do not
record those ids as created.