Repository navigation
Releases: hyphatech/spindle
Release list
0.8.0
Spindle.Melange.clientprints the routes as one OCaml module for Melange, from the same walk as the OpenAPI document and the Zod schemas: wiretype's types for every shape, and a function per route that calls it, its inputs labelled and typed and its answer a promise of aresult-- the route's answer, or`Invalidan input that breaks its description with nothing sent,`Lost,`Refusedwith the server's refusal, or`Unexpected. A stream of events is a function handed each event as a variant, read overfetchand stopped withRoute.Stream.close; a WebSocket's is handed each message typed, and itsRoute.Socket.sendis typed, checked and held until the socket opens. A socket reading a header, and a response of any kind, have no function.api melangewrites it, andapi check --melange FILEholds a committed copy to the routes. Melange is a test dependency alone: a native user installs nothing new.- Breaking: wiretype 0.6 is required.
0.7.0
- Breaking:
Spindle.Zod.module_isSpindle.Zod.schemas, a noun likeOpenapi.document, without the trailing underscore. - A resource is what the application holds -- a pool, a client, a hub -- read by a route as a dependency:
Dep.of_resource "database"names one,let+ pool = db inreads it, andSpindle.serve,App.startandTest.apptake~resources:[ Dep.resource db pool ]. The routes are then a value, made before any database is, so a command line or a document describes them with none. A route reading a resource the server was not given stops it before it listens, naming the route and the resource, as does one supplied twice or aDep.resourceof whatof_resourcedid not make; one read insideDep.bindand not supplied answers500, logged by name.Test.appneeds only the resources the routes a test calls read, andTest.callraisesInvalid_argumenton one that reads a resource it was not given. The API's document and the Zod schemas never list a resource, which no request carries.Dep.needhas aResourceconstructor. - Breaking:
Spindle_cli.run's~appis the app itself, a(Spindle.App.t, string) result, where it was a function of the environment and a switch, and theSpindle_cli.apptype is gone. It takes()where it tookenv, so a command line needs no event loop:let () = Spindle_cli.run ~name ~app:(Spindle.App.make routes) (). Routes that read their database as a resource need nothing made to be described. - Breaking:
Rate.createtakes no~mono_clock, so a limit is a value made at the top level, beside the routes that read it:let sign_ins = Spindle.Rate.create ~limit:10 ~per:Mtime.Span.min (). Its counts are the app's, so two apps -- two servers, two tests -- never share a key's, and they are timed on the monotonic clockApp.startnow takes as~mono_clock, whichSpindle.servegives from its environment; a limit in an app never started answers500. A test givesTest.app ~mono_clockanEio_mockclock to move; by default its clock never moves.
0.6.1
- Installs from Hypha's opam repository:
opam repo add hypha https://github.com/hyphatech/opam-repository.git, thenopam install spindle spindle-postgres spindle-cli. Thepin-dependsare gone: spindle takes wiretype 0.5 and rowtype 0.8, each up to its next breaking release.
0.6.0
- An answer is written by the fiber that flushes it, where every flush was handed to a writer fiber of the connection's own and waited for: a send is one write, with no fiber woken and no promise to wait on, and a deadline moves by writing an
int, where it boxed anMtime.teach time. Sending to many clients at once leaves far less for the collector to promote -- delivering an event to ten thousand subscribers costs about a third less CPU -- and a plaintext answer about an eighth less. A write the socket refuses ends its connection without another attempt. A takeover is still handed aBuf_write.twith a fiber that copies it. spindle-postgresis pinned to rowtype 0.8.0, its latest release, where it was pinned to 0.7.0.
0.5.0
- A content type a route takes that is no
type/subtype,type/*ortype/*+suffix--"json","application/ json", one with parameters -- is refused byApp.make, naming the route and the pattern, soSpindle.serveandTest.appraise on it, where it matched nothing and refused every body without a word.Compress.makeraisesInvalid_argumentfor such a~typesentry, which compressed nothing.Spindle_http.Media_type.is_patternis the test. - Through wiretype 0.5.0, an answer outside its description's bounds is never written: the route's bug, a
500logged with why. A read-only member a request sends is not read, and a write-only one is never in an answer, as the API's document already said. - Breaking:
spindleneeds wiretype 0.5.0, and depends on Uuidm.Spindle.Codec.uuidis aUuidm.t, where it was astring, as a UUID is in a body and in a row, so one identifier crosses them as one type. - Breaking: a codec of your own is spelt as wiretype spells a kind:
Codec.custom ~kind ~parse ~print (), whose~parseanswersOkthe value orErrorthe sentence a client is told, where it answered an option and?expectsended a sentence the framework made.Codec.parseanswers the sameresult.Codec.enum's~kindis optional, asWiretype.enum's is. - Breaking:
spindle-postgresneeds rowtype 0.7.0 and postgres-eio 0.5.0. A connection target is aRowtype_postgres.Conninfo.tforSpindle_postgres.connectandPool.create, read from a connection string byConninfo.of_string;Pool.run, a program's entry point, still takes the string and reads it once, a string that does not parse ending the program as a database that refuses does.?connect_timeoutis gone: the target carries its own (Conninfo.with_connect_timeout), and one that names none is still bounded at 10 s. - Breaking:
Spindle_postgres.Pool.query, which runs one statement, isPool.exec, so it is not taken for rowtype'srunbesidePool.run, the program's pool.Spindle_postgres.erroris[ Rowtype.error | Rowtype_postgres.Transaction.failure | Rowtype_postgres.Pool.error ], anderror_to_stringisRowtype_postgres.error_to_string, the one printer for all of it. - rowtype's new
`Unsent-- the database could not be reached, so nothing was done -- is answered bySpindle_postgres.refusalas503 busy, withRetry-After: 1, as`Pool `Busyis, and logged as a warning. spindle-postgresno longer setsDateStyle: the driver sets it, and the other settings it reads text by, on every connection itself.- A closing connection's last bytes are read against the connection's deadline, for
linger, as every other wait on a client is, where a timer of its own bounded them; a deadline's tick countslingeramong the limits it is a tenth of, so it is a tenth of a second at the defaults. Request.clientread from a trusted proxy'sX-Forwarded-Foris the address alone, where an element written1.2.3.4:5678or[2001:db8::1]:4711was taken whole: every connection was a client of its own, and aRate.limitkeyed by the client limited nobody. A trusted hop written with its port is passed over as one without.- A log line is UTF-8 and one line whatever it holds: each byte of a message or a field that begins no character is written U+FFFD, as Go's
encoding/jsonwrites it, where a JSON line copied it and was one a shipper drops; and aPrettyline writes a line's end in its message or a field as\nor\r, where a Postgres error'sDETAILbegan a line of its own. - Breaking: an exception's message is never logged.
Spindle_http.Log.raisedwriteserror.kindanderror.stackand no longererror.message, and the500a raise is answered with, a stream's failure, a background job's and a connection's say the exception's kind, where each wrotePrintexc.to_string-- whatever the raiser wrote, a card number among it. An I/O error's words, the operating system's, are still a failed connection's. - Breaking: the packages
spindle_postgresandspindle_cliarespindle-postgresandspindle-cli, and so are their libraries, as opam names a package; adunefile says(libraries spindle-postgres). The modules staySpindle_postgresandSpindle_cli. The log sourcespindle_postgresisspindle-postgres, the name a user installed. - Every public
errortype has its printer beside it:Session,Body,Multipart,Spindle_http.HeadandHead.Response, andSpindle_http.Multiparthaveerror_to_string, andTesthaswebsocket_error_to_string. - Breaking:
Cors.make's?headers, the request headers a preflight allows, is?allow_headers, asAccess-Control-Allow-Headersnames them:~headers:["x-token"]read as headers to send, as everywhere else?headersis. - Breaking:
nowis always an instant.Spindle.serve's?now,Server.run's andServer.serve_on's~nowandRequest.make's~now, each aunit -> Ptime.t, areread_clock, whereTest.call ~nowandSession's~nowstay aPtime.t.Spindle.clock_now clock ()isSpindle.of_clock clock, which isSpindle_http.Instant.of_clock. - Breaking:
Spindle.body_streamtakes?content_types:string list, eachtype/subtype,type/*ortype/*+suffix, where it took a predicate on the rawContent-Type: the framework compares type and subtype without case and ignores parameters, soapplication/octet-stream; charset=binaryis the type it names, and the OpenAPI document lists them as the request body's content, where it always saidapplication/octet-stream.Dep.of_bodyloses?content_type: the types are itsneed's,Dep.Raw { content_types }orDep.Json's, andDep.RawandDep.Streamcarry them.Spindle_http.Media_type.matchesis the comparison, whichCompress.make'stypesuse too. - Breaking:
Spindle_client.erroris[ `Unsent of string | `Lost of string ], the words every Hypha library uses for what happened to the work, so a handler's onelet*runs over a client call and a database statement.UnansweredandTimeout, which a caller handled alike since the server may have acted on either, are both`Lost, a passed deadline's words saying so.websocket_erroris[ error | `Not_upgraded of response | `Websocket of Spindle_http.Websocket.error ], where it wasFailed,RefusedandEnded; andevents_end'sRefusedisAnswered, so the two no longer share a constructor. - A client call's deadline covers looking its host's name up: eio_posix looks a name up in a system thread no cancellation reaches, so a call with a 200 ms timeout waited for a slow resolver for as long as it took. One fiber of the client's, on its switch, now runs every lookup, and a call abandons its wait at its deadline.
- Under
compress, every precondition sees through a compressed answer's marked tag: a route is askedIf-MatchandIf-None-Matchwith the-gzipmark taken off --If-None-Matchwhere the request takes gzip -- soIf-Matchholding the tag a client was given passes, where it was412, and an application's route compares the tags it set. A304repeats the marked tag where the client sent it, and every304saysVary: Accept-Encodingunless it names a type never compressed, as RFC 9110 §15.4.5 requires of theVaryits200would carry; a304fromStaticorFileshad none.If-Rangestill reads the tag as it came, since a part is only ever of the uncompressed answer. - Breaking:
Spindle_http.Status.tnames every status RFC 9110 §15 defines, and 103, 425, 428, 451 and 511 --`Continue,`Early_hints,`Non_authoritative_information,`Reset_content,`Multiple_choices,`Use_proxy,`Payment_required,`Not_acceptable,`Proxy_authentication_required,`Length_required,`Expectation_failed,`Misdirected_request,`Too_early,`Precondition_required,`Unavailable_for_legal_reasonsand`Network_authentication_required-- andSpindle_http.Meth.tnames`CONNECTand`TRACE, where each was`Code nor`Other s. Both sets are closed at 1.0: a status or method registered later stays`Code nor`Other sfor every 1.x release. - Breaking:
Spindle_http.Websocket.sendof a text message that is not UTF-8 answersError (Unwritable _), a new constructor oferror, and sends nothing, where it went on the wire for the peer to fail the connection with 1007 (RFC 6455 §8.1). A JSON value its description cannot encode answers the same, where it was logged and answeredOk (). A handler that returnsUnwritablecloses its socket with 1011. - Breaking:
Spindle_http.Multipart.to_stringanswers aresult, and refuses what its reader would not read back as itself: a boundary RFC 2046 does not allow, a content type that cannot be written, and content holding the delimiter, which wrote a part of the caller's choosing into the body. A part'sheadersare not written, as the.mlinow says. - A takeover answered while the server stops is in flight from its route to its socket, so it gets the drain to say goodbye, where it was cancelled the moment its
101had gone. - A body read still waiting when its route's handler returns -- in a fiber the route left behind -- is stopped and answers
`End, logged as the route's bug, before the connection loop reads on. The two read the connection at once, and the connection hung with no deadline. - A
426names itsUpgradeinConnection(upgrade, orupgrade, closewhen the connection ends), as RFC 9110 §7.8 requires of every sender ofUpgrade; a WebSocket route's426sentUpgradealone. - A client that asked for
100 Continueand left before it was sent makes the route's body read answerError (Unreadable _), whereEio.Buf_write.Flush_abortedwas raised into the route. - A stream's
sendanswersError Goneonce its producer has returned or raised, and logs it as the route's bu...
0.4.0
-
Breaking:
spindle_postgresneeds rowtype 0.6.0 and postgres-eio 0.4.0. A unique, exclusion or foreign-key constraint declaredDEFERRABLEthat refuses a transaction'sCOMMITis`Conflictwith the constraint's name, as one a statement finds is, so a route's`Conflictarm answers it with the code it declares, where it was`Not_committedand answered500.Rowtype_postgres.Conninfo.tis a private record: set a database or a connect timeout withConninfo.with_databaseorConninfo.with_connect_timeout. -
Breaking:
spindleneeds wiretype 0.4.0, which changes whatSpindle.OpenapiandSpindle.Zodwrite for some types: a derived type with parameters is written where it is used, not as one shared component, unless[@@kind]names it; and kinds that hold each other in a ring of three or more refer to each other, where the member closing the ring was any JSON. A JSON body whose map key is read in more than one spelling -- auuidin either case -- is refused when it gives one key twice. -
A WebSocket send's
send_timeoutruns from the last byte the peer took, as an answer's does, where it ran afresh for each 16 KiB written: a peer reading a large message steadily below about 546 bytes a second was cut off asLost. One that stops taking anything still is, up to a tenth of the limit late. -
Spindle.Refusal.busywrites aRetry-Afterof at least one second, asrate_limiteddoes, where aretry_after_sbelow one was written as it came, and a negative one is noRetry-AfterRFC 9110 allows. -
Spindle.Rate.createraisesInvalid_argumentfor apershorter thanlimitnanoseconds, which left no time between two calls and so limited nothing. Not breaking in practice: no limit that worked is refused. -
Breaking:
App.makerefuses a JSON answer under204or205--Returns.json ~status:`No_contentor either listed inReturns.json_response'sstatuses-- which HTTP sends without a body, so the document promised JSON the wire dropped. Only a table that was already wrong stops starting: declare such an answer withReturns.emptyorReturns.empty_response. -
Breaking:
Spindle_http.Head.Response.readanswers aHead.Response.error,ClosedorMalformed of string, where it answered the polymorphic`Closedand`Malformed: one shape for the same event asHead.Request.read'sHead.error, whoseClosedit matches. -
Breaking: a Structured Field date is
Spindle_http.Structured.Date of Ptime.t, where it was anintof seconds since 1970, since every instant is aPtime.t. A date outside the years 0 to 9999, which RFC 9651 allows and noPtime.tholds, is refused when read, as its published suite lets a parser do; one written with a fraction of a second is written in the second it is in. -
Breaking: a
Spindle_clientanswer'sstatus--response's andanswer's -- is aSpindle_http.Status.t, as a route answers with andHead.Response.tholds, where it was anint: match`OK, or take the number withSpindle_http.Status.to_int. -
Breaking:
Spindle_client.errorsays whether the request went out.Unreachableis gone: a request that never left -- no address, a refused connection, failed TLS, a URL or field the client cannot write -- isUnsent, and one that went and was not answered whole -- closed under it, cut short, unreadable, too long -- isUnanswered, so a caller knows when aPOSTcan be sent again.Timeoutmay be either, and is to be taken asUnanswered. A client span's failure saysunsentorunansweredwhere it saidunreachable. -
A WebSocket message's buffer grows as its bytes arrive, where it was made as large as its first frame declared: fourteen bytes from a peer held as much memory as
max_messageallows, a mebibyte by default, for as long as the peer cared to send nothing more. -
Spindle_clientsends a URL's path and query as the URL wrote them, where it decoded them and encoded them again:%3Bwent as;,%2Fas/and+as%20, so a signed URL reached its server as one it did not sign. -
A WebSocket handler that raises is logged once, at
error, with what was raised and where: a server's socket on its line saying how it ended, wherespindle.httpalso logged a line of its own, and a client's on the lineSpindle_client.websocketwrites when its socket ends, which now says where it went and how long it lived.Spindle_http.Websocket.run_serverandrun_clientlog nothing of a raise they pass on. -
A request whose
Expectlists100-continueamong other elements, or in a secondExpectfield, is sent100 Continue, where only a field that was100-continuealone was:Expectis a list (RFC 9110 §10.1.1), and is read as one. -
Spindle_clientopens every connection through thenetit is given, withEio.Net.connect, where it opened its sockets withUnixitself, so a test can give itEio_mock.Net. Each connection lives on a switch of its own, so one that fails to connect still leaves no socket behind. A socket with no descriptor beneath it, as a mock network's, is closed after its call and never kept, since the client could not tell that the server closed it. -
A server whose
acceptfails -- out of file descriptors or memory, or a connection reset before it was taken -- logs it atwarningand accepts again 10 ms later, where the failure ended the server and cancelled every connection it held. -
A
Response.streamsent from several fibers at once sends one after another, as an event stream's filler and producer already did, andsend_timeoutends every one of them: two sends at once could leave one waiting for ever on a client that stopped reading, holding its connection slot. -
Path.urlwrites a segment of.or..as%2Eor%2E%2E, where it wrote it as it was, and a browser resolved/users/..to/. -
Breaking:
Key.signandKey.sealuse keys of their own, derived from the secret apart from a signed and an encrypted cookie's, so a token an application signs or seals over text a caller chose is never a cookie:Key.signover a signed cookie's payload was a valid cookie, andKey.sealan encrypted one. A tokenKey.signorKey.sealmade before no longer verifies or opens; cookies are made and read as before. -
Breaking: under
Cors.Any, a write from another site carrying anAuthorizationin a scheme a browser caches and sends by itself --Basic,Digest,Negotiate,NTLM-- or one that is no credentials is403 cross_origin, as one carrying aCookiewas: a form another site posts is never preflighted and rides a cached login as it does a session. ABearertoken, or any other scheme a page sets itself, is served as before. -
Breaking:
App.makerefuses aReturns.json ~statusorReturns.empty ~statusthat is not2xx, as it refused one listed inReturns.json_response, and aRefusal.Codewhose status is not4xxor5xx, wherever it is declared: the OpenAPI document could give such a route two answers under one status, and a refusal answered200read as a success. A redirect isReturns.responsewithResponse.redirect. -
Breaking:
Cookie.signedandCookie.encryptedtake~max_age:Mtime.Span.twhere they took~max_age_s:int, asSession.createtakes its spans: it is a bound the server holds a cookie to, not only theMax-Ageit writes, which is the span in whole seconds rounded up.Cookie.make ?max_age_sis that field and stays a number of seconds. -
Breaking:
Spindle.Test.response'sstatusis aSpindle_http.Status.t, as every other status in the API is, where it was anint: a test comparesSpindle_http.Status.to_int r.status, or matches`OK. -
Breaking: a form's files are read by
Form.File.required,Form.File.optionalandForm.File.list, each aForm.File.t, as its fields are byForm.required,optionalandlist, where they wereForm.file,Form.file_optandForm.files, each aForm.file.
0.3.1
0.3.0
-
Breaking: every instant is a
Ptime.t, where it was anintof milliseconds since 1970 that said neither its unit nor that it was a time.Spindle.nowis aPtime.t Dep.t, and so is whatRequest.nowanswers; the~nowthatSpindle.serve,Server.run,Server.serve_onandRequest.maketake is aunit -> Ptime.t, andSpindle.epoch_msisSpindle.clock_now, the clock's time as one.Test.call,Test.Browser.call,Test.eventsandTest.websockettake?now:Ptime.t,Ptime.epochunless given.Session.entry'screated_ms,seen_msandexpires_msarecreated,seenandexpires, andSession.find,start,renew,updateandsweep, a store'ssweepandCookie.Named.unsealtake~now:Ptime.t.Codec.instantis aPtime.t Codec.t, andConditional's validators, and so a file'sLast-Modified, hold aPtime.t. Inspindle.http,Date.parseisnow:Ptime.t -> string -> Ptime.t optionandWrite.datewrites aPtime.t; a trace's span holdsstart : Ptime.tandduration : Mtime.Span.t, where it heldstart_nsandend_ns.Spindle_postgres.instantis gone:Rowtype.instantreads and writes aPtime.t. Each is written as it was -- an HTTP date, an RFC 3339 JSON string, atimestamptz, OTLP's nanoseconds -- where it crosses a wire. -
Breaking: every duration is an
Mtime.Span.tnamed for what it bounds, where it was a float of seconds, or an int of milliseconds, named for its unit: a wait in the wrong unit no longer compiles. Write~send_timeout:Mtime.Span.(60 * s).Spindle.serve,Server.runandServer.serve_ontake?head_timeout,?idle_timeout,?body_timeout,?send_timeout,?lingerand?drain, where each ended in_s, and refuse a timeout of zero.Returns.events,Returns.websocket,Response.eventsandSpindle_http.Websocket'srun_serverandrun_clienttake?keep_alive;Response.stream's keep-alive is{ quiet; filler }, andResponse.connectionandSpindle_http.Connection.tholdsend_timeout.Spindle_client'screateandruntake?timeoutand?idle_timeout, where they took?timeout_sand?idle_s;call,stream,eventsandwebsockettake?timeout,streamandevents?read_timeout, andwebsocket?keep_alive; andSpindle_client.Timeoutcarries theMtime.Span.tthat passed.Alarm.createtakes?slackandAlarm.set~after, where they took?slack_msand~in_ms.Rate.createtakes~per, the span its~limitis counted over, where it took~per_s, and refuses a~perof zero.Session.createtakes~idleand~absolute, where it took~idle_sand~absolute_s.spindle_postgres'sconnect,Pool.createandPool.runtake?statement_timeout,?idle_in_transaction_timeout,?connect_timeoutand?timeout,Pool.createandPool.run?wait,?max_lifetimeand?idle_checktoo, andPool.use,queryandtransaction?wait. Every default is the same length as before. -
Spindle.until ~now deadlineis how long until an instant: the span fromnowto it, rounded up to the nanosecond, and zero once it has passed, since a wait cannot be negative -- what anAlarmis set for,~after:(Spindle.until ~now deadline).Spindle_http.Instanthas it, and an instant read from a clock and an instant a span on. -
In
spindle_postgres, a statement's and an idle transaction's bounds are written to Postgres in whole milliseconds, rounded up, so a span under a millisecond is still a bound; a span of zero lifts one, as Postgres's0does. -
A number written into a message for another to read stays a whole number in the message's unit, labelled with it:
Refusal.rate_limited ~retry_after_sandRefusal.busy ?retry_after_s,Cors.make ?max_age_s,Cookie.make ?max_age_s,Cookie.signedandCookie.encrypted's~max_age_s,Event.retry ~msand a read event'sretry_ms. A session's cookie'sMax-Ageis its absolute limit in whole seconds, rounded up, so the cookie outlives the session. -
spindleasks for wiretype 0.3.0 and depends on ptime, andspindle_postgresfor rowtype 0.4.0 and postgres-eio 0.3.0, each pinned at that tag, so what changed there reaches a program through them. Breaking: a program that uses one of them itself moves with it -- wiretype'sinstantis aPtime.t, itsdurationanMtime.Span.tand itsdictlabels~keyand~value; rowtype-postgres's durations areMtime.Span.ts named for what they bound. -
A log line's time is read from the system's clock through ptime, and a file's entity tag is made from its time to the nanosecond as ptime holds it, so a file's tag may change once, a cache asking for it again.
-
Breaking:
Refusal.rate_limitedisretry_after_s:int -> t, without the()it took after its one argument, which is labelled and not optional:Refusal.rate_limited ~retry_after_s:30. -
spindle_postgresasks for rowtype 0.3.0 and postgres-eio 0.2.0, and pins both at those tags, so what changed there reaches a program through it. Breaking: a statement on a closed connection is`Connection `ClosedinSpindle_postgres.error, where it was`Closed; a fiber cancelled whilePool.use,queryortransactionruns its work stops at once, its connection closed and made again at the next lend, where the work ran to its end; and a program that uses rowtype itself moves to 0.3.0 with it. -
In
spindle_postgres,Session's store tells a busy pool in the pool's own words, "no connection came free within the pool's wait", aserror_to_stringand the libraries under it do, where it said "no connection within the wait". -
A connection's last bytes -- what a WebSocket handler wrote before it returned, a stream's last chunk -- have
send_timeoutfrom the last byte the client took, as every answer has, and the connection is closed once it passes. A client that stopped reading held its connection slot for ever. -
Spindle_client.websocketreturns once a send to a server that stopped reading answersLost: what was left unwritten hastimeoutfor the server to take anything, and is then dropped with the connection. The call waited for it to go, which could be for ever. -
Spindle_client.streamtakes an answer whose head arrived in the instant its deadline passed, where it answeredTimeoutand left the answer's connection open until the client's switch ended. -
Response.stream ~lengthbelow zero is the route's bug, answered500with an error in the log, as a header that frames the answer is. The server wroteContent-Length: -1and raisedInvalid_argumentinto the producer at its first send. -
In
spindle_postgres,Pool.checkfails once the pool is closed, or closing, where/readyzanswered200: a closed pool has no connection idle, which the check read as every connection lent. -
In
spindle_postgres,Session.schemafor a table whose name is longer than 52 bytes leaves its index for Postgres to name, where it named it after the table and Postgres cut that name to 63 bytes: at 63, the table's own name, and the schema failed with42P07. -
Spindle.Server.run ~hostreadslocalhostin any case,LocalHostincluded, as both loopback addresses; another case went to the resolver, which may name only one. -
Spindle_clientreads a server'sKeep-Alive: timeout="5", quoted, as it doestimeout=5, where it read no timeout and kept the connection idle for its ownidle_timeoutbeside a server that closes it sooner. -
Spindle.Test.app's refusal of aFiles.directoryroute tells a test to serve a directory it opened withFiles.route, where it namedStatic.loadandStatic.routefor either; its documents name both routes it refuses. -
A WebSocket handler that raises ends its socket's line at
error, with what was raised and where (error.kind,error.message,error.stack), as a stream's or a background job's raise is; it was aninfoline that said only that the handler raised. -
The examples atop
SpindleandSpindle.Returnstype-check: the first servesroutes ~engine, the second answersfind_user id.spindle.http's README says what its readers answer as they do. -
Spindle_client.error's deadline that passed isTimeout, where it wasTimed_out: the word postgres-eio and Eio's ownTimeout.rungive the same failure. Breaking: a match on a call's error namesTimeout. -
A body's failures have one word each, in
Spindle.Body.errorand inSpindle_http.Framing.readandread_somealike: a body that could not be read isUnreadablein both, whereFraminganswered`Broken, and one the server'sbody_budgethad no room for isOver_budget, where both answeredBusy, which said nothing of the budget. The answer is unchanged:503 busy. Breaking: a match on a body's error names the new constructors. -
In
spindle_postgres, a pool's failures are its states under`Pool:`Pool `BusywhereSpindle_postgres.error,Pool.use,queryandtransactionanswered`Busy, and`Pool `Closedfor a pool closed, or closing under a borrow waiting, at once, where a closed pool waited outwaitand answered`Busy.refusalanswers both503 busy, logged as a warning with no detail, anderror_to_stringwords the closed one "the pool is closed". A closed connection stays rowtype's, a500. Breaking: a match on what a pool answers names`Pool. -
Spindle.client_gone, a dependency, is a promise the server resolves once a request's client has gone: an events stream's write failing, its keep-alive's included, or the exchange ending.Broadcast.next ~untilanswersNoneonce it resolves, so a producer waiting on a quiet topic ends when its client leaves, and gives back its fiber and connection slot, rather than at the next event. Nothing is cancelled; a producer that does not wait on it learns at its nextsend, as before. -
In
spindle_cli, every command's--helplists exit status 1, whatapi checkexit...
0.2.0
- Breaking: Spindle needs wiretype 0.2.0, whose changes reach an application through it: a refusal's
atquotes a member name that is not plain (body["a.b"]), an answer holding a float that is not finite is a500rather thannull, and the document's schemas say what the reader means where they once guessed. wiretype's CHANGES.md lists each. - Spindle needs eio 1.6, the first to raise
Eio.Fs.Symlink, whichFileshas matched since 0.1.0, whose opam files allowed 1.5. spindle:Codec.floatreads a number in decimal digits, with an optional minus sign, fraction and exponent, and refuses whatfloat_of_stringwould also take but no client means, and a number too large for a float. It is described asnumber. Breaking:NumberjoinsCodec.shape, so a match over it needs the case.spindle:Codec.uuid,Codec.dateandCodec.instantread a UUID, an RFC 3339 date and an instant as wiretype's kinds read them in a body, and are described with their formats. Breaking:FormatjoinsCodec.shape.spindle:Query.defaultandHeader.defaultread an input that may be absent as its value or a default, and the document says the default. Breaking:defaultjoinsDep.input.