@abide/abide 0.48.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 +85 -59
- package/CHANGELOG.md +216 -0
- package/README.md +21 -10
- package/package.json +15 -4
- package/src/abideLsp.ts +5 -6
- package/src/abideResolverPlugin.ts +197 -169
- package/src/build.ts +79 -56
- package/src/buildCli.ts +3 -5
- package/src/buildDisconnected.ts +2 -5
- package/src/bundleApp.ts +2 -3
- package/src/checkAbide.ts +12 -2
- package/src/compile.ts +2 -4
- 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/installDownloads.ts +13 -3
- package/src/lib/bundle/installMacMenu.ts +13 -3
- package/src/lib/bundle/spawnEmbeddedServer.ts +13 -4
- package/src/lib/bundle/waitForServer.ts +6 -1
- package/src/lib/cli/parseArgvForRpc.ts +36 -9
- package/src/lib/cli/tokenizeArgvFlags.ts +4 -2
- package/src/lib/mcp/createMcpResourceServer.ts +13 -2
- package/src/lib/mcp/mcpTools.ts +15 -6
- package/src/lib/mcp/toolResultFromResponse.ts +40 -12
- 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 +113 -14
- package/src/lib/server/rpc/resolveRpcJsonSchema.ts +28 -0
- package/src/lib/server/rpc/runWithRpcTimeout.ts +12 -1
- 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/finalizeResponse.ts +11 -1
- 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 +24 -16
- 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 +20 -0
- package/src/lib/shared/buildRpcRequest.ts +6 -2
- 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/canonicalJson.ts +24 -1
- package/src/lib/shared/changeAffectsClient.ts +12 -7
- package/src/lib/shared/createCacheStore.ts +31 -8
- package/src/lib/shared/createChannelLog.ts +24 -0
- 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/shared/exitOnBuildFailure.ts +4 -3
- 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/parseEnv.ts +17 -6
- 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/serializeEnv.ts +18 -6
- package/src/lib/shared/snapshotShippable.ts +8 -7
- package/src/lib/shared/snippet.ts +11 -6
- 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/url.ts +5 -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 -7
- 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/bindListenEvent.ts +5 -6
- 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 +52 -13
- package/src/lib/ui/compile/compileShadow.ts +265 -42
- package/src/lib/ui/compile/composeProps.ts +15 -4
- 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 +419 -37
- package/src/lib/ui/compile/expressionIsPrefixEvaluable.ts +19 -0
- package/src/lib/ui/compile/generateBuild.ts +74 -17
- package/src/lib/ui/compile/generateSSR.ts +286 -75
- 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/isWhitespaceText.ts +10 -2
- 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 +51 -10
- 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 -1090
- 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 +26 -40
- package/src/lib/ui/compile/resolveReactiveExport.ts +4 -7
- package/src/lib/ui/compile/scopeCss.ts +27 -4
- 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 +62 -75
- 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/discardBoundary.ts +24 -6
- package/src/lib/ui/dom/disposeRange.ts +2 -1
- package/src/lib/ui/dom/each.ts +41 -12
- package/src/lib/ui/dom/eachAsync.ts +39 -3
- package/src/lib/ui/dom/fillBefore.ts +6 -7
- 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/mergeProps.ts +1 -1
- 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/mountSlot.ts +1 -1
- 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/restProps.ts +2 -2
- package/src/lib/ui/dom/spreadProps.ts +3 -4
- package/src/lib/ui/dom/switchBlock.ts +30 -7
- package/src/lib/ui/dom/tryBlock.ts +233 -70
- 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 +9 -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/props.ts +17 -0
- package/src/lib/ui/remoteProxy.ts +84 -159
- package/src/lib/ui/renderChain.ts +53 -13
- package/src/lib/ui/renderToStream.ts +22 -18
- package/src/lib/ui/resumeSeedScript.ts +1 -1
- package/src/lib/ui/router.ts +86 -53
- package/src/lib/ui/runtime/AsyncCellError.ts +20 -0
- package/src/lib/ui/runtime/CELL_SEED.ts +14 -0
- package/src/lib/ui/runtime/CHILD_PRESENT.ts +2 -2
- 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 +17 -4
- 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 +31 -54
- 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 +33 -1
- 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/scope.ts +11 -0
- package/src/lib/ui/runtime/toTeardown.ts +10 -3
- 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/types/UiProps.ts +6 -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/runtime/withoutHydration.ts +22 -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/state.ts +9 -3
- 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/scopeLabel.ts +0 -21
- package/src/lib/ui/dom/text.ts +0 -20
- package/src/lib/ui/history.ts +0 -101
- package/src/lib/ui/installHotBridge.ts +0 -93
- 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 -28
- 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
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
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { canonicalJson } from '../../shared/canonicalJson.ts'
|
|
2
|
-
import { outboxProbeSlot } from '../../shared/outboxProbeSlot.ts'
|
|
3
|
-
import type { OutboxQueue } from './createOutboxQueue.ts'
|
|
4
|
-
|
|
5
|
-
/* One registered durable RPC: its url, the RemoteFunction it belongs to (tag for the
|
|
6
|
-
global aggregate), and its live queue. */
|
|
7
|
-
type RegisteredOutbox = { url: string; rpc: unknown; queue: OutboxQueue<unknown> }
|
|
8
|
-
|
|
9
|
-
/*
|
|
10
|
-
Client registry of every durable RPC's outbox queue, keyed by url. A durable RPC
|
|
11
|
-
(`outbox: true`) registers on first use so the global `outbox()` aggregate can
|
|
12
|
-
flatten every queue and `pending()` can see queued entries. App-scoped, not component-
|
|
13
|
-
scoped — the queues outlive any mount.
|
|
14
|
-
*/
|
|
15
|
-
const registered = new Map<string, RegisteredOutbox>()
|
|
16
|
-
|
|
17
|
-
export const outboxRegistry = {
|
|
18
|
-
register(url: string, queue: OutboxQueue<unknown>, rpc: unknown): void {
|
|
19
|
-
registered.set(url, { url, rpc, queue })
|
|
20
|
-
},
|
|
21
|
-
get(url: string): OutboxQueue<unknown> | undefined {
|
|
22
|
-
return registered.get(url)?.queue
|
|
23
|
-
},
|
|
24
|
-
all(): RegisteredOutbox[] {
|
|
25
|
-
return [...registered.values()]
|
|
26
|
-
},
|
|
27
|
-
/* Drop every registered queue. App-scoped state outlives any mount, so tests that
|
|
28
|
-
park writes share one process-global registry; reset() gives each its own clean
|
|
29
|
-
slate. Only the in-memory registry is dropped — `queue.dispose()` keeps the
|
|
30
|
-
persisted snapshot (a reload rehydrates it), so a sign-out that must abandon
|
|
31
|
-
unsynced work has to clear persistence separately. */
|
|
32
|
-
reset(): void {
|
|
33
|
-
for (const { queue } of registered.values()) {
|
|
34
|
-
queue.dispose?.()
|
|
35
|
-
}
|
|
36
|
-
registered.clear()
|
|
37
|
-
},
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/* The queues a pending() selector spans: a durable rpc selector carries the queue's
|
|
41
|
-
`url`, so it narrows to that one; the bare form spans every registered queue; any
|
|
42
|
-
other selector (producer fn, tags, subscribable) carries no url and matches none. */
|
|
43
|
-
const queuesFor = (selector: unknown): OutboxQueue<unknown>[] => {
|
|
44
|
-
if (selector === undefined) {
|
|
45
|
-
return outboxRegistry.all().map(({ queue }) => queue)
|
|
46
|
-
}
|
|
47
|
-
const url = (selector as { url?: unknown }).url
|
|
48
|
-
if (typeof url !== 'string') {
|
|
49
|
-
return []
|
|
50
|
-
}
|
|
51
|
-
const queue = outboxRegistry.get(url)
|
|
52
|
-
return queue === undefined ? [] : [queue]
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/* Prober for the shared pending() probe (see outboxProbeSlot). A parked write has no
|
|
56
|
-
value yet, so it counts as pending. Reads queue.entries() (doc-backed), so the probe
|
|
57
|
-
re-runs as writes park, drain, fail, or cancel; optional args narrow to one parked
|
|
58
|
-
call by structural compare — the double-submit guard for a form. The compare uses
|
|
59
|
-
canonicalJson (key-sorted, the encoder keyForRemoteCall keys body args with), not
|
|
60
|
-
JSON.stringify, so a selector's args match a parked entry regardless of key order —
|
|
61
|
-
the same equality the cache-side selector grammar already uses. */
|
|
62
|
-
outboxProbeSlot.probe = (selector, args): boolean => {
|
|
63
|
-
const entries = queuesFor(selector).flatMap((queue) => queue.entries())
|
|
64
|
-
if (args === undefined) {
|
|
65
|
-
return entries.length > 0
|
|
66
|
-
}
|
|
67
|
-
const key = canonicalJson(args)
|
|
68
|
-
return entries.some((entry) => canonicalJson(entry.args) === key)
|
|
69
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { PatchEvent } from './types/PatchEvent.ts'
|
|
2
|
-
|
|
3
|
-
/*
|
|
4
|
-
Process-global tap for every document mutation. `createDoc` emits a `PatchEvent`
|
|
5
|
-
here on each applied patch; cross-cutting consumers (undo history, persistence,
|
|
6
|
-
sync) subscribe. One chokepoint means those features journal a single uniform
|
|
7
|
-
stream instead of each re-deriving change detection. `active` lets the emitter
|
|
8
|
-
skip computing an inverse — its only real cost — when nobody is listening.
|
|
9
|
-
*/
|
|
10
|
-
const listeners = new Set<(event: PatchEvent) => void>()
|
|
11
|
-
|
|
12
|
-
export const PATCH_BUS = {
|
|
13
|
-
/* True only while at least one consumer is attached; gates inverse capture. */
|
|
14
|
-
get active(): boolean {
|
|
15
|
-
return listeners.size > 0
|
|
16
|
-
},
|
|
17
|
-
emit(event: PatchEvent): void {
|
|
18
|
-
for (const listener of listeners) {
|
|
19
|
-
listener(event)
|
|
20
|
-
}
|
|
21
|
-
},
|
|
22
|
-
subscribe(listener: (event: PatchEvent) => void): () => void {
|
|
23
|
-
listeners.add(listener)
|
|
24
|
-
return () => {
|
|
25
|
-
listeners.delete(listener)
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { PATCH_BUS } from './PATCH_BUS.ts'
|
|
2
|
-
import type { Doc } from './types/Doc.ts'
|
|
3
|
-
|
|
4
|
-
/*
|
|
5
|
-
Runs a component `build` and returns its result (the mount handle / disposer)
|
|
6
|
-
alongside the component's own `model` document — the serializable `state` doc,
|
|
7
|
-
needed so a hot swap can carry its value across (see `hotReplace`). The model is
|
|
8
|
-
found, not threaded: a component seeds its `model` first (the desugared
|
|
9
|
-
`const $$model = scope()` + its init `replace` patches (state slots lowered by `lowerDocAccess`) run before any child mounts or
|
|
10
|
-
control-flow blocks), so the FIRST patch announced on the bus during the build names
|
|
11
|
-
it. A component with no `state` mints no model and emits nothing first — `model` is
|
|
12
|
-
then `undefined` and there is nothing to preserve. Used only on the hot path; the
|
|
13
|
-
subscription is torn down with the build.
|
|
14
|
-
*/
|
|
15
|
-
export function captureModelDoc<T>(build: () => T): {
|
|
16
|
-
value: T
|
|
17
|
-
model: Doc | undefined
|
|
18
|
-
} {
|
|
19
|
-
let model: Doc | undefined
|
|
20
|
-
const unsubscribe = PATCH_BUS.subscribe((event) => {
|
|
21
|
-
model ??= event.doc
|
|
22
|
-
})
|
|
23
|
-
try {
|
|
24
|
-
return { value: build(), model }
|
|
25
|
-
} finally {
|
|
26
|
-
unsubscribe()
|
|
27
|
-
}
|
|
28
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { HotInstance } from './types/HotInstance.ts'
|
|
2
|
-
|
|
3
|
-
/*
|
|
4
|
-
Live component instances grouped by module id (`UiComponent.__abideId`).
|
|
5
|
-
`mountChild` adds each instance it mounts while hot reload is active; `hotReplace`
|
|
6
|
-
walks a module's set to swap every instance when that module is edited. The set's
|
|
7
|
-
remove callback (filed with the mounting owner) drops an instance on unmount, so
|
|
8
|
-
no stale record survives to be swapped into a detached host.
|
|
9
|
-
*/
|
|
10
|
-
export const hotInstances: Map<string, Set<HotInstance>> = new Map()
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Whether abide-ui hot-module replacement is active. Off by default, so production
|
|
3
|
-
and ordinary dev render exactly as before — `mountChild` takes the plain path.
|
|
4
|
-
The dev client sets it true once the hot bridge is in place, switching every
|
|
5
|
-
child mount onto the path that records its instance so an edited component can be
|
|
6
|
-
disposed and re-run where it stands.
|
|
7
|
-
*/
|
|
8
|
-
export const hotReloadEnabled: { current: boolean } = { current: false }
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { fillRange } from '../dom/fillRange.ts'
|
|
2
|
-
import { captureModelDoc } from './captureModelDoc.ts'
|
|
3
|
-
import { hotInstances } from './hotInstances.ts'
|
|
4
|
-
import { seedModelDoc } from './seedModelDoc.ts'
|
|
5
|
-
import type { UiComponent } from './types/UiComponent.ts'
|
|
6
|
-
|
|
7
|
-
/*
|
|
8
|
-
Swaps every live instance of an edited component to its new factory. Per instance:
|
|
9
|
-
snapshot its model, dispose the current scope and clear its DOM range (the disposer
|
|
10
|
-
leaves the `start`/`end` markers in place), re-fill the SAME range with the new
|
|
11
|
-
module's `build` and the same props, then re-seed the fresh model from the snapshot —
|
|
12
|
-
so the user's in-progress `state` survives the edit (state above the boundary already
|
|
13
|
-
survives: props are thunks re-reading the parent's live signals). The hot module
|
|
14
|
-
calls this on load with its freshly compiled `next`. Returns whether it swapped at
|
|
15
|
-
least one instance: false (the edited component has none mounted — e.g. a
|
|
16
|
-
router-mounted page, or a hidden branch) tells the caller to fall back to a full
|
|
17
|
-
reload, since nothing on screen would update.
|
|
18
|
-
*/
|
|
19
|
-
export function hotReplace(moduleId: string, next: UiComponent): boolean {
|
|
20
|
-
const set = hotInstances.get(moduleId)
|
|
21
|
-
if (set === undefined || set.size === 0) {
|
|
22
|
-
return false
|
|
23
|
-
}
|
|
24
|
-
for (const instance of set) {
|
|
25
|
-
const saved = instance.model?.snapshot()
|
|
26
|
-
instance.dispose()
|
|
27
|
-
instance.factory = next
|
|
28
|
-
const { value: handle, model } = captureModelDoc(() =>
|
|
29
|
-
fillRange(instance.start, instance.end, next.build, instance.props, instance.label),
|
|
30
|
-
)
|
|
31
|
-
instance.dispose = handle.dispose
|
|
32
|
-
instance.model = model
|
|
33
|
-
if (saved !== undefined && model !== undefined) {
|
|
34
|
-
seedModelDoc(model, saved)
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return true
|
|
38
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { Scope } from '../types/Scope.ts'
|
|
2
|
-
|
|
3
|
-
/*
|
|
4
|
-
Dev-only registry of every live scope, for the inspector's Reactive tab. `scopes`
|
|
5
|
-
stays empty and untouched unless installInspectorBridge flips `enabled` (gated by
|
|
6
|
-
the server-injected `__abideInspect`), so production allocates and tracks nothing.
|
|
7
|
-
createScope adds on construction and removes on dispose; the bridge reconstructs
|
|
8
|
-
the scope forest from each entry's `id` + `parent.id` (the Scope surface exposes
|
|
9
|
-
no children accessor, so the flat set + parent links is the traversal path). One
|
|
10
|
-
mutable singleton object — mirrors `reactiveAbortState`, reached without a barrel.
|
|
11
|
-
*/
|
|
12
|
-
export const liveScopes: { enabled: boolean; scopes: Set<Scope> } = {
|
|
13
|
-
enabled: false,
|
|
14
|
-
scopes: new Set(),
|
|
15
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { decodeRefJson } from '../../shared/decodeRefJson.ts'
|
|
2
|
-
import { encodeRefJson } from '../../shared/encodeRefJson.ts'
|
|
3
|
-
import type { PersistenceStore } from '../types/PersistenceStore.ts'
|
|
4
|
-
|
|
5
|
-
/*
|
|
6
|
-
The default `persist` backend: `localStorage` keyed by the persistence key, using
|
|
7
|
-
the ref-json codec as the wire form (which also clones on decode, so a stored
|
|
8
|
-
snapshot can't alias the live tree). ref-json over plain JSON because a doc
|
|
9
|
-
snapshot can hold the types JSON silently coerces (Date), drops (undefined) or
|
|
10
|
-
throws on (bigint, cycles, shared references) — a throw here is a swallowed save,
|
|
11
|
-
i.e. silent persistence loss. Returns `undefined` where there is no `localStorage`
|
|
12
|
-
— the server, or a browser with storage disabled — which `persist` reads as "stay
|
|
13
|
-
inert". A corrupt or unreadable entry (including one written by an older JSON
|
|
14
|
-
build) loads as `undefined` rather than throwing, so one bad write can't wedge
|
|
15
|
-
boot; the next save rewrites it in ref-json form.
|
|
16
|
-
*/
|
|
17
|
-
export function localStoragePersistence(): PersistenceStore | undefined {
|
|
18
|
-
if (typeof localStorage === 'undefined') {
|
|
19
|
-
return undefined
|
|
20
|
-
}
|
|
21
|
-
return {
|
|
22
|
-
load: (key) => {
|
|
23
|
-
const raw = localStorage.getItem(key)
|
|
24
|
-
if (raw === null) {
|
|
25
|
-
return undefined
|
|
26
|
-
}
|
|
27
|
-
try {
|
|
28
|
-
return decodeRefJson(raw)
|
|
29
|
-
} catch {
|
|
30
|
-
return undefined
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
save: (key, snapshot) => {
|
|
34
|
-
/* Swallow a failed write (QuotaExceededError, storage disabled mid-session) —
|
|
35
|
-
it fires from a debounced flush / pagehide handler with no caller to catch it,
|
|
36
|
-
and a dropped persist must not crash the app. */
|
|
37
|
-
try {
|
|
38
|
-
localStorage.setItem(key, encodeRefJson(snapshot))
|
|
39
|
-
} catch {
|
|
40
|
-
// best-effort persistence
|
|
41
|
-
}
|
|
42
|
-
},
|
|
43
|
-
remove: (key) => {
|
|
44
|
-
localStorage.removeItem(key)
|
|
45
|
-
},
|
|
46
|
-
}
|
|
47
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { hotInstances } from './hotInstances.ts'
|
|
2
|
-
import type { HotInstance } from './types/HotInstance.ts'
|
|
3
|
-
|
|
4
|
-
/*
|
|
5
|
-
Records a live component instance under its module id and returns a remove
|
|
6
|
-
callback. `mountChild` files the remove with the mounting owner, so the instance
|
|
7
|
-
leaves the registry when its parent (or branch/row) tears down — the module's set
|
|
8
|
-
is dropped once empty, leaving no orphan keys.
|
|
9
|
-
*/
|
|
10
|
-
export function registerHotInstance(moduleId: string, instance: HotInstance): () => void {
|
|
11
|
-
let set = hotInstances.get(moduleId)
|
|
12
|
-
if (set === undefined) {
|
|
13
|
-
set = new Set()
|
|
14
|
-
hotInstances.set(moduleId, set)
|
|
15
|
-
}
|
|
16
|
-
set.add(instance)
|
|
17
|
-
return () => {
|
|
18
|
-
set.delete(instance)
|
|
19
|
-
if (set.size === 0) {
|
|
20
|
-
hotInstances.delete(moduleId)
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|