@opencode/util 0.0.0-beta-19275

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.
Files changed (81) hide show
  1. package/dist/activity-calendar.d.ts +21 -0
  2. package/dist/activity-calendar.js +42 -0
  3. package/dist/activity-calendar.test.d.ts +1 -0
  4. package/dist/activity-calendar.test.js +119 -0
  5. package/dist/binary.d.ts +7 -0
  6. package/dist/binary.js +34 -0
  7. package/dist/binary.test.d.ts +1 -0
  8. package/dist/binary.test.js +16 -0
  9. package/dist/bom.d.ts +22 -0
  10. package/dist/bom.js +36 -0
  11. package/dist/bom.test.d.ts +1 -0
  12. package/dist/bom.test.js +19 -0
  13. package/dist/cross-spawn-spawner.d.ts +3 -0
  14. package/dist/cross-spawn-spawner.js +438 -0
  15. package/dist/effect/app-node-platform.d.ts +6 -0
  16. package/dist/effect/app-node-platform.js +11 -0
  17. package/dist/effect/app-node.d.ts +62 -0
  18. package/dist/effect/app-node.js +8 -0
  19. package/dist/effect/layer-node.d.ts +135 -0
  20. package/dist/effect/layer-node.js +156 -0
  21. package/dist/effect/memo-map.d.ts +2 -0
  22. package/dist/effect/memo-map.js +2 -0
  23. package/dist/effect/runtime.d.ts +8 -0
  24. package/dist/effect/runtime.js +16 -0
  25. package/dist/effect/service-use.d.ts +7 -0
  26. package/dist/effect/service-use.js +27 -0
  27. package/dist/effect-flock.d.ts +31 -0
  28. package/dist/effect-flock.js +186 -0
  29. package/dist/encode.d.ts +4 -0
  30. package/dist/encode.js +38 -0
  31. package/dist/encode.test.d.ts +1 -0
  32. package/dist/encode.test.js +23 -0
  33. package/dist/flock.d.ts +30 -0
  34. package/dist/flock.js +273 -0
  35. package/dist/fs-util.d.ts +137 -0
  36. package/dist/fs-util.js +211 -0
  37. package/dist/glob.d.ts +12 -0
  38. package/dist/glob.js +26 -0
  39. package/dist/global-roots.d.ts +8 -0
  40. package/dist/global-roots.js +17 -0
  41. package/dist/global-roots.workerd.d.ts +7 -0
  42. package/dist/global-roots.workerd.js +15 -0
  43. package/dist/global.d.ts +30 -0
  44. package/dist/global.js +54 -0
  45. package/dist/hash.d.ts +4 -0
  46. package/dist/hash.js +12 -0
  47. package/dist/npm-config.d.ts +4 -0
  48. package/dist/npm-config.js +34 -0
  49. package/dist/npm.d.ts +37 -0
  50. package/dist/npm.js +386 -0
  51. package/dist/observability/logging.d.ts +14 -0
  52. package/dist/observability/logging.js +151 -0
  53. package/dist/observability/otlp.d.ts +18 -0
  54. package/dist/observability/otlp.js +76 -0
  55. package/dist/observability/shared.d.ts +1 -0
  56. package/dist/observability/shared.js +7 -0
  57. package/dist/observability.d.ts +13 -0
  58. package/dist/observability.js +37 -0
  59. package/dist/patch.d.ts +43 -0
  60. package/dist/patch.js +331 -0
  61. package/dist/path.d.ts +4 -0
  62. package/dist/path.js +33 -0
  63. package/dist/path.test.d.ts +1 -0
  64. package/dist/path.test.js +36 -0
  65. package/dist/process.d.ts +54 -0
  66. package/dist/process.js +167 -0
  67. package/dist/retry.d.ts +8 -0
  68. package/dist/retry.js +37 -0
  69. package/dist/retry.test.d.ts +1 -0
  70. package/dist/retry.test.js +31 -0
  71. package/dist/runtime/import.bun.d.ts +2 -0
  72. package/dist/runtime/import.bun.js +8 -0
  73. package/dist/runtime/import.node.d.ts +2 -0
  74. package/dist/runtime/import.node.js +62 -0
  75. package/dist/runtime/import.workerd.d.ts +2 -0
  76. package/dist/runtime/import.workerd.js +7 -0
  77. package/dist/runtime-import.d.ts +1 -0
  78. package/dist/runtime-import.js +1 -0
  79. package/dist/session-title-fallback.d.ts +16 -0
  80. package/dist/session-title-fallback.js +23 -0
  81. package/package.json +75 -0
@@ -0,0 +1,135 @@
1
+ import { Brand, Context, Layer } from "effect";
2
+ export * as LayerNode from "./layer-node.js";
3
+ declare const GraphTypeId: unique symbol;
4
+ declare const VarianceTypeId: unique symbol;
5
+ declare const NodeTypeId: unique symbol;
6
+ declare const ReplacementTypeId: unique symbol;
7
+ export type Tag<Name extends string = string> = Name & Brand.Brand<"LayerNode.Tag">;
8
+ export type Graph<A, E = never, T extends Tag | undefined = Tag | undefined> = GraphValue<A, E, T>;
9
+ export type Node<A, E = never, T extends Tag | undefined = undefined> = NodeValue<A, E, T>;
10
+ export type Provider<A, E = never, T extends Tag | undefined = undefined> = ProviderNode<A, E, T>;
11
+ export type Replacement = ReplacementValue;
12
+ export type Replacements = readonly Replacement[];
13
+ type AnyGraph = Graph<never, unknown>;
14
+ type AnyNode = AnyGraph & {
15
+ readonly [NodeTypeId]: unknown;
16
+ readonly tag: Tag | undefined;
17
+ };
18
+ type RuntimeLayer = Layer.Layer<never, unknown, unknown>;
19
+ type GraphList = readonly [] | readonly [AnyGraph, ...AnyGraph[]];
20
+ export type Output<Item> = CommonOutput<Item, Item extends Graph<infer A, unknown> ? A : never>;
21
+ export type Error<Item> = Item extends Graph<never, infer E> ? E : never;
22
+ type GraphTag<Item> = [Item] extends [never] ? undefined : Item extends Graph<never, unknown, infer T> ? T : never;
23
+ type CommonOutput<Item, A> = A extends unknown ? ([Item] extends [Graph<A, unknown>] ? A : never) : never;
24
+ type LayerOutput<Item extends Layer.Any, A = Layer.Success<Item>> = A extends unknown ? [Item] extends [Layer.Layer<A, unknown, unknown>] ? A : never : never;
25
+ type ListOutput<Items extends readonly AnyGraph[], A = never> = [Items] extends [
26
+ readonly [infer Head extends AnyGraph, ...infer Tail extends readonly AnyGraph[]]
27
+ ] ? ListOutput<Tail, A | Output<Head>> : A;
28
+ type Definition = {
29
+ readonly kind: "group";
30
+ readonly name: string;
31
+ readonly dependencies: readonly AnyGraph[];
32
+ } | {
33
+ readonly kind: "unbound";
34
+ readonly name: string;
35
+ } | {
36
+ readonly kind: "layer";
37
+ readonly name: string;
38
+ readonly implementation: RuntimeLayer;
39
+ readonly dependencies: readonly AnyGraph[];
40
+ };
41
+ declare class GraphValue<in A, out E, out T extends Tag | undefined> {
42
+ private readonly graph;
43
+ readonly [VarianceTypeId]: {
44
+ readonly output: (_: A) => void;
45
+ readonly error: () => E;
46
+ readonly tags: () => T;
47
+ };
48
+ readonly [GraphTypeId]: Definition;
49
+ constructor(definition: Definition);
50
+ get name(): string;
51
+ }
52
+ declare class NodeValue<in out A, in out E, in out T extends Tag | undefined> extends GraphValue<A, E, T> {
53
+ readonly tag: T;
54
+ readonly [NodeTypeId]: (_: [A, E, T]) => [A, E, T];
55
+ constructor(definition: Exclude<Definition, {
56
+ kind: "group";
57
+ }>, tag: T);
58
+ /** Replace this declaration, including every dependency on it, without acquiring its old wiring. */
59
+ replace<const Target extends AnyNode | Layer.Any>(this: Node<A, E, T>, replacement: Target & CheckReplacement<A, E, T, Target>): Replacement;
60
+ }
61
+ declare class ProviderNode<in out A, in out E, in out T extends Tag | undefined> extends NodeValue<A, E, T> {
62
+ /** Decorate the implementation while preserving its dependency wiring and service contract. */
63
+ mapLayer(this: Provider<A, E, T>, f: <R>(layer: Layer.Layer<A, E, R>) => Layer.Layer<A, E, R>): Provider<A, E, T>;
64
+ }
65
+ declare class ReplacementValue {
66
+ private readonly checked;
67
+ readonly [ReplacementTypeId]: {
68
+ readonly source: AnyNode;
69
+ readonly target: AnyNode;
70
+ };
71
+ constructor(source: AnyNode, target: AnyNode);
72
+ }
73
+ type CheckErrors<Expected, Actual> = [Exclude<Actual, Expected>] extends [never] ? never : {
74
+ readonly "New replacement errors": Exclude<Actual, Expected>;
75
+ };
76
+ type CheckReplacement<A, E, T, Target> = [ReplacementErrors<A, E, T, Target>] extends [never] ? unknown : ReplacementErrors<A, E, T, Target>;
77
+ type ReplacementErrors<A, E, T, Target> = Target extends AnyNode ? [Exclude<A, Output<Target>>] extends [never] ? [GraphTag<Target>] extends [T] ? CheckErrors<E, Error<Target>> : {
78
+ readonly "Invalid replacement tag": GraphTag<Target>;
79
+ } : {
80
+ readonly "Missing replacement outputs": Exclude<A, Output<Target>>;
81
+ } : Target extends Layer.Layer<A, infer E2, never> ? CheckErrors<E, E2> : {
82
+ readonly "Replacement must be a closed layer": Target;
83
+ };
84
+ type CheckDependencies<Implementation extends Layer.Any, Items extends GraphList> = [
85
+ Exclude<Layer.Services<Implementation>, ListOutput<Items>>
86
+ ] extends [never] ? unknown : {
87
+ readonly "Missing dependencies": Exclude<Layer.Services<Implementation>, ListOutput<Items>>;
88
+ };
89
+ type Identity = {
90
+ readonly service: Context.Service.Any;
91
+ readonly name?: never;
92
+ } | {
93
+ readonly name: string;
94
+ readonly service?: never;
95
+ };
96
+ type CheckLayer<Implementation> = [Implementation] extends [RuntimeLayer] ? unknown : {
97
+ readonly "Layer contract must be preserved": Implementation;
98
+ };
99
+ type TagInput<T> = {
100
+ readonly tag: T;
101
+ } | ([T] extends [undefined] ? {
102
+ readonly tag?: undefined;
103
+ } : never);
104
+ type MakeInput<Implementation extends Layer.Any, Items extends GraphList, T extends Tag | undefined> = Identity & TagInput<T> & {
105
+ readonly layer: Implementation & CheckLayer<NoInfer<Implementation>>;
106
+ readonly deps: Items & CheckDependencies<Implementation, NoInfer<Items>>;
107
+ };
108
+ type DistributiveOmit<A, K extends PropertyKey> = A extends unknown ? Omit<A, K> : never;
109
+ export type TagConfig = Readonly<Record<string, readonly string[]>>;
110
+ type TagNames<Config extends TagConfig> = keyof Config & string;
111
+ type CheckTags<Items extends GraphList, Names extends string> = [
112
+ Exclude<GraphTag<Items[number]>, Tag<Names> | undefined>
113
+ ] extends [never] ? unknown : {
114
+ readonly "Invalid tag dependencies": Exclude<GraphTag<Items[number]>, Tag<Names> | undefined>;
115
+ };
116
+ export interface Tags<Config extends TagConfig> {
117
+ readonly values: {
118
+ readonly [Name in TagNames<Config>]: Tag<Name>;
119
+ };
120
+ readonly make: <Name extends TagNames<Config>>(name: Name) => <const Implementation extends Layer.Any, const Items extends GraphList>(input: DistributiveOmit<MakeInput<Implementation, Items, Tag<Name>>, "tag"> & CheckTags<Items, Name | Extract<Config[Name][number], string>>) => Provider<LayerOutput<Implementation>, Layer.Error<Implementation> | Error<Items[number]>, Tag<Name>>;
121
+ }
122
+ export declare function tags<const Config extends {
123
+ readonly [Name in keyof Config]: readonly (keyof Config & string)[];
124
+ }>(config: Config): Tags<Config>;
125
+ export declare function make<const Implementation extends Layer.Any, const Items extends GraphList, const T extends Tag | undefined = undefined>(input: MakeInput<Implementation, Items, T>): Provider<LayerOutput<Implementation>, Layer.Error<Implementation> | Error<Items[number]>, T>;
126
+ export declare function unbound<R, Shape, const T extends Tag>(service: Context.Key<R, Shape>, tag: T): Node<R, never, T>;
127
+ /** Ordered, associative composition. Only these roots' outputs are exposed; their dependencies remain private. */
128
+ export declare function group<const Items extends readonly AnyGraph[]>(dependencies: Items): Graph<ListOutput<Items>, Error<Items[number]>, GraphTag<Items[number]>>;
129
+ export interface CompileOptions {
130
+ readonly replacements?: Replacements;
131
+ /** Share subgraphs rooted at this tag; give the remaining wiring a fresh map for each build. */
132
+ readonly shared?: Tag;
133
+ }
134
+ /** Resolve the final overrides before validating or acquiring anything. Effect owns acquisition and finalization. */
135
+ export declare function compile<A, E>(root: Graph<A, E>, options?: CompileOptions): Layer.Layer<A, E>;
@@ -0,0 +1,156 @@
1
+ import { Brand, Context, Effect, Layer } from "effect";
2
+ export * as LayerNode from "./layer-node.js";
3
+ const GraphTypeId = Symbol("LayerNode.Graph");
4
+ const VarianceTypeId = Symbol("LayerNode.Variance");
5
+ const NodeTypeId = Symbol("LayerNode.Node");
6
+ const ReplacementTypeId = Symbol("LayerNode.Replacement");
7
+ const makeTag = Brand.nominal();
8
+ class GraphValue {
9
+ [GraphTypeId];
10
+ constructor(definition) {
11
+ this[GraphTypeId] = definition;
12
+ }
13
+ get name() {
14
+ return this[GraphTypeId].name;
15
+ }
16
+ }
17
+ class NodeValue extends GraphValue {
18
+ tag;
19
+ constructor(definition, tag) {
20
+ super(definition);
21
+ this.tag = tag;
22
+ }
23
+ /** Replace this declaration, including every dependency on it, without acquiring its old wiring. */
24
+ replace(replacement) {
25
+ if (replacement instanceof NodeValue) {
26
+ if (replacement.tag !== this.tag)
27
+ throw new Error(`Cannot replace ${this.name} across tags`);
28
+ return new ReplacementValue(this, replacement);
29
+ }
30
+ if (!Layer.isLayer(replacement))
31
+ throw new Error("A replacement must be a node or an Effect Layer");
32
+ return new ReplacementValue(this, makeProvider({ name: this.name, layer: replacement, deps: [] }, this.tag));
33
+ }
34
+ }
35
+ class ProviderNode extends NodeValue {
36
+ /** Decorate the implementation while preserving its dependency wiring and service contract. */
37
+ mapLayer(f) {
38
+ const definition = this[GraphTypeId];
39
+ if (definition.kind !== "layer")
40
+ throw new Error(`Cannot map unbound layer node: ${this.name}`);
41
+ return new ProviderNode({ ...definition, implementation: f(definition.implementation) }, this.tag);
42
+ }
43
+ }
44
+ class ReplacementValue {
45
+ [ReplacementTypeId];
46
+ constructor(source, target) {
47
+ this[ReplacementTypeId] = { source, target };
48
+ }
49
+ }
50
+ export function tags(config) {
51
+ const names = Object.keys(config);
52
+ const values = Object.fromEntries(names.map((name) => [name, makeTag(name)]));
53
+ return {
54
+ values,
55
+ make: (name) => (input) => makeProvider(input, values[name]),
56
+ };
57
+ }
58
+ export function make(input) {
59
+ return makeProvider(input, input.tag);
60
+ }
61
+ function makeProvider(input, tag) {
62
+ if (!Layer.isLayer(input.layer))
63
+ throw new Error("A layer node requires an Effect Layer");
64
+ return new ProviderNode({
65
+ kind: "layer",
66
+ name: input.service !== undefined ? input.service.key : input.name,
67
+ implementation: input.layer,
68
+ dependencies: [...input.deps],
69
+ }, tag);
70
+ }
71
+ export function unbound(service, tag) {
72
+ return new NodeValue({ kind: "unbound", name: service.key }, tag);
73
+ }
74
+ /** Ordered, associative composition. Only these roots' outputs are exposed; their dependencies remain private. */
75
+ export function group(dependencies) {
76
+ return new GraphValue({ kind: "group", name: "group", dependencies: [...dependencies] });
77
+ }
78
+ /** Resolve the final overrides before validating or acquiring anything. Effect owns acquisition and finalization. */
79
+ export function compile(root, options = {}) {
80
+ const shared = options.shared;
81
+ const replacements = new Map(options.replacements?.map((item) => {
82
+ const replacement = item[ReplacementTypeId];
83
+ return [replacement.source, replacement.target];
84
+ }));
85
+ const cache = {
86
+ shared: new Map(),
87
+ local: new Map(),
88
+ };
89
+ const stack = [];
90
+ const definitions = { shared: new Map(), local: new Map() };
91
+ const resolve = (graph, inherited = false) => {
92
+ const definition = graph[GraphTypeId];
93
+ const isShared = inherited || (shared !== undefined && graph instanceof NodeValue && graph.tag === shared);
94
+ const resolved = isShared ? cache.shared : cache.local;
95
+ const cached = resolved.get(graph);
96
+ if (cached)
97
+ return cached;
98
+ const cycle = stack.indexOf(graph);
99
+ if (cycle !== -1) {
100
+ throw new Error(`Cycle detected in layer graph: ${[...stack.slice(cycle), graph].map((item) => item.name).join(" -> ")}`);
101
+ }
102
+ stack.push(graph);
103
+ const replacement = replacements.get(graph);
104
+ const result = (() => {
105
+ if (replacement && replacement !== graph)
106
+ return resolve(replacement, isShared);
107
+ if (definition.kind === "group")
108
+ return definition.dependencies.flatMap((dependency) => resolve(dependency, isShared));
109
+ if (definition.kind === "unbound")
110
+ throw new Error(`Unbound layer node: ${definition.name}`);
111
+ const node = {
112
+ implementation: definition.implementation,
113
+ dependencies: definition.dependencies.flatMap((dependency) => resolve(dependency, isShared)),
114
+ shared: isShared,
115
+ };
116
+ const registry = node.shared ? definitions.shared : definitions.local;
117
+ const existing = registry.get(node.implementation);
118
+ if (existing) {
119
+ if (existing.dependencies.length !== node.dependencies.length ||
120
+ existing.dependencies.some((dependency, index) => dependency !== node.dependencies[index])) {
121
+ throw new Error(`Layer ${definition.name} is wired to different dependencies; use a distinct implementation Layer`);
122
+ }
123
+ return [existing];
124
+ }
125
+ registry.set(node.implementation, node);
126
+ return [node];
127
+ })();
128
+ stack.pop();
129
+ resolved.set(graph, result);
130
+ return result;
131
+ };
132
+ const roots = resolve(root);
133
+ return Layer.fromBuild((memoMap, scope) => {
134
+ const local = shared === undefined ? memoMap : Layer.makeMemoMapUnsafe();
135
+ const ambient = Layer.succeed(Layer.CurrentMemoMap, memoMap);
136
+ const layers = new Map();
137
+ const build = (node) => {
138
+ const cached = layers.get(node);
139
+ if (cached)
140
+ return cached;
141
+ const dependencies = node.dependencies.map(build);
142
+ // Acquisition uses the selected cache, while lazy LayerMaps inherit the enclosing shared cache.
143
+ const implementation = Layer.suspend(() => node.implementation.pipe(Layer.provide([ambient, ...dependencies])));
144
+ const layer = Layer.fromBuild((_, scope) => buildContext(implementation, node.shared ? memoMap : local, scope));
145
+ layers.set(node, layer);
146
+ return layer;
147
+ };
148
+ return buildContext(roots.map(build).reduce((result, layer) => layer.pipe(Layer.provideMerge(result)), Layer.empty), memoMap, scope);
149
+ });
150
+ }
151
+ class BuildResult extends Context.Service()("@opencode/LayerNode/BuildResult") {
152
+ }
153
+ function buildContext(layer, memoMap, scope) {
154
+ // Preserve the exact output context before Effect appends its own memo-map metadata.
155
+ return Layer.buildWithMemoMap(layer.pipe(Layer.flatMap((context) => Layer.succeed(BuildResult, context))), memoMap, scope).pipe(Effect.map(Context.get(BuildResult)));
156
+ }
@@ -0,0 +1,2 @@
1
+ import { Layer } from "effect";
2
+ export declare const memoMap: Layer.MemoMap;
@@ -0,0 +1,2 @@
1
+ import { Layer } from "effect";
2
+ export const memoMap = Layer.makeMemoMapUnsafe();
@@ -0,0 +1,8 @@
1
+ import { Layer, type Context, type Effect } from "effect";
2
+ export declare function makeRuntime<I, S, E>(service: Context.Service<I, S>, layer: Layer.Layer<I, E>): {
3
+ runSync: <A, Err>(fn: (svc: S) => Effect.Effect<A, Err, I>) => A;
4
+ runPromiseExit: <A, Err>(fn: (svc: S) => Effect.Effect<A, Err, I>, options?: Effect.RunOptions) => Promise<import("effect/Exit").Exit<A, E | Err>>;
5
+ runPromise: <A, Err>(fn: (svc: S) => Effect.Effect<A, Err, I>, options?: Effect.RunOptions) => Promise<A>;
6
+ runFork: <A, Err>(fn: (svc: S) => Effect.Effect<A, Err, I>) => import("effect/Fiber").Fiber<A, E | Err>;
7
+ runCallback: <A, Err>(fn: (svc: S) => Effect.Effect<A, Err, I>) => (interruptor?: number | undefined) => void;
8
+ };
@@ -0,0 +1,16 @@
1
+ import { Layer, ManagedRuntime } from "effect";
2
+ import { memoMap } from "./memo-map.js";
3
+ import { Observability } from "../observability.js";
4
+ export function makeRuntime(service, layer) {
5
+ let rt;
6
+ const getRuntime = () => (rt ??= ManagedRuntime.make(Layer.provideMerge(layer, Observability.layer()), {
7
+ memoMap,
8
+ }));
9
+ return {
10
+ runSync: (fn) => getRuntime().runSync(service.use(fn)),
11
+ runPromiseExit: (fn, options) => getRuntime().runPromiseExit(service.use(fn), options),
12
+ runPromise: (fn, options) => getRuntime().runPromise(service.use(fn), options),
13
+ runFork: (fn) => getRuntime().runFork(service.use(fn)),
14
+ runCallback: (fn) => getRuntime().runCallback(service.use(fn)),
15
+ };
16
+ }
@@ -0,0 +1,7 @@
1
+ import { Context, Effect } from "effect";
2
+ type EffectMethod = (...args: ReadonlyArray<never>) => Effect.Effect<unknown, unknown, unknown>;
3
+ type ServiceUse<Identifier, Shape> = {
4
+ readonly [Key in keyof Shape as Shape[Key] extends EffectMethod ? Key : never]: Shape[Key] extends (...args: infer Args) => infer Return ? Args extends ReadonlyArray<unknown> ? Return extends Effect.Effect<infer A, infer E, infer R> ? (...args: Args) => Effect.Effect<A, E, R | Identifier> : never : never : never;
5
+ };
6
+ export declare const serviceUse: <Identifier, Shape>(tag: Context.Service<Identifier, Shape>) => ServiceUse<Identifier, Shape>;
7
+ export {};
@@ -0,0 +1,27 @@
1
+ import { Context, Effect } from "effect";
2
+ export const serviceUse = (tag) => {
3
+ const cache = new Map();
4
+ // This is the only dynamic boundary: TypeScript knows the accessor shape,
5
+ // but Proxy property names are runtime values.
6
+ const access = new Proxy({}, {
7
+ get: (_, key) => {
8
+ if (typeof key !== "string")
9
+ return undefined;
10
+ const cached = cache.get(key);
11
+ if (cached)
12
+ return cached;
13
+ const accessor = (...args) => tag.use((service) => {
14
+ // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- Proxy keys are checked at runtime.
15
+ const method = service[key];
16
+ if (typeof method !== "function")
17
+ return Effect.die(new Error(`Service method not found: ${key}`));
18
+ // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- ServiceUse exposes only Effect-returning methods.
19
+ return method(...args);
20
+ });
21
+ cache.set(key, accessor);
22
+ return accessor;
23
+ },
24
+ });
25
+ // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- Proxy implements the mapped accessor surface lazily.
26
+ return access;
27
+ };
@@ -0,0 +1,31 @@
1
+ import { Context, Effect, Schema } from "effect";
2
+ import type { Scope } from "effect";
3
+ export declare namespace EffectFlock {
4
+ const LockTimeoutError_base: Schema.Class<LockTimeoutError, Schema.TaggedStruct<"LockTimeoutError", {
5
+ readonly key: Schema.String;
6
+ }>, import("effect/Cause").YieldableError>;
7
+ export class LockTimeoutError extends LockTimeoutError_base {
8
+ }
9
+ const LockCompromisedError_base: Schema.Class<LockCompromisedError, Schema.TaggedStruct<"LockCompromisedError", {
10
+ readonly detail: Schema.String;
11
+ }>, import("effect/Cause").YieldableError>;
12
+ export class LockCompromisedError extends LockCompromisedError_base {
13
+ }
14
+ export type LockError = LockTimeoutError | LockCompromisedError;
15
+ export interface Options {
16
+ readonly staleMs?: number;
17
+ readonly timeoutMs?: number;
18
+ }
19
+ export interface Interface {
20
+ readonly acquire: (key: string, dir?: string, options?: Options) => Effect.Effect<void, LockError, Scope.Scope>;
21
+ readonly withLock: {
22
+ (key: string, dir?: string): <A, E, R>(body: Effect.Effect<A, E, R>) => Effect.Effect<A, E | LockError, R>;
23
+ <A, E, R>(body: Effect.Effect<A, E, R>, key: string, dir?: string): Effect.Effect<A, E | LockError, R>;
24
+ };
25
+ }
26
+ const Service_base: Context.ServiceClass<Service, "EffectFlock", Interface>;
27
+ export class Service extends Service_base {
28
+ }
29
+ export const node: import("./effect/layer-node.js").Provider<Service, never, import("./effect/layer-node.js").Tag<"global">>;
30
+ export {};
31
+ }
@@ -0,0 +1,186 @@
1
+ import path from "path";
2
+ import os from "os";
3
+ import { randomUUID } from "crypto";
4
+ import { Context, Effect, Function, Layer, Option, Schedule, Schema } from "effect";
5
+ import { FSUtil } from "./fs-util.js";
6
+ import { Global } from "./global.js";
7
+ import { makeGlobalNode } from "./effect/app-node.js";
8
+ import { Hash } from "./hash.js";
9
+ export var EffectFlock;
10
+ (function (EffectFlock) {
11
+ // ---------------------------------------------------------------------------
12
+ // Errors
13
+ // ---------------------------------------------------------------------------
14
+ class LockTimeoutError extends Schema.TaggedError()("LockTimeoutError", {
15
+ key: Schema.String,
16
+ }) {
17
+ }
18
+ EffectFlock.LockTimeoutError = LockTimeoutError;
19
+ class LockCompromisedError extends Schema.TaggedError()("LockCompromisedError", {
20
+ detail: Schema.String,
21
+ }) {
22
+ }
23
+ EffectFlock.LockCompromisedError = LockCompromisedError;
24
+ class ReleaseError extends Schema.TaggedError()("ReleaseError", {
25
+ detail: Schema.String,
26
+ cause: Schema.optional(Schema.Defect()),
27
+ }) {
28
+ get message() {
29
+ return this.detail;
30
+ }
31
+ }
32
+ /** Internal: signals "lock is held, retry later". Never leaks to callers. */
33
+ class NotAcquired extends Schema.TaggedError()("NotAcquired", {}) {
34
+ }
35
+ // ---------------------------------------------------------------------------
36
+ // Timing defaults
37
+ // ---------------------------------------------------------------------------
38
+ const DEFAULT_STALE_MS = 60_000;
39
+ const DEFAULT_TIMEOUT_MS = 5 * 60_000;
40
+ const BASE_DELAY_MS = 100;
41
+ const MAX_DELAY_MS = 2_000;
42
+ const retrySchedule = (timeoutMs) => Schedule.min([
43
+ Schedule.exponential(BASE_DELAY_MS, 1.7),
44
+ Schedule.spaced(Math.min(MAX_DELAY_MS, Math.max(BASE_DELAY_MS, Math.floor(timeoutMs / 10)))),
45
+ ]).pipe(Schedule.jittered, Schedule.while((meta) => meta.elapsed < timeoutMs));
46
+ // ---------------------------------------------------------------------------
47
+ // Lock metadata schema
48
+ // ---------------------------------------------------------------------------
49
+ const LockMetaJson = Schema.fromJsonString(Schema.Struct({
50
+ token: Schema.String,
51
+ pid: Schema.Number,
52
+ hostname: Schema.String,
53
+ createdAt: Schema.String,
54
+ }));
55
+ const decodeMeta = Schema.decodeUnknownSync(LockMetaJson);
56
+ const encodeMeta = Schema.encodeSync(LockMetaJson);
57
+ class Service extends Context.Service()("EffectFlock") {
58
+ }
59
+ EffectFlock.Service = Service;
60
+ // ---------------------------------------------------------------------------
61
+ // Layer
62
+ // ---------------------------------------------------------------------------
63
+ function wall() {
64
+ return performance.timeOrigin + performance.now();
65
+ }
66
+ const mtimeMs = (info) => Option.getOrElse(info.mtime, () => new Date(0)).getTime();
67
+ const isPathGone = (e) => e.reason._tag === "NotFound" || e.reason._tag === "Unknown";
68
+ const layer = Layer.effect(Service, Effect.gen(function* () {
69
+ const global = yield* Global.Service;
70
+ const fs = yield* FSUtil.Service;
71
+ const lockRoot = path.join(global.state, "locks");
72
+ const hostname = os.hostname();
73
+ const ensuredDirs = new Set();
74
+ // -- helpers (close over fs) --
75
+ const safeStat = (file) => fs.stat(file).pipe(Effect.catchIf(isPathGone, () => Effect.void), Effect.orDie);
76
+ const forceRemove = (target) => fs.remove(target, { recursive: true }).pipe(Effect.ignore);
77
+ /** Atomic mkdir — returns true if created, false if already exists, dies on other errors. */
78
+ const atomicMkdir = (dir) => fs.makeDirectory(dir, { mode: 0o700 }).pipe(Effect.as(true), Effect.catchIf((e) => e.reason._tag === "AlreadyExists", () => Effect.succeed(false)), Effect.orDie);
79
+ /** Write with exclusive create — compromised error if file already exists. */
80
+ const exclusiveWrite = (filePath, content, lockDir, detail) => fs.writeFileString(filePath, content, { flag: "wx" }).pipe(Effect.catch(() => Effect.gen(function* () {
81
+ yield* forceRemove(lockDir);
82
+ return yield* new LockCompromisedError({ detail });
83
+ })));
84
+ const cleanStaleBreaker = Effect.fnUntraced(function* (breakerPath, staleMs) {
85
+ const bs = yield* safeStat(breakerPath);
86
+ if (bs && wall() - mtimeMs(bs) > staleMs)
87
+ yield* forceRemove(breakerPath);
88
+ return false;
89
+ });
90
+ const ensureDir = Effect.fnUntraced(function* (dir) {
91
+ if (ensuredDirs.has(dir))
92
+ return;
93
+ yield* fs.makeDirectory(dir, { recursive: true }).pipe(Effect.orDie);
94
+ ensuredDirs.add(dir);
95
+ });
96
+ const isStale = Effect.fnUntraced(function* (lockDir, heartbeatPath, metaPath, staleMs) {
97
+ const now = wall();
98
+ const hb = yield* safeStat(heartbeatPath);
99
+ if (hb)
100
+ return now - mtimeMs(hb) > staleMs;
101
+ const meta = yield* safeStat(metaPath);
102
+ if (meta)
103
+ return now - mtimeMs(meta) > staleMs;
104
+ const dir = yield* safeStat(lockDir);
105
+ if (!dir)
106
+ return false;
107
+ return now - mtimeMs(dir) > staleMs;
108
+ });
109
+ const tryAcquireLockDir = (lockDir, key, staleMs) => Effect.gen(function* () {
110
+ const token = randomUUID();
111
+ const metaPath = path.join(lockDir, "meta.json");
112
+ const heartbeatPath = path.join(lockDir, "heartbeat");
113
+ // Atomic mkdir — the POSIX lock primitive
114
+ const created = yield* atomicMkdir(lockDir);
115
+ if (!created) {
116
+ if (!(yield* isStale(lockDir, heartbeatPath, metaPath, staleMs)))
117
+ return yield* new NotAcquired();
118
+ // Stale — race for breaker ownership
119
+ const breakerPath = lockDir + ".breaker";
120
+ const claimed = yield* fs.makeDirectory(breakerPath, { mode: 0o700 }).pipe(Effect.as(true), Effect.catchIf((e) => e.reason._tag === "AlreadyExists", () => cleanStaleBreaker(breakerPath, staleMs)), Effect.catchIf(isPathGone, () => Effect.succeed(false)), Effect.orDie);
121
+ if (!claimed)
122
+ return yield* new NotAcquired();
123
+ // We own the breaker — double-check staleness, nuke, recreate
124
+ const recreated = yield* Effect.gen(function* () {
125
+ if (!(yield* isStale(lockDir, heartbeatPath, metaPath, staleMs)))
126
+ return false;
127
+ yield* forceRemove(lockDir);
128
+ return yield* atomicMkdir(lockDir);
129
+ }).pipe(Effect.ensuring(forceRemove(breakerPath)));
130
+ if (!recreated)
131
+ return yield* new NotAcquired();
132
+ }
133
+ // We own the lock dir — write heartbeat + meta with exclusive create
134
+ yield* exclusiveWrite(heartbeatPath, "", lockDir, "heartbeat already existed");
135
+ const metaJson = encodeMeta({ token, pid: process.pid, hostname, createdAt: new Date().toISOString() });
136
+ yield* exclusiveWrite(metaPath, metaJson, lockDir, "meta.json already existed");
137
+ return { token, metaPath, heartbeatPath, lockDir };
138
+ }).pipe(Effect.withSpan("EffectFlock.tryAcquire", {
139
+ attributes: { key },
140
+ }));
141
+ // -- retry wrapper (preserves Handle type) --
142
+ const acquireHandle = (lockfile, key, options) => tryAcquireLockDir(lockfile, key, options.staleMs).pipe(Effect.retry({
143
+ while: (err) => err._tag === "NotAcquired",
144
+ schedule: retrySchedule(options.timeoutMs),
145
+ }), Effect.catchTag("NotAcquired", () => Effect.fail(new LockTimeoutError({ key }))), Effect.timeoutOrElse({
146
+ duration: options.timeoutMs,
147
+ orElse: () => Effect.fail(new LockTimeoutError({ key })),
148
+ }));
149
+ // -- release --
150
+ const release = (handle) => Effect.gen(function* () {
151
+ const raw = yield* fs.readFileString(handle.metaPath).pipe(Effect.catch((err) => {
152
+ if (isPathGone(err))
153
+ return Effect.die(new ReleaseError({ detail: "metadata missing" }));
154
+ return Effect.die(err);
155
+ }));
156
+ const parsed = yield* Effect.try({
157
+ try: () => decodeMeta(raw),
158
+ catch: (cause) => new ReleaseError({ detail: "metadata invalid", cause }),
159
+ }).pipe(Effect.orDie);
160
+ if (parsed.token !== handle.token)
161
+ return yield* Effect.die(new ReleaseError({ detail: "token mismatch" }));
162
+ yield* forceRemove(handle.lockDir);
163
+ });
164
+ // -- build service --
165
+ const acquire = Effect.fn("EffectFlock.acquire")(function* (key, dir, options = {}) {
166
+ const lockDir = dir ?? lockRoot;
167
+ const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
168
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
169
+ yield* ensureDir(lockDir);
170
+ const lockfile = path.join(lockDir, Hash.fast(key) + ".lock");
171
+ // acquireRelease: acquire is uninterruptible, release is guaranteed
172
+ const handle = yield* Effect.acquireRelease(acquireHandle(lockfile, key, { staleMs, timeoutMs }), (handle) => release(handle));
173
+ // Heartbeat fiber — scoped, so it's interrupted before release runs
174
+ yield* Effect.suspend(() => {
175
+ const now = new Date();
176
+ return fs.utimes(handle.heartbeatPath, now, now);
177
+ }).pipe(Effect.ignore, Effect.repeat(Schedule.spaced(Math.max(100, Math.floor(staleMs / 3)))), Effect.forkScoped);
178
+ });
179
+ const withLock = Function.dual((args) => Effect.isEffect(args[0]), (body, key, dir) => Effect.scoped(Effect.gen(function* () {
180
+ yield* acquire(key, dir);
181
+ return yield* body;
182
+ })));
183
+ return Service.of({ acquire, withLock });
184
+ }));
185
+ EffectFlock.node = makeGlobalNode({ service: Service, layer: layer, deps: [Global.node, FSUtil.node] });
186
+ })(EffectFlock || (EffectFlock = {}));
@@ -0,0 +1,4 @@
1
+ export declare function base64Encode(value: string): string;
2
+ export declare function base64Decode(value: string): string;
3
+ export declare function checksum(content: string): string | undefined;
4
+ export declare function sampledChecksum(content: string, limit?: number): string | undefined;
package/dist/encode.js ADDED
@@ -0,0 +1,38 @@
1
+ export function base64Encode(value) {
2
+ const bytes = new TextEncoder().encode(value);
3
+ const binary = Array.from(bytes, (byte) => String.fromCharCode(byte)).join("");
4
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
5
+ }
6
+ export function base64Decode(value) {
7
+ const binary = atob(value.replace(/-/g, "+").replace(/_/g, "/"));
8
+ return new TextDecoder().decode(Uint8Array.from(binary, (character) => character.charCodeAt(0)));
9
+ }
10
+ export function checksum(content) {
11
+ if (!content)
12
+ return;
13
+ let hash = 0x811c9dc5;
14
+ for (let index = 0; index < content.length; index++) {
15
+ hash ^= content.charCodeAt(index);
16
+ hash = Math.imul(hash, 0x01000193);
17
+ }
18
+ return (hash >>> 0).toString(36);
19
+ }
20
+ export function sampledChecksum(content, limit = 500_000) {
21
+ if (!content)
22
+ return;
23
+ if (content.length <= limit)
24
+ return checksum(content);
25
+ const size = 4096;
26
+ return `${content.length}:${[
27
+ 0,
28
+ Math.floor(content.length * 0.25),
29
+ Math.floor(content.length * 0.5),
30
+ Math.floor(content.length * 0.75),
31
+ content.length - size,
32
+ ]
33
+ .map((point) => {
34
+ const start = Math.max(0, Math.min(content.length - size, point - Math.floor(size / 2)));
35
+ return checksum(content.slice(start, start + size)) ?? "";
36
+ })
37
+ .join(":")}`;
38
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { base64Decode, base64Encode, checksum, sampledChecksum } from "./encode.js";
3
+ describe("frontend encoding", () => {
4
+ test("uses unpadded URL-safe UTF-8 base64", () => {
5
+ expect(base64Encode("hello")).toBe("aGVsbG8");
6
+ expect(base64Encode("✓ à la mode")).toBe("4pyTIMOgIGxhIG1vZGU");
7
+ expect(base64Decode("4pyTIMOgIGxhIG1vZGU")).toBe("✓ à la mode");
8
+ expect(base64Decode("dXNlcjpwYXNz")).toBe("user:pass");
9
+ });
10
+ test("rejects invalid base64", () => {
11
+ expect(() => base64Decode("%%%")).toThrow();
12
+ });
13
+ test("keeps stable FNV checksums", () => {
14
+ expect(checksum("")).toBeUndefined();
15
+ expect(checksum("hello")).toBe("m3bicr");
16
+ expect(checksum("✓ à la mode")).toBe("jmczk0");
17
+ });
18
+ test("samples large values without changing the size boundary", () => {
19
+ const value = "abcdef".repeat(100);
20
+ expect(sampledChecksum(value, value.length)).toBe(checksum(value));
21
+ expect(sampledChecksum(value, value.length - 1)).toBe("600:1isj1k5:1isj1k5:1isj1k5:1isj1k5:1isj1k5");
22
+ });
23
+ });