@abide/abide 0.49.0 → 0.50.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 +70 -52
- package/CHANGELOG.md +177 -0
- package/README.md +12 -7
- package/package.json +14 -5
- package/src/abideLsp.ts +5 -6
- package/src/abideResolverPlugin.ts +197 -169
- package/src/build.ts +79 -56
- package/src/buildDisconnected.ts +0 -2
- package/src/checkAbide.ts +12 -2
- package/src/devEntry.ts +113 -28
- package/src/discoveryEntry.ts +3 -2
- package/src/lib/bundle/disconnected.abide +69 -73
- package/src/lib/bundle/infoPlist.ts +13 -7
- package/src/lib/bundle/spawnEmbeddedServer.ts +13 -4
- package/src/lib/bundle/waitForServer.ts +6 -1
- package/src/lib/mcp/mcpTools.ts +15 -6
- package/src/lib/server/error.ts +6 -6
- package/src/lib/server/json.ts +17 -3
- package/src/lib/server/rpc/defineRpc.ts +58 -24
- package/src/lib/server/rpc/parseArgs.ts +98 -13
- package/src/lib/server/rpc/resolveRpcJsonSchema.ts +28 -0
- package/src/lib/server/rpc/types/RpcHelper.ts +75 -105
- package/src/lib/server/rpc/types/RpcRegistryEntry.ts +24 -9
- package/src/lib/server/rpc/validationError.ts +1 -1
- package/src/lib/server/runtime/DEV_RELOAD_CLIENT_SCRIPT.ts +0 -15
- package/src/lib/server/runtime/buildCacheSnapshot.ts +10 -9
- package/src/lib/server/runtime/buildInspectorSurface.ts +6 -4
- package/src/lib/server/runtime/buildOpenApiSpec.ts +55 -11
- package/src/lib/server/runtime/buildPreloadManifest.ts +12 -10
- package/src/lib/server/runtime/createAppAssetServer.ts +18 -15
- package/src/lib/server/runtime/createAppRouteResolver.ts +145 -0
- package/src/lib/server/runtime/createPlumbingRouter.ts +212 -0
- package/src/lib/server/runtime/createRouteDispatcher.ts +4 -10
- package/src/lib/server/runtime/createServer.ts +120 -324
- package/src/lib/server/runtime/createUiPageRenderer.ts +137 -26
- package/src/lib/server/runtime/devClientFingerprint.ts +19 -34
- package/src/lib/server/runtime/installAmbientScopeStore.ts +30 -9
- package/src/lib/server/runtime/logExposedSurfaces.ts +8 -7
- package/src/lib/server/runtime/pathStore.ts +15 -0
- package/src/lib/server/runtime/registryManifests.ts +1 -1
- package/src/lib/server/runtime/renderCellBarrierStore.ts +15 -0
- package/src/lib/server/runtime/runWithRequestScope.ts +3 -0
- package/src/lib/server/runtime/serializeCacheSnapshot.ts +6 -4
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +14 -14
- package/src/lib/server/runtime/streamCacheResolutions.ts +14 -7
- package/src/lib/server/runtime/streamFromIterator.ts +30 -2
- package/src/lib/server/runtime/textResponse.ts +23 -0
- package/src/lib/server/runtime/types/DevReloadStamp.ts +6 -10
- package/src/lib/server/runtime/types/InspectorCacheEntry.ts +1 -1
- package/src/lib/server/runtime/types/RequestStore.ts +27 -0
- package/src/lib/server/runtime/warnUnguardedMcp.ts +15 -5
- package/src/lib/server/sockets/createSocketDispatcher.ts +9 -11
- package/src/lib/server/sockets/defineSocket.ts +5 -4
- package/src/lib/server/sse.ts +5 -1
- package/src/lib/shared/ASYNC_CELL.ts +5 -0
- package/src/lib/shared/DEV_REBUILD_PATH.ts +6 -0
- package/src/lib/shared/HTTP_METHODS.ts +6 -0
- package/src/lib/shared/HttpError.ts +1 -5
- package/src/lib/shared/MCP_PATH.ts +6 -0
- package/src/lib/shared/PROXIED_SERVER_SUBDIRS.ts +15 -0
- package/src/lib/shared/REF_JSON_TAGS.ts +13 -1
- package/src/lib/shared/RPC_SHIM_GLOBALS.ts +9 -0
- package/src/lib/shared/activeCacheStore.ts +1 -0
- package/src/lib/shared/activePage.ts +1 -0
- package/src/lib/shared/buildArtifact.ts +4 -1
- package/src/lib/shared/buildSocketOverChannel.ts +4 -3
- package/src/lib/shared/bundleGraphFromMetafile.ts +68 -0
- package/src/lib/shared/cache.ts +156 -131
- package/src/lib/shared/changeAffectsClient.ts +12 -7
- package/src/lib/shared/createCacheStore.ts +31 -8
- package/src/lib/shared/createLifecycleChannel.ts +7 -1
- package/src/lib/shared/createReachable.ts +47 -78
- package/src/lib/shared/createRemoteFunction.ts +52 -30
- package/src/lib/shared/createRpcServerProgram.ts +820 -0
- package/src/lib/shared/decodeRefJson.ts +4 -4
- package/src/lib/shared/decodeResponse.ts +2 -2
- package/src/lib/shared/decodeWireBody.ts +35 -0
- package/src/lib/shared/detectRpcMethod.ts +8 -1
- package/src/lib/shared/done.ts +8 -2
- package/src/lib/shared/encodeRefJson.ts +4 -4
- package/src/lib/shared/encodeWireBody.ts +18 -0
- package/src/lib/{server/rpc → shared}/fieldErrorsFromIssues.ts +5 -1
- package/src/lib/shared/generateDeclarations.ts +72 -0
- package/src/lib/shared/hasSeedableRequest.ts +25 -0
- package/src/lib/shared/hydrationWindow.ts +53 -0
- package/src/lib/shared/isAsyncCell.ts +11 -0
- package/src/lib/shared/isAsyncIterable.ts +8 -0
- package/src/lib/shared/isSubscribable.ts +3 -3
- package/src/lib/shared/isThenable.ts +9 -0
- package/src/lib/shared/jsonSchemaForType.ts +258 -0
- package/src/lib/shared/lenientDecode.ts +15 -0
- package/src/lib/shared/loadProjectTsConfig.ts +28 -0
- package/src/lib/shared/markFrameworkSourcesIgnored.ts +1 -1
- package/src/lib/shared/matchRoute.ts +4 -14
- package/src/lib/shared/peek.ts +14 -0
- package/src/lib/shared/pending.ts +9 -6
- package/src/lib/shared/pendingAsyncCellsSlot.ts +13 -0
- package/src/lib/shared/prepareRpcModule.ts +91 -93
- package/src/lib/shared/prepareSocketModule.ts +3 -2
- package/src/lib/shared/probeRegistries.ts +5 -18
- package/src/lib/shared/reachable.ts +7 -10
- package/src/lib/shared/refresh.ts +12 -2
- package/src/lib/shared/refreshing.ts +8 -2
- package/src/lib/shared/resolvedCellsSlot.ts +13 -0
- package/src/lib/shared/reviveWireField.ts +49 -0
- package/src/lib/shared/reviveWireOutput.ts +36 -0
- package/src/lib/shared/rpcServerForRoot.ts +25 -0
- package/src/lib/shared/scanPages.ts +25 -0
- package/src/lib/shared/snapshotShippable.ts +8 -7
- package/src/lib/shared/streamedCellsSlot.ts +14 -0
- package/src/lib/shared/subscribableFromResponse.ts +4 -4
- package/src/lib/shared/subscribableProbes.ts +1 -1
- package/src/lib/shared/tailProbeSlot.ts +1 -1
- package/src/lib/shared/types/AsyncComputed.ts +20 -0
- package/src/lib/shared/types/AsyncState.ts +13 -0
- package/src/lib/shared/types/CacheEntry.ts +7 -7
- package/src/lib/shared/types/CacheOptions.ts +23 -37
- package/src/lib/shared/types/CachePolicy.ts +25 -0
- package/src/lib/shared/types/CacheSnapshotEntry.ts +6 -5
- package/src/lib/shared/types/ErrorJsonSchemas.ts +8 -0
- package/src/lib/shared/types/HttpMethod.ts +3 -1
- package/src/lib/shared/types/InputCoercion.ts +17 -0
- package/src/lib/shared/types/{Subscribable.ts → NamedAsyncIterable.ts} +1 -1
- package/src/lib/shared/types/OutputWirePlan.ts +17 -0
- package/src/lib/shared/types/PagesScan.ts +7 -0
- package/src/lib/shared/types/PendingAsyncCells.ts +10 -0
- package/src/lib/shared/types/RawRemoteFunction.ts +6 -0
- package/src/lib/shared/types/RemoteCallable.ts +8 -7
- package/src/lib/shared/types/RemoteFunction.ts +16 -15
- package/src/lib/shared/types/ResolvedCells.ts +11 -0
- package/src/lib/shared/types/ReturnBody.ts +15 -0
- package/src/lib/shared/types/RpcBuildStamps.ts +23 -0
- package/src/lib/shared/types/RpcErrorGuard.ts +3 -8
- package/src/lib/shared/types/Socket.ts +4 -4
- package/src/lib/shared/types/SsrPayload.ts +5 -1
- package/src/lib/shared/types/StreamPolicy.ts +11 -0
- package/src/lib/shared/types/StreamedCells.ts +19 -0
- package/src/lib/shared/types/StreamedResolution.ts +24 -6
- package/src/lib/shared/types/TailHooks.ts +1 -1
- package/src/lib/shared/types/WireKind.ts +11 -0
- package/src/lib/shared/validationHttpError.ts +33 -0
- package/src/lib/shared/warmSeedKey.ts +16 -0
- package/src/lib/shared/wireJsonReplacer.ts +30 -0
- package/src/lib/shared/writeRpcDts.ts +11 -1
- package/src/lib/shared/writeTestSocketsDts.ts +1 -1
- package/src/lib/test/createTestApp.ts +19 -1
- package/src/lib/ui/README.md +1 -1
- package/src/lib/ui/activePendingCells.ts +14 -0
- package/src/lib/ui/compile/BLOCK_KEYWORDS.ts +21 -0
- package/src/lib/ui/compile/SSR_ESCAPE.ts +4 -1
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +35 -8
- package/src/lib/ui/compile/abideUiPlugin.ts +19 -2
- package/src/lib/ui/compile/analyzeComponent.ts +89 -6
- package/src/lib/ui/compile/assignmentTargetNames.ts +54 -0
- package/src/lib/ui/compile/asyncInterpolationFields.ts +152 -0
- package/src/lib/ui/compile/asyncValuePositionError.ts +21 -0
- package/src/lib/ui/compile/asyncValuePositionInterpolations.ts +64 -0
- package/src/lib/ui/compile/attrLiftPosition.ts +18 -0
- package/src/lib/ui/compile/cachedSourceFile.ts +40 -0
- package/src/lib/ui/compile/catchBinding.ts +7 -5
- package/src/lib/ui/compile/classifyInterpolationType.ts +46 -0
- package/src/lib/ui/compile/collectAbideDiagnostics.ts +96 -0
- package/src/lib/ui/compile/compileComponent.ts +19 -3
- package/src/lib/ui/compile/compileModule.ts +27 -58
- package/src/lib/ui/compile/compileSSR.ts +51 -12
- package/src/lib/ui/compile/compileShadow.ts +145 -18
- package/src/lib/ui/compile/composeProps.ts +12 -2
- package/src/lib/ui/compile/createShadowLanguageService.ts +116 -68
- package/src/lib/ui/compile/createShadowProgram.ts +35 -6
- package/src/lib/ui/compile/declaredNames.ts +36 -0
- package/src/lib/ui/compile/desugarSignals.ts +418 -36
- package/src/lib/ui/compile/expressionIsPrefixEvaluable.ts +19 -0
- package/src/lib/ui/compile/generateBuild.ts +52 -9
- package/src/lib/ui/compile/generateSSR.ts +242 -43
- package/src/lib/ui/compile/hoistableAwaits.ts +193 -0
- package/src/lib/ui/compile/hoistableChildRenders.ts +112 -0
- package/src/lib/ui/compile/interpolatedTemplateLiteral.ts +3 -1
- package/src/lib/ui/compile/interpolationClassifierForRoot.ts +37 -0
- package/src/lib/ui/compile/isSpuriousAsyncReadDiagnostic.ts +174 -0
- package/src/lib/ui/compile/liftAsyncSubExpressions.ts +166 -0
- package/src/lib/ui/compile/lowerAsyncInterpolations.ts +92 -0
- package/src/lib/ui/compile/lowerContext.ts +10 -0
- package/src/lib/ui/compile/lowerDocAccess.ts +26 -23
- package/src/lib/ui/compile/lowerScript.ts +40 -4
- package/src/lib/ui/compile/nodeAtShadowOffset.ts +28 -0
- package/src/lib/ui/compile/parseTemplate.ts +18 -1092
- package/src/lib/ui/compile/parseTemplateRecovering.ts +1385 -0
- package/src/lib/ui/compile/referencedIdentifiers.ts +35 -0
- package/src/lib/ui/compile/renameSignalRefs.ts +21 -22
- package/src/lib/ui/compile/seedTypeClassifierForRoot.ts +65 -0
- package/src/lib/ui/compile/shadowInterpolationClassifier.ts +47 -0
- package/src/lib/ui/compile/sourceFileOptionsSignature.ts +20 -0
- package/src/lib/ui/compile/structuralHeadTokens.ts +107 -0
- package/src/lib/ui/compile/templateSemanticTokens.ts +21 -0
- package/src/lib/ui/compile/templateStartOffset.ts +15 -0
- package/src/lib/ui/compile/tryPlan.ts +4 -3
- package/src/lib/ui/compile/types/AnalyzedComponent.ts +4 -0
- package/src/lib/ui/compile/types/AsyncInterpolationField.ts +19 -0
- package/src/lib/ui/compile/types/InterpolationClassifier.ts +12 -0
- package/src/lib/ui/compile/types/InterpolationKind.ts +7 -0
- package/src/lib/ui/compile/types/ParseDiagnostic.ts +8 -0
- package/src/lib/ui/compile/types/SeedTypeClassifier.ts +15 -0
- package/src/lib/ui/compile/types/TemplateNode.ts +36 -4
- package/src/lib/ui/compile/types/ValuePositionInterpolation.ts +13 -0
- package/src/lib/ui/compile/writtenTemplateNames.ts +84 -0
- package/src/lib/ui/computed.ts +28 -1
- package/src/lib/ui/createScope.ts +35 -68
- package/src/lib/ui/dom/anchoredBranch.ts +142 -0
- package/src/lib/ui/dom/appendText.ts +8 -3
- package/src/lib/ui/dom/awaitBlock.ts +43 -94
- package/src/lib/ui/dom/bindProp.ts +22 -0
- package/src/lib/ui/dom/bindableProp.ts +47 -0
- package/src/lib/ui/dom/cellPending.ts +24 -0
- package/src/lib/ui/dom/disposeRange.ts +2 -1
- package/src/lib/ui/dom/each.ts +26 -4
- package/src/lib/ui/dom/eachAsync.ts +39 -3
- package/src/lib/ui/dom/fillBoundary.ts +2 -3
- package/src/lib/ui/dom/fillRange.ts +4 -4
- package/src/lib/ui/dom/hydrate.ts +6 -12
- package/src/lib/ui/dom/isComment.ts +1 -1
- package/src/lib/ui/dom/matchingRangeClose.ts +29 -0
- package/src/lib/ui/dom/mount.ts +1 -2
- package/src/lib/ui/dom/mountChild.ts +9 -40
- package/src/lib/ui/dom/mountRange.ts +2 -3
- package/src/lib/ui/dom/mountStreamedChild.ts +84 -0
- package/src/lib/ui/dom/mountSwappableRange.ts +46 -4
- package/src/lib/ui/dom/mutateDocContainer.ts +65 -0
- package/src/lib/ui/dom/on.ts +2 -2
- package/src/lib/ui/dom/readCell.ts +40 -0
- package/src/lib/ui/dom/switchBlock.ts +30 -7
- package/src/lib/ui/dom/tryBlock.ts +230 -66
- package/src/lib/ui/dom/types/SwitchCase.ts +5 -1
- package/src/lib/ui/dom/when.ts +12 -2
- package/src/lib/ui/dom/withScope.ts +2 -2
- package/src/lib/ui/finalizeStreamedChildren.ts +89 -0
- package/src/lib/ui/flight.ts +56 -0
- package/src/lib/ui/html.ts +12 -1
- package/src/lib/ui/isolateCellBarrier.ts +17 -0
- package/src/lib/ui/linked.ts +48 -2
- package/src/lib/ui/remoteProxy.ts +84 -159
- package/src/lib/ui/renderChain.ts +49 -13
- package/src/lib/ui/renderToStream.ts +22 -18
- package/src/lib/ui/resumeSeedScript.ts +1 -1
- package/src/lib/ui/router.ts +63 -36
- package/src/lib/ui/runtime/AsyncCellError.ts +20 -0
- package/src/lib/ui/runtime/CELL_SEED.ts +14 -0
- package/src/lib/ui/runtime/CURRENT_BOUNDARY.ts +11 -0
- package/src/lib/ui/runtime/CURRENT_PATH.ts +29 -0
- package/src/lib/ui/runtime/RENDER.ts +10 -7
- package/src/lib/ui/runtime/RESUME.ts +4 -4
- package/src/lib/ui/runtime/STREAMED_CELLS.ts +57 -0
- package/src/lib/ui/runtime/ambientPathBacking.ts +32 -0
- package/src/lib/ui/runtime/applyPatchToTree.ts +1 -1
- package/src/lib/ui/runtime/blockId.ts +31 -0
- package/src/lib/ui/runtime/boundaryFor.ts +11 -0
- package/src/lib/ui/runtime/cellBarrierBacking.ts +30 -0
- package/src/lib/ui/runtime/createAsyncCell.ts +300 -0
- package/src/lib/ui/runtime/createDoc.ts +3 -42
- package/src/lib/ui/runtime/createEffectNode.ts +9 -0
- package/src/lib/ui/runtime/enterRenderPass.ts +5 -4
- package/src/lib/ui/runtime/flushEffects.ts +16 -6
- package/src/lib/ui/runtime/isAsyncFunction.ts +14 -0
- package/src/lib/ui/runtime/nextBlockId.ts +8 -7
- package/src/lib/ui/runtime/renderPath.ts +16 -0
- package/src/lib/ui/runtime/types/Boundary.ts +9 -0
- package/src/lib/ui/runtime/types/RenderContext.ts +9 -7
- package/src/lib/ui/runtime/types/SsrRender.ts +9 -2
- package/src/lib/ui/runtime/types/UiComponent.ts +1 -5
- package/src/lib/ui/runtime/withOptionalPath.ts +8 -0
- package/src/lib/ui/runtime/withPath.ts +16 -0
- package/src/lib/ui/runtime/withPathFrom.ts +20 -0
- package/src/lib/ui/seedStreamedResolution.ts +31 -6
- package/src/lib/ui/settleAsyncCells.ts +24 -0
- package/src/lib/ui/socketProxy.ts +1 -1
- package/src/lib/ui/startClient.ts +16 -31
- package/src/lib/ui/trackedComputed.ts +68 -0
- package/src/lib/ui/types/Scope.ts +8 -24
- package/src/lib/ui/watch.ts +3 -3
- package/src/serverEntry.ts +14 -0
- package/template/src/server/rpc/getHello.ts +15 -13
- package/template/test/app.test.ts +1 -1
- package/src/lib/server/runtime/devHotModuleResponse.ts +0 -41
- package/src/lib/shared/DEV_HOT_PREFIX.ts +0 -7
- package/src/lib/shared/UNREACHABLE_STATUSES.ts +0 -13
- package/src/lib/shared/hasReplayableRequest.ts +0 -17
- package/src/lib/shared/hydratingSlot.ts +0 -12
- package/src/lib/shared/outboxProbeSlot.ts +0 -20
- package/src/lib/shared/types/Outbox.ts +0 -9
- package/src/lib/shared/types/OutboxEntry.ts +0 -27
- package/src/lib/shared/types/SmartReadOptions.ts +0 -36
- package/src/lib/shared/wakeHydrationPeeks.ts +0 -20
- package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +0 -5
- package/src/lib/ui/compile/REACTIVE_CALLEES.ts +0 -11
- package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +0 -80
- package/src/lib/ui/compile/markupTokens.ts +0 -313
- package/src/lib/ui/compile/structuralBlockTokens.ts +0 -100
- package/src/lib/ui/dom/applyResolved.ts +0 -87
- package/src/lib/ui/dom/mutateDocArray.ts +0 -38
- package/src/lib/ui/dom/scopeLabel.ts +0 -21
- package/src/lib/ui/dom/text.ts +0 -20
- package/src/lib/ui/history.ts +0 -108
- package/src/lib/ui/installHotBridge.ts +0 -95
- package/src/lib/ui/installInspectorBridge.ts +0 -140
- package/src/lib/ui/outbox.ts +0 -35
- package/src/lib/ui/persist.ts +0 -115
- package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +0 -281
- package/src/lib/ui/rpcOutbox/outboxRegistry.ts +0 -69
- package/src/lib/ui/runtime/PATCH_BUS.ts +0 -28
- package/src/lib/ui/runtime/captureModelDoc.ts +0 -43
- package/src/lib/ui/runtime/hotInstances.ts +0 -10
- package/src/lib/ui/runtime/hotReloadEnabled.ts +0 -8
- package/src/lib/ui/runtime/hotReplace.ts +0 -38
- package/src/lib/ui/runtime/liveScopes.ts +0 -15
- package/src/lib/ui/runtime/localStoragePersistence.ts +0 -47
- package/src/lib/ui/runtime/registerHotInstance.ts +0 -23
- package/src/lib/ui/runtime/seedModelDoc.ts +0 -26
- package/src/lib/ui/runtime/types/HotInstance.ts +0 -22
- package/src/lib/ui/runtime/types/PatchEvent.ts +0 -16
- package/src/lib/ui/seedResolved.ts +0 -28
- package/src/lib/ui/sync.ts +0 -48
- package/src/lib/ui/types/History.ts +0 -14
- package/src/lib/ui/types/PersistHandle.ts +0 -11
- package/src/lib/ui/types/PersistenceStore.ts +0 -12
- package/src/lib/ui/types/ResolvedFrame.ts +0 -15
- package/src/lib/ui/types/SyncTransport.ts +0 -13
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { snippet } from '../shared/snippet.ts'
|
|
2
|
-
import { anchorCursor } from './dom/anchorCursor.ts'
|
|
3
|
-
import { appendSnippet } from './dom/appendSnippet.ts'
|
|
4
|
-
import { appendStatic } from './dom/appendStatic.ts'
|
|
5
|
-
import { appendText } from './dom/appendText.ts'
|
|
6
|
-
import { appendTextAt } from './dom/appendTextAt.ts'
|
|
7
|
-
import { attach } from './dom/attach.ts'
|
|
8
|
-
import { attr } from './dom/attr.ts'
|
|
9
|
-
import { awaitBlock } from './dom/awaitBlock.ts'
|
|
10
|
-
import { bindSelectValue } from './dom/bindSelectValue.ts'
|
|
11
|
-
import { cloneStatic } from './dom/cloneStatic.ts'
|
|
12
|
-
import { each } from './dom/each.ts'
|
|
13
|
-
import { eachAsync } from './dom/eachAsync.ts'
|
|
14
|
-
import { hydrate } from './dom/hydrate.ts'
|
|
15
|
-
import { mergeProps } from './dom/mergeProps.ts'
|
|
16
|
-
import { mount } from './dom/mount.ts'
|
|
17
|
-
import { mountChild } from './dom/mountChild.ts'
|
|
18
|
-
import { mountSlot } from './dom/mountSlot.ts'
|
|
19
|
-
import { mutateDocArray } from './dom/mutateDocArray.ts'
|
|
20
|
-
import { on } from './dom/on.ts'
|
|
21
|
-
import { outlet } from './dom/outlet.ts'
|
|
22
|
-
import { readCall } from './dom/readCall.ts'
|
|
23
|
-
import { restProps } from './dom/restProps.ts'
|
|
24
|
-
import { skeleton } from './dom/skeleton.ts'
|
|
25
|
-
import { spreadAttrs } from './dom/spreadAttrs.ts'
|
|
26
|
-
import { spreadProps } from './dom/spreadProps.ts'
|
|
27
|
-
import { switchBlock } from './dom/switchBlock.ts'
|
|
28
|
-
import { tryBlock } from './dom/tryBlock.ts'
|
|
29
|
-
import { when } from './dom/when.ts'
|
|
30
|
-
import { effect } from './effect.ts'
|
|
31
|
-
import { enterScope } from './enterScope.ts'
|
|
32
|
-
import { exitScope } from './exitScope.ts'
|
|
33
|
-
import { enterRenderPass } from './runtime/enterRenderPass.ts'
|
|
34
|
-
import { escapeKey } from './runtime/escapeKey.ts'
|
|
35
|
-
import { exitRenderPass } from './runtime/exitRenderPass.ts'
|
|
36
|
-
import { hotReloadEnabled } from './runtime/hotReloadEnabled.ts'
|
|
37
|
-
import { hotReplace } from './runtime/hotReplace.ts'
|
|
38
|
-
import { nextBlockId } from './runtime/nextBlockId.ts'
|
|
39
|
-
import { scope } from './scope.ts'
|
|
40
|
-
import { watch } from './watch.ts'
|
|
41
|
-
|
|
42
|
-
/*
|
|
43
|
-
Dev-only: exposes the abide-ui runtime plus `hotReplace` on `window.__abide`, and
|
|
44
|
-
flips on `hotReloadEnabled` so `mountChild` records every instance it mounts. A
|
|
45
|
-
hot module (the dev server's standalone recompile of one edited `.abide`) reads
|
|
46
|
-
its runtime from this object instead of importing fresh copies — so it shares the
|
|
47
|
-
one reactive graph and instance registry — then calls `hotReplace` to swap every
|
|
48
|
-
live instance in place. The keys mirror `UI_RUNTIME_IMPORTS` (+ `hotReplace`); a
|
|
49
|
-
test guards that pairing. `startClient` calls this when the server marks the page
|
|
50
|
-
dev (the live-reload script sets `window.__abideDev`).
|
|
51
|
-
*/
|
|
52
|
-
export function installHotBridge(): void {
|
|
53
|
-
;(globalThis as { __abide?: Record<string, unknown> }).__abide = {
|
|
54
|
-
snippet,
|
|
55
|
-
scope,
|
|
56
|
-
enterScope,
|
|
57
|
-
exitScope,
|
|
58
|
-
effect,
|
|
59
|
-
watch,
|
|
60
|
-
mount,
|
|
61
|
-
appendText,
|
|
62
|
-
appendTextAt,
|
|
63
|
-
appendSnippet,
|
|
64
|
-
appendStatic,
|
|
65
|
-
cloneStatic,
|
|
66
|
-
skeleton,
|
|
67
|
-
anchorCursor,
|
|
68
|
-
attr,
|
|
69
|
-
on,
|
|
70
|
-
attach,
|
|
71
|
-
bindSelectValue,
|
|
72
|
-
each,
|
|
73
|
-
eachAsync,
|
|
74
|
-
when,
|
|
75
|
-
awaitBlock,
|
|
76
|
-
tryBlock,
|
|
77
|
-
switchBlock,
|
|
78
|
-
mountSlot,
|
|
79
|
-
outlet,
|
|
80
|
-
mountChild,
|
|
81
|
-
mergeProps,
|
|
82
|
-
spreadProps,
|
|
83
|
-
restProps,
|
|
84
|
-
spreadAttrs,
|
|
85
|
-
readCall,
|
|
86
|
-
mutateDocArray,
|
|
87
|
-
hydrate,
|
|
88
|
-
escapeKey,
|
|
89
|
-
nextBlockId,
|
|
90
|
-
enterRenderPass,
|
|
91
|
-
exitRenderPass,
|
|
92
|
-
hotReplace,
|
|
93
|
-
}
|
|
94
|
-
hotReloadEnabled.current = true
|
|
95
|
-
}
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import { effect } from './effect.ts'
|
|
2
|
-
import { clientPage } from './runtime/clientPage.ts'
|
|
3
|
-
import { historyEntries } from './runtime/historyEntries.ts'
|
|
4
|
-
import { liveScopes } from './runtime/liveScopes.ts'
|
|
5
|
-
import { PATCH_BUS } from './runtime/PATCH_BUS.ts'
|
|
6
|
-
import { runtimePath } from './runtime/runtimePath.ts'
|
|
7
|
-
|
|
8
|
-
/* Same-origin cross-tab channel the inspector page listens on. The app tab
|
|
9
|
-
publishes scope + router state; the inspector subscribes. Web-standard, so no
|
|
10
|
-
server route or buffer is involved — core publishes, the package consumes. */
|
|
11
|
-
const CHANNEL = 'abide:inspector'
|
|
12
|
-
|
|
13
|
-
/* Coalesce a burst of patches into one snapshot — the post is the cost, the read is cheap. */
|
|
14
|
-
const SNAPSHOT_DEBOUNCE_MS = 60
|
|
15
|
-
|
|
16
|
-
/* JSON round-trip to strip anything structured-clone can't carry (functions,
|
|
17
|
-
class instances, DOM nodes) before postMessage, degrading to undefined rather
|
|
18
|
-
than throwing into the channel. */
|
|
19
|
-
function cloneable(value: unknown): unknown {
|
|
20
|
-
try {
|
|
21
|
-
return JSON.parse(JSON.stringify(value))
|
|
22
|
-
} catch {
|
|
23
|
-
return undefined
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/* The live scope forest as a flat list — the inspector rebuilds the tree from
|
|
28
|
-
each node's `parent` id (the Scope surface exposes no children accessor). */
|
|
29
|
-
function scopeNodes(): Array<{
|
|
30
|
-
id: string
|
|
31
|
-
label: string | undefined
|
|
32
|
-
parent: string | undefined
|
|
33
|
-
state: unknown
|
|
34
|
-
recorded: boolean
|
|
35
|
-
}> {
|
|
36
|
-
return Array.from(liveScopes.scopes, (scope) => ({
|
|
37
|
-
id: scope.id,
|
|
38
|
-
label: scope.label,
|
|
39
|
-
parent: scope.parent?.id,
|
|
40
|
-
state: cloneable(scope.snapshot()),
|
|
41
|
-
recorded: scope.canUndo() || scope.canRedo(),
|
|
42
|
-
}))
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/* Router state off the existing reactive holders — read, not subscribed, here;
|
|
46
|
-
the nav effect drives re-reads. */
|
|
47
|
-
function routerState() {
|
|
48
|
-
const page = clientPage.value
|
|
49
|
-
return {
|
|
50
|
-
path: runtimePath.value,
|
|
51
|
-
route: page.route,
|
|
52
|
-
/* `page.params` is a reactive Proxy — structuredClone (postMessage) can't carry
|
|
53
|
-
a Proxy and throws, dropping every router frame. Spread to a plain object. */
|
|
54
|
-
params: { ...page.params },
|
|
55
|
-
navigating: page.navigating,
|
|
56
|
-
url: page.url.href,
|
|
57
|
-
entry: historyEntries.current,
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/*
|
|
62
|
-
Installs the client→inspector bridge: a BroadcastChannel the inspector page reads
|
|
63
|
-
to render its Reactive + Router tabs. Gated by `__abideInspect` (server-injected
|
|
64
|
-
only when the inspector is enabled) and called from startClient before the router
|
|
65
|
-
builds any scope — so registration is armed before the first scope exists. A
|
|
66
|
-
no-op where BroadcastChannel is absent (SSR, old runtimes). Mirrors
|
|
67
|
-
installHotBridge: dev instrumentation, not public surface.
|
|
68
|
-
*/
|
|
69
|
-
export function installInspectorBridge(): void {
|
|
70
|
-
/* No BroadcastChannel means no consumer for the scope registry — bail before
|
|
71
|
-
arming `liveScopes.enabled`, so createScope never pays scope tracking for a
|
|
72
|
-
set nothing reads. */
|
|
73
|
-
if (typeof BroadcastChannel === 'undefined') {
|
|
74
|
-
return
|
|
75
|
-
}
|
|
76
|
-
liveScopes.enabled = true
|
|
77
|
-
const channel = new BroadcastChannel(CHANNEL)
|
|
78
|
-
const tab = typeof crypto !== 'undefined' ? crypto.randomUUID() : String(performance.now())
|
|
79
|
-
const post = (message: object) => {
|
|
80
|
-
try {
|
|
81
|
-
channel.postMessage({ tab, ...message })
|
|
82
|
-
} catch {
|
|
83
|
-
/* A non-cloneable slipped through despite the JSON pass — drop the frame
|
|
84
|
-
rather than throw into a patch/nav handler. */
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const announce = () =>
|
|
89
|
-
post({
|
|
90
|
-
kind: 'announce',
|
|
91
|
-
url: typeof location !== 'undefined' ? location.href : '',
|
|
92
|
-
app: typeof document !== 'undefined' ? document.title : '',
|
|
93
|
-
})
|
|
94
|
-
const sendSnapshot = () =>
|
|
95
|
-
post({ kind: 'snapshot', scopes: scopeNodes(), router: routerState() })
|
|
96
|
-
|
|
97
|
-
let snapshotTimer: ReturnType<typeof setTimeout> | undefined
|
|
98
|
-
const scheduleSnapshot = () => {
|
|
99
|
-
clearTimeout(snapshotTimer)
|
|
100
|
-
snapshotTimer = setTimeout(sendSnapshot, SNAPSHOT_DEBOUNCE_MS)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/* Inspector handshakes: `hello` (it just opened — everyone re-announce + push)
|
|
104
|
-
and `request` (it wants a fresh snapshot of this tab). */
|
|
105
|
-
channel.onmessage = (event) => {
|
|
106
|
-
const message = event.data as { kind?: string; tab?: string }
|
|
107
|
-
if (message.kind === 'hello') {
|
|
108
|
-
announce()
|
|
109
|
-
sendSnapshot()
|
|
110
|
-
} else if (message.kind === 'request' && message.tab === tab) {
|
|
111
|
-
sendSnapshot()
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/* Every doc mutation flows through PATCH_BUS; forward the patch as a live
|
|
116
|
-
mutation event and re-snapshot (debounced) so values stay current. */
|
|
117
|
-
PATCH_BUS.subscribe((patchEvent) => {
|
|
118
|
-
post({ kind: 'patch', op: patchEvent.patch.op, path: patchEvent.patch.path })
|
|
119
|
-
scheduleSnapshot()
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
/* Reading the page + path States tracks them, so this re-runs on navigation. */
|
|
123
|
-
effect(() => {
|
|
124
|
-
clientPage.value
|
|
125
|
-
runtimePath.value
|
|
126
|
-
post({ kind: 'nav', router: routerState() })
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
if (typeof addEventListener === 'function') {
|
|
130
|
-
addEventListener('pagehide', () => {
|
|
131
|
-
post({ kind: 'bye' })
|
|
132
|
-
channel.close()
|
|
133
|
-
})
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
announce()
|
|
137
|
-
/* First snapshot once the router has built the initial scope tree (this runs
|
|
138
|
-
before router()). */
|
|
139
|
-
setTimeout(sendSnapshot, 0)
|
|
140
|
-
}
|
package/src/lib/ui/outbox.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type { OutboxEntry } from '../shared/types/OutboxEntry.ts'
|
|
2
|
-
import type { RemoteFunction } from '../shared/types/RemoteFunction.ts'
|
|
3
|
-
import { outboxRegistry } from './rpcOutbox/outboxRegistry.ts'
|
|
4
|
-
|
|
5
|
-
/* One entry in the global outbox, tagged with the RPC it belongs to. */
|
|
6
|
-
export type GlobalOutboxEntry = OutboxEntry<unknown> & { rpc: RemoteFunction<unknown, unknown> }
|
|
7
|
-
|
|
8
|
-
/* The global outbox: callable for the flat entry list, awaitable `retry()` to drain every
|
|
9
|
-
queue (resolves when every replay settles). */
|
|
10
|
-
export type GlobalOutbox = (() => GlobalOutboxEntry[]) & { retry: () => Promise<void> }
|
|
11
|
-
|
|
12
|
-
function list(): GlobalOutboxEntry[] {
|
|
13
|
-
return outboxRegistry.all().flatMap(({ rpc, queue }) =>
|
|
14
|
-
queue.entries().map((entry) => ({
|
|
15
|
-
...entry,
|
|
16
|
-
rpc: rpc as RemoteFunction<unknown, unknown>,
|
|
17
|
-
})),
|
|
18
|
-
)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/*
|
|
22
|
-
The global, reactive view of every durable RPC's outbox — a flat list of undelivered
|
|
23
|
-
entries across all `outbox` rpcs, each tagged with its `rpc`. Reactive: reading it in a
|
|
24
|
-
template/effect subscribes to each registered queue, so it updates as writes park, drain,
|
|
25
|
-
fail, or cancel. Use it for an app-wide "N unsynced" badge or a sync panel; cancel a
|
|
26
|
-
pending write through `entry.controller.abort()`, retry one through `entry.retry()`, or
|
|
27
|
-
drain everything through `outbox.retry()`. A single rpc's slice is `rpc.outbox`. Server-
|
|
28
|
-
side there are no client queues, so it returns an empty list.
|
|
29
|
-
*/
|
|
30
|
-
// @documentation ui
|
|
31
|
-
export const outbox: GlobalOutbox = Object.assign(list, {
|
|
32
|
-
async retry(): Promise<void> {
|
|
33
|
-
await Promise.all(outboxRegistry.all().map(({ queue }) => queue.retry()))
|
|
34
|
-
},
|
|
35
|
-
})
|
package/src/lib/ui/persist.ts
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { escapeKey } from './runtime/escapeKey.ts'
|
|
2
|
-
import { localStoragePersistence } from './runtime/localStoragePersistence.ts'
|
|
3
|
-
import { PATCH_BUS } from './runtime/PATCH_BUS.ts'
|
|
4
|
-
import type { Doc } from './runtime/types/Doc.ts'
|
|
5
|
-
import type { PersistenceStore } from './types/PersistenceStore.ts'
|
|
6
|
-
import type { PersistHandle } from './types/PersistHandle.ts'
|
|
7
|
-
|
|
8
|
-
/*
|
|
9
|
-
Makes a document durable: seeds it from the saved snapshot on boot, then writes
|
|
10
|
-
its snapshot back (debounced) on every change the patch bus announces — so the
|
|
11
|
-
state survives a reload. The bus is the single tap; `persist` adds no bookkeeping
|
|
12
|
-
to the doc. A store-less environment (the server, or a browser without
|
|
13
|
-
localStorage and no injected store) returns an inert handle, so the same call is
|
|
14
|
-
safe to write isomorphically.
|
|
15
|
-
|
|
16
|
-
Restore is a per-top-level-key overlay (saved wins, current keeps keys saved
|
|
17
|
-
lacks), so a doc that boots empty fills from the snapshot while a key added in a
|
|
18
|
-
newer app version keeps its fresh default. Writes coalesce into one per `debounce`
|
|
19
|
-
window and flush when the tab is hidden, so the tail of a burst isn't lost.
|
|
20
|
-
*/
|
|
21
|
-
// @documentation plumbing
|
|
22
|
-
export function persist(
|
|
23
|
-
doc: Doc,
|
|
24
|
-
key: string,
|
|
25
|
-
{
|
|
26
|
-
store = localStoragePersistence(),
|
|
27
|
-
debounce = 200,
|
|
28
|
-
}: { store?: PersistenceStore; debounce?: number } = {},
|
|
29
|
-
): PersistHandle {
|
|
30
|
-
/* No durable store → inert: server render, or a browser without localStorage. */
|
|
31
|
-
if (store === undefined) {
|
|
32
|
-
return { flush: () => undefined, clear: () => undefined, dispose: () => undefined }
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const saved = store.load(key)
|
|
36
|
-
if (saved !== undefined) {
|
|
37
|
-
restore(doc, saved)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/* Coalesce a burst into one write: the timer is armed by the first patch and
|
|
41
|
-
writes the latest snapshot when it fires, bounding staleness to `debounce`. */
|
|
42
|
-
let timer: ReturnType<typeof setTimeout> | undefined
|
|
43
|
-
const flush = (): void => {
|
|
44
|
-
if (timer !== undefined) {
|
|
45
|
-
clearTimeout(timer)
|
|
46
|
-
timer = undefined
|
|
47
|
-
}
|
|
48
|
-
store.save(key, doc.snapshot())
|
|
49
|
-
}
|
|
50
|
-
const schedule = (): void => {
|
|
51
|
-
if (timer === undefined) {
|
|
52
|
-
timer = setTimeout(flush, debounce)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const unsubscribe = PATCH_BUS.subscribe((event) => {
|
|
57
|
-
if (event.doc === doc) {
|
|
58
|
-
schedule()
|
|
59
|
-
}
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
/* Flush the tail before the tab goes away, so a pending debounced write survives. */
|
|
63
|
-
const onHidden = (): void => {
|
|
64
|
-
if (typeof document !== 'undefined' && document.visibilityState === 'hidden') {
|
|
65
|
-
flush()
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
const canListen = typeof window !== 'undefined' && typeof window.addEventListener === 'function'
|
|
69
|
-
if (canListen) {
|
|
70
|
-
window.addEventListener('pagehide', flush)
|
|
71
|
-
document.addEventListener('visibilitychange', onHidden)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
flush,
|
|
76
|
-
clear: () => {
|
|
77
|
-
if (timer !== undefined) {
|
|
78
|
-
clearTimeout(timer)
|
|
79
|
-
timer = undefined
|
|
80
|
-
}
|
|
81
|
-
store.remove(key)
|
|
82
|
-
},
|
|
83
|
-
dispose: () => {
|
|
84
|
-
unsubscribe()
|
|
85
|
-
if (timer !== undefined) {
|
|
86
|
-
clearTimeout(timer)
|
|
87
|
-
timer = undefined
|
|
88
|
-
}
|
|
89
|
-
if (canListen) {
|
|
90
|
-
window.removeEventListener('pagehide', flush)
|
|
91
|
-
document.removeEventListener('visibilitychange', onHidden)
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/* Load `saved` into `doc`: a per-key overlay for object roots (so the doc's own
|
|
98
|
-
keys that the snapshot lacks keep their value), else a wholesale root replace
|
|
99
|
-
(a primitive or array root). */
|
|
100
|
-
function restore(doc: Doc, saved: unknown): void {
|
|
101
|
-
const current = doc.snapshot()
|
|
102
|
-
if (isPlainObject(saved) && isPlainObject(current)) {
|
|
103
|
-
/* `replace` takes a `/`-delimited escaped path, so a top-level key containing
|
|
104
|
-
`/` or `~` must be escaped to a single segment or it'd be mis-routed. */
|
|
105
|
-
for (const key of Object.keys(saved)) {
|
|
106
|
-
doc.replace(escapeKey(key), saved[key])
|
|
107
|
-
}
|
|
108
|
-
return
|
|
109
|
-
}
|
|
110
|
-
doc.replace('', saved)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
114
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
115
|
-
}
|
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
import { decodeResponse } from '../../shared/decodeResponse.ts'
|
|
2
|
-
import type { OutboxEntry, OutboxStatus } from '../../shared/types/OutboxEntry.ts'
|
|
3
|
-
import { UNREACHABLE_STATUSES } from '../../shared/UNREACHABLE_STATUSES.ts'
|
|
4
|
-
import { persist } from '../persist.ts'
|
|
5
|
-
import { createDoc as doc } from '../runtime/createDoc.ts'
|
|
6
|
-
import type { PersistenceStore } from '../types/PersistenceStore.ts'
|
|
7
|
-
|
|
8
|
-
/* Persisted form of an entry — the Request reduced to its replayable parts (a live
|
|
9
|
-
Request + AbortController don't serialize). `body`/`contentType` are captured
|
|
10
|
-
asynchronously just after park (Request.text() is async); they seed empty and fill
|
|
11
|
-
in before any reload, so a restored entry replays with its original body. */
|
|
12
|
-
type StoredEntry<Args> = {
|
|
13
|
-
id: string
|
|
14
|
-
args: Args
|
|
15
|
-
method: string
|
|
16
|
-
url: string
|
|
17
|
-
body: string
|
|
18
|
-
contentType: string
|
|
19
|
-
status: OutboxStatus
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/* Non-serializable identity for a live entry: its abort handle, the in-session Request
|
|
23
|
-
(body intact), and the per-entry retry trigger. Rebuilt from the persisted item on a
|
|
24
|
-
cold read after reload. */
|
|
25
|
-
type Identity = { controller: AbortController; request: Request; retry: () => Promise<void> }
|
|
26
|
-
|
|
27
|
-
export type OutboxQueue<Args> = {
|
|
28
|
-
park: (args: Args, request: Request, reason?: unknown) => OutboxEntry<Args>
|
|
29
|
-
entries: () => OutboxEntry<Args>[]
|
|
30
|
-
/* Undelivered-entry count — the same reactive read as `entries()`, without building
|
|
31
|
-
the entry objects. A caller parks straight to the tail when this is non-empty. */
|
|
32
|
-
size: () => number
|
|
33
|
-
retry: () => Promise<void>
|
|
34
|
-
drain: () => Promise<void>
|
|
35
|
-
dispose: () => void
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/* Persistence key per RPC url — the queue's durable identity. */
|
|
39
|
-
const keyFor = (url: string): string => `abide:outbox:${url}`
|
|
40
|
-
|
|
41
|
-
/*
|
|
42
|
-
A durable, app-owned FIFO retry queue for one RPC. A call fetches directly and throws as
|
|
43
|
-
before while the queue is empty; when the server can't be reached — a transport failure or
|
|
44
|
-
a 502/503/504/520–527/530 — the caller `park`s the request here (a side-effect; the call still
|
|
45
|
-
throws), and once a backlog exists the caller parks every later call at the tail too, so
|
|
46
|
-
FIFO order is preserved on replay. Built on `doc` + `persist`, so the queue IS a persisted
|
|
47
|
-
document that survives a reload. Draining is manual — the app calls `retry()` (per-entry or
|
|
48
|
-
whole-queue) when it decides to replay; nothing drains automatically. The drain re-sends
|
|
49
|
-
head-first: any server response removes the entry — a 2xx (delivered) AND a real 4xx/500
|
|
50
|
-
(the server handled+rejected it; retrying won't help, so it leaves the queue) — while a
|
|
51
|
-
still-unreachable result keeps it `queued` for the next `retry()`, and an abort removes it.
|
|
52
|
-
Resends run under the entry's OWN abort signal alone — scope-abort and the client timeout
|
|
53
|
-
are deliberately not composed in, so a parked write survives an unmount and waits out the
|
|
54
|
-
outage.
|
|
55
|
-
*/
|
|
56
|
-
export function createOutboxQueue<Args>(opts: {
|
|
57
|
-
url: string
|
|
58
|
-
send: (request: Request) => Promise<Response>
|
|
59
|
-
store?: PersistenceStore
|
|
60
|
-
}): OutboxQueue<Args> {
|
|
61
|
-
const stored = doc({ items: [] as StoredEntry<Args>[] })
|
|
62
|
-
const persistence = persist(stored, keyFor(opts.url), { store: opts.store, debounce: 0 })
|
|
63
|
-
/* Live identity (controller + Request + retry) by id, rebuilt from persisted items on
|
|
64
|
-
a cold read; `errors` is transient last-failure state (never persisted). */
|
|
65
|
-
const live = new Map<string, Identity>()
|
|
66
|
-
const errors = new Map<string, unknown>()
|
|
67
|
-
/* Per-entry `settled` deferreds, created lazily on first read of `entry.settled` (so a
|
|
68
|
-
never-awaited refusal can't become an unhandled rejection) and resolved/rejected once
|
|
69
|
-
at the drain exit. Transient — a reload starts fresh. */
|
|
70
|
-
type Settler = {
|
|
71
|
-
promise: Promise<unknown>
|
|
72
|
-
resolve: (value: unknown) => void
|
|
73
|
-
reject: (error: unknown) => void
|
|
74
|
-
}
|
|
75
|
-
const settlers = new Map<string, Settler>()
|
|
76
|
-
|
|
77
|
-
/* The lazily-armed `settled` promise for an id: the outcome of this write as if the
|
|
78
|
-
original call had reached the server. */
|
|
79
|
-
const settledFor = (id: string): Promise<unknown> => {
|
|
80
|
-
const existing = settlers.get(id)
|
|
81
|
-
if (existing !== undefined) {
|
|
82
|
-
return existing.promise
|
|
83
|
-
}
|
|
84
|
-
let resolve!: (value: unknown) => void
|
|
85
|
-
let reject!: (error: unknown) => void
|
|
86
|
-
const promise = new Promise<unknown>((res, rej) => {
|
|
87
|
-
resolve = res
|
|
88
|
-
reject = rej
|
|
89
|
-
})
|
|
90
|
-
settlers.set(id, { promise, resolve, reject })
|
|
91
|
-
return promise
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/* Settle an id's deferred (if armed) and drop it — single-shot. */
|
|
95
|
-
const settle = (id: string, run: (settler: Settler) => void): void => {
|
|
96
|
-
const settler = settlers.get(id)
|
|
97
|
-
if (settler !== undefined) {
|
|
98
|
-
run(settler)
|
|
99
|
-
settlers.delete(id)
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const items = (): StoredEntry<Args>[] => stored.read('items') as StoredEntry<Args>[]
|
|
104
|
-
|
|
105
|
-
const remove = (id: string): void => {
|
|
106
|
-
const index = items().findIndex((item) => item.id === id)
|
|
107
|
-
if (index !== -1) {
|
|
108
|
-
stored.remove(`items/${index}`)
|
|
109
|
-
persistence.flush()
|
|
110
|
-
}
|
|
111
|
-
live.delete(id)
|
|
112
|
-
errors.delete(id)
|
|
113
|
-
/* A cancel/supersede removal with nobody having delivered first → reject `settled`
|
|
114
|
-
as a canceled call would. Delivery/refusal settle before calling remove, so this
|
|
115
|
-
is a no-op for them (single-shot). */
|
|
116
|
-
settle(id, (s) => s.reject(new DOMException('The outbox entry was canceled', 'AbortError')))
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const setStatus = (id: string, status: OutboxStatus): void => {
|
|
120
|
-
const index = items().findIndex((item) => item.id === id)
|
|
121
|
-
if (index !== -1) {
|
|
122
|
-
stored.replace(`items/${index}/status`, status)
|
|
123
|
-
persistence.flush()
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/* The non-serializable identity for an id: reuse the live one (Request body intact in
|
|
128
|
-
session), else rebuild from the persisted parts (fresh controller, cold reload). */
|
|
129
|
-
const identityFor = (item: StoredEntry<Args>): Identity => {
|
|
130
|
-
const existing = live.get(item.id)
|
|
131
|
-
if (existing !== undefined) {
|
|
132
|
-
return existing
|
|
133
|
-
}
|
|
134
|
-
const controller = new AbortController()
|
|
135
|
-
controller.signal.addEventListener('abort', () => remove(item.id), { once: true })
|
|
136
|
-
/* `body || undefined` so a bodyless method rebuilds without one (a Request with a
|
|
137
|
-
body-carrying method + no body is fine; GET/DELETE with a body throws). */
|
|
138
|
-
const request = new Request(item.url, {
|
|
139
|
-
method: item.method,
|
|
140
|
-
body: item.body || undefined,
|
|
141
|
-
headers: item.contentType ? { 'content-type': item.contentType } : undefined,
|
|
142
|
-
})
|
|
143
|
-
/* Per-entry `retry()` kicks a FIFO drain — every entry is `queued`, so it resends
|
|
144
|
-
head-first and reaches this one in order. */
|
|
145
|
-
const identity: Identity = { controller, request, retry: () => drain() }
|
|
146
|
-
live.set(item.id, identity)
|
|
147
|
-
return identity
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const toLive = (item: StoredEntry<Args>): OutboxEntry<Args> => {
|
|
151
|
-
const { controller, request, retry } = identityFor(item)
|
|
152
|
-
return {
|
|
153
|
-
id: item.id,
|
|
154
|
-
controller,
|
|
155
|
-
request,
|
|
156
|
-
args: item.args,
|
|
157
|
-
status: item.status,
|
|
158
|
-
error: errors.get(item.id),
|
|
159
|
-
retry,
|
|
160
|
-
/* Lazy: the deferred is only armed when someone reads `settled`, so an
|
|
161
|
-
unawaited refusal can't surface as an unhandled rejection. */
|
|
162
|
-
get settled() {
|
|
163
|
-
return settledFor(item.id)
|
|
164
|
-
},
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/* Persist the request body + content type just after park (Request.text() is async).
|
|
169
|
-
The in-session drain replays the LIVE request (body intact) regardless; this fills
|
|
170
|
-
the persisted form before any reload. A bodyless method leaves the empty seed. */
|
|
171
|
-
const captureRequest = async (id: string, request: Request): Promise<void> => {
|
|
172
|
-
const body = request.body === null ? '' : await request.clone().text()
|
|
173
|
-
const contentType = request.headers.get('content-type') ?? ''
|
|
174
|
-
const index = items().findIndex((item) => item.id === id)
|
|
175
|
-
if (index === -1) {
|
|
176
|
-
return
|
|
177
|
-
}
|
|
178
|
-
stored.replace(`items/${index}/body`, body)
|
|
179
|
-
stored.replace(`items/${index}/contentType`, contentType)
|
|
180
|
-
persistence.flush()
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/* Resends run under the entry's OWN signal only — a parked write survives unmount +
|
|
184
|
-
waits out the outage, so scope-abort + client-timeout are NOT composed in. */
|
|
185
|
-
const sendable = (entry: OutboxEntry<Args>): Request =>
|
|
186
|
-
new Request(entry.request, { signal: entry.controller.signal })
|
|
187
|
-
|
|
188
|
-
let draining = false
|
|
189
|
-
let settled: Promise<void> = Promise.resolve()
|
|
190
|
-
const drain = (): Promise<void> => {
|
|
191
|
-
if (draining) {
|
|
192
|
-
return settled
|
|
193
|
-
}
|
|
194
|
-
draining = true
|
|
195
|
-
settled = (async () => {
|
|
196
|
-
try {
|
|
197
|
-
while (true) {
|
|
198
|
-
const head = items()[0]
|
|
199
|
-
if (head === undefined) {
|
|
200
|
-
break
|
|
201
|
-
}
|
|
202
|
-
const entry = toLive(head)
|
|
203
|
-
if (entry.controller.signal.aborted) {
|
|
204
|
-
remove(head.id) // canceled while queued
|
|
205
|
-
continue
|
|
206
|
-
}
|
|
207
|
-
setStatus(head.id, 'sending')
|
|
208
|
-
try {
|
|
209
|
-
const response = await opts.send(sendable(entry))
|
|
210
|
-
if (UNREACHABLE_STATUSES.has(response.status)) {
|
|
211
|
-
setStatus(head.id, 'queued') // still down → wait for the next trigger
|
|
212
|
-
break
|
|
213
|
-
}
|
|
214
|
-
/* The server responded — 2xx OR a real 4xx/500. Either way it
|
|
215
|
-
HANDLED the request (the outbox only holds the unreachable), so
|
|
216
|
-
the entry leaves the queue and the next proceeds in order. Decode
|
|
217
|
-
it exactly as a live call would so `settled` resolves with the
|
|
218
|
-
result, or rejects with the real HttpError on a refusal. */
|
|
219
|
-
try {
|
|
220
|
-
const value = await decodeResponse(response)
|
|
221
|
-
settle(head.id, (s) => s.resolve(value))
|
|
222
|
-
} catch (responseError) {
|
|
223
|
-
settle(head.id, (s) => s.reject(responseError))
|
|
224
|
-
}
|
|
225
|
-
remove(head.id)
|
|
226
|
-
continue
|
|
227
|
-
} catch {
|
|
228
|
-
if (entry.controller.signal.aborted) {
|
|
229
|
-
remove(head.id) // canceled mid-send
|
|
230
|
-
continue
|
|
231
|
-
}
|
|
232
|
-
/* Transport failure on resend — still unreachable, keep queued. */
|
|
233
|
-
setStatus(head.id, 'queued')
|
|
234
|
-
break
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
} finally {
|
|
238
|
-
draining = false
|
|
239
|
-
}
|
|
240
|
-
})()
|
|
241
|
-
return settled
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return {
|
|
245
|
-
park(args, request, reason) {
|
|
246
|
-
const id = crypto.randomUUID()
|
|
247
|
-
const controller = new AbortController()
|
|
248
|
-
controller.signal.addEventListener('abort', () => remove(id), { once: true })
|
|
249
|
-
live.set(id, { controller, request, retry: () => drain() })
|
|
250
|
-
if (reason !== undefined) {
|
|
251
|
-
errors.set(id, reason)
|
|
252
|
-
}
|
|
253
|
-
stored.add('items/-', {
|
|
254
|
-
id,
|
|
255
|
-
args,
|
|
256
|
-
method: request.method,
|
|
257
|
-
url: request.url,
|
|
258
|
-
body: '',
|
|
259
|
-
contentType: '',
|
|
260
|
-
status: 'queued',
|
|
261
|
-
})
|
|
262
|
-
persistence.flush()
|
|
263
|
-
void captureRequest(id, request)
|
|
264
|
-
const item = items().find((entry) => entry.id === id) as StoredEntry<Args>
|
|
265
|
-
return toLive(item)
|
|
266
|
-
},
|
|
267
|
-
entries() {
|
|
268
|
-
return items().map(toLive)
|
|
269
|
-
},
|
|
270
|
-
size() {
|
|
271
|
-
return items().length
|
|
272
|
-
},
|
|
273
|
-
retry() {
|
|
274
|
-
return drain()
|
|
275
|
-
},
|
|
276
|
-
drain,
|
|
277
|
-
dispose() {
|
|
278
|
-
persistence.dispose()
|
|
279
|
-
},
|
|
280
|
-
}
|
|
281
|
-
}
|