@poe-platform/safe-fs 0.1.816 → 0.1.818

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/README.md CHANGED
@@ -15,6 +15,9 @@ To bound aggregate pages across multiple live stores, pass the same
15
15
  This shares a 256 KiB resident-page budget; `cache.residentBytes` reports current
16
16
  page retention. Eviction uses each store's filesystem, and each store still owns
17
17
  its `close()` cleanup. The budget excludes caller buffers and backend storage.
18
+ Call `await storage.prepare()` before opening a directory iterator when its
19
+ snapshot includes the scratch directory. It acquires and unlinks the backing
20
+ handle up front, so later spills do not mutate that directory's namespace.
18
21
  `IntegerTable.entries()` replays records in unsigned key order with a fixed
19
22
  16-level node stack and batches of at most 128 records. It accepts zero-filled
20
23
  caller storage with `allocate`, `read` and `write`; mutation during traversal
@@ -9,7 +9,7 @@ export * from "./fs/webdav/index.js";
9
9
  export * from "./bridge/index.js";
10
10
  export * from "./python/index.js";
11
11
  export { parseXml, parseXmlSteps, parseXmlSourceSteps, normalizeXmlChunks, parseXmlStream, XmlLimitError } from "./xml.js";
12
- export type { XmlName, XmlAttribute, XmlContent, XmlElement, XmlLimits, XmlParserFrame, XmlFrameRequest } from "./xml.js";
12
+ export type { XmlName, XmlAttribute, XmlContent, XmlElement, XmlLimits, XmlParserFrame, XmlFrameRequest, XmlNamespaceScope, XmlNamespaceRequest, XmlAttributeRequest, XmlAttributeRecord } from "./xml.js";
13
13
  export * from "./contracts/object.js";
14
14
  export { ObjectAuthority } from "./fs/object-authority.js";
15
15
  export * from "./fs/s3/index.js";
@@ -44,6 +44,8 @@ export declare class PagedStorage {
44
44
  private closing;
45
45
  constructor(context: PagedStorageContext, maxPages?: number, cache?: PagedStorageCache | undefined);
46
46
  allocate(length: number): number;
47
+ /** Acquire and unlink backing storage before observing a directory snapshot. */
48
+ prepare(): Promise<void>;
47
49
  private operation;
48
50
  private spill;
49
51
  private flush;
@@ -73,6 +73,11 @@ export class PagedStorage {
73
73
  this.end += length;
74
74
  return offset;
75
75
  }
76
+ /** Acquire and unlink backing storage before observing a directory snapshot. */
77
+ async prepare() {
78
+ await this.operation(async () => { if (!this.descriptor)
79
+ await this.spill(); });
80
+ }
76
81
  operation(action) {
77
82
  this.signal.throwIfAborted();
78
83
  const execute = () => {
@@ -0,0 +1,47 @@
1
+ export interface XmlAttributeRecord {
2
+ name: string;
3
+ prefix: string;
4
+ localName: string;
5
+ value: string;
6
+ next: number;
7
+ }
8
+ export interface XmlAttributeState {
9
+ reference: number;
10
+ first: number;
11
+ length: number;
12
+ size: number;
13
+ }
14
+ export type XmlAttributeRequest = {
15
+ attributeOperation: 'has' | 'expanded';
16
+ state: XmlAttributeState;
17
+ name: string;
18
+ found?: boolean;
19
+ } | {
20
+ attributeOperation: 'append';
21
+ state: XmlAttributeState;
22
+ attribute: Omit<XmlAttributeRecord, 'next'>;
23
+ result?: XmlAttributeState;
24
+ } | {
25
+ attributeOperation: 'read';
26
+ state: XmlAttributeState;
27
+ reference: number;
28
+ values: boolean;
29
+ result?: XmlAttributeRecord;
30
+ };
31
+ /** Buffered APIs use local collections; external parsing retains only a store
32
+ * handle and one current attribute. Hosts preserve order and last-value recovery. */
33
+ export declare class XmlAttributes {
34
+ private readonly external;
35
+ private state;
36
+ private readonly names;
37
+ private readonly records;
38
+ private readonly expandedNames;
39
+ constructor(external: boolean);
40
+ get length(): number;
41
+ get size(): number;
42
+ get first(): number;
43
+ has(name: string): Generator<XmlAttributeRequest, boolean, void>;
44
+ append(name: string, prefix: string, localName: string, value: string): Generator<XmlAttributeRequest, void, void>;
45
+ read(reference: number, values: boolean): Generator<XmlAttributeRequest, XmlAttributeRecord, void>;
46
+ expanded(name: string): Generator<XmlAttributeRequest, boolean, void>;
47
+ }
@@ -0,0 +1,62 @@
1
+ /** Buffered APIs use local collections; external parsing retains only a store
2
+ * handle and one current attribute. Hosts preserve order and last-value recovery. */
3
+ export class XmlAttributes {
4
+ external;
5
+ state = { reference: 0, first: 0, length: 0, size: 0 };
6
+ names = new Map();
7
+ records = [];
8
+ expandedNames = new Set();
9
+ constructor(external) {
10
+ this.external = external;
11
+ }
12
+ get length() { return this.state.length; }
13
+ get size() { return this.state.size; }
14
+ get first() { return this.state.first; }
15
+ *has(name) {
16
+ if (!this.length)
17
+ return false;
18
+ if (!this.external)
19
+ return this.names.has(name);
20
+ const request = { attributeOperation: 'has', state: this.state, name };
21
+ yield request;
22
+ if (request.found === undefined)
23
+ throw new TypeError('Incomplete XML attribute lookup');
24
+ return request.found;
25
+ }
26
+ *append(name, prefix, localName, value) {
27
+ if (!this.external) {
28
+ this.names.set(name, value);
29
+ this.records.push({ name, prefix, localName });
30
+ this.state = { reference: 0, first: 1, length: this.records.length, size: this.names.size };
31
+ return;
32
+ }
33
+ const request = { attributeOperation: 'append', state: this.state, attribute: { name, prefix, localName, value } };
34
+ yield request;
35
+ if (!request.result)
36
+ throw new TypeError('Incomplete XML attribute append');
37
+ this.state = request.result;
38
+ }
39
+ *read(reference, values) {
40
+ if (!this.external) {
41
+ const attribute = this.records[reference - 1];
42
+ return { ...attribute, value: values ? this.names.get(attribute.name) : '', next: reference < this.length ? reference + 1 : 0 };
43
+ }
44
+ const request = { attributeOperation: 'read', state: this.state, reference, values };
45
+ yield request;
46
+ if (!request.result)
47
+ throw new TypeError('Incomplete XML attribute read');
48
+ return request.result;
49
+ }
50
+ *expanded(name) {
51
+ if (!this.external) {
52
+ const found = this.expandedNames.has(name);
53
+ this.expandedNames.add(name);
54
+ return found;
55
+ }
56
+ const request = { attributeOperation: 'expanded', state: this.state, name };
57
+ yield request;
58
+ if (request.found === undefined)
59
+ throw new TypeError('Incomplete XML expanded attribute lookup');
60
+ return request.found;
61
+ }
62
+ }
@@ -4,6 +4,7 @@ export interface XmlParserFrame {
4
4
  content: XmlContent[] | undefined;
5
5
  name: string;
6
6
  namespaces: Map<string, string>;
7
+ namespaceScope?: XmlNamespaceScope;
7
8
  }
8
9
  export type XmlFrameRequest = {
9
10
  readonly frameOperation: 'push';
@@ -26,3 +27,23 @@ export declare class XmlFrames {
26
27
  push(frame: XmlParserFrame): Generator<XmlFrameRequest, void, void>;
27
28
  pop(): Generator<XmlFrameRequest, XmlParserFrame | undefined, void>;
28
29
  }
30
+ /** A host-owned immutable namespace scope; count includes the built-in xml prefix. */
31
+ export interface XmlNamespaceScope {
32
+ readonly reference: number;
33
+ readonly size: number;
34
+ }
35
+ export type XmlNamespaceRequest = {
36
+ readonly namespaceOperation: 'get';
37
+ readonly scope: XmlNamespaceScope;
38
+ readonly prefix: string;
39
+ value?: string;
40
+ complete?: true;
41
+ } | {
42
+ readonly namespaceOperation: 'set';
43
+ readonly scope: XmlNamespaceScope;
44
+ readonly prefix: string;
45
+ readonly value: string;
46
+ result?: XmlNamespaceScope;
47
+ };
48
+ export declare function namespaceValue(scope: Map<string, string> | XmlNamespaceScope, prefix: string): Generator<XmlNamespaceRequest, string | undefined, void>;
49
+ export declare function bindNamespace(scope: Map<string, string> | XmlNamespaceScope, prefix: string, value: string): Generator<XmlNamespaceRequest, Map<string, string> | XmlNamespaceScope, void>;
@@ -43,3 +43,23 @@ export class XmlFrames {
43
43
  return frame;
44
44
  }
45
45
  }
46
+ export function* namespaceValue(scope, prefix) {
47
+ if (scope instanceof Map)
48
+ return scope.get(prefix);
49
+ const request = { namespaceOperation: 'get', scope, prefix };
50
+ yield request;
51
+ if (!request.complete)
52
+ throw new TypeError('Incomplete XML namespace read');
53
+ return request.value;
54
+ }
55
+ export function* bindNamespace(scope, prefix, value) {
56
+ if (scope instanceof Map) {
57
+ scope.set(prefix, value);
58
+ return scope;
59
+ }
60
+ const request = { namespaceOperation: 'set', scope, prefix, value };
61
+ yield request;
62
+ if (!request.result)
63
+ throw new TypeError('Incomplete XML namespace update');
64
+ return request.result;
65
+ }
@@ -1,5 +1,7 @@
1
- import { type XmlFrameRequest } from "./frames.js";
2
- export type { XmlParserFrame, XmlFrameRequest } from "./frames.js";
1
+ import { type XmlAttributeRequest } from "./attributes.js";
2
+ export type { XmlAttributeRequest, XmlAttributeRecord, XmlAttributeState } from "./attributes.js";
3
+ import { type XmlNamespaceRequest, type XmlFrameRequest } from "./frames.js";
4
+ export type { XmlParserFrame, XmlFrameRequest, XmlNamespaceScope, XmlNamespaceRequest } from "./frames.js";
3
5
  export { normalizeXmlChunks } from "./characters.js";
4
6
  import { type XmlSourceStep } from "./source.js";
5
7
  export type { XmlSourceRead, XmlSourceStep } from "./source.js";
@@ -61,11 +63,18 @@ export interface XmlSourceLimits extends XmlStepLimits {
61
63
  readonly fragmentContent?: boolean;
62
64
  /** Store active parser frames through host-serviced requests. Requires retainTree: false. */
63
65
  readonly storeFrames?: boolean;
66
+ /** Resolve namespace scopes through host-serviced requests. Requires retainTree: false. */
67
+ readonly storeNamespaces?: boolean;
68
+ /** Store attribute collections through host requests and emit resolved attributes individually. */
69
+ readonly storeAttributes?: boolean;
70
+ readonly onAttribute?: (attribute: XmlAttribute, element: XmlElement) => void;
64
71
  }
65
- export type XmlParseStep = XmlSourceStep | XmlFrameRequest;
72
+ export type XmlParseStep = XmlSourceStep | XmlFrameRequest | XmlNamespaceRequest | XmlAttributeRequest;
66
73
  /** Parse a validated, BOM-free, line-normalized UTF-16 source via bounded read requests. */
67
74
  export declare function parseXmlSourceSteps(length: number | undefined, limits?: XmlStepLimits & {
68
75
  readonly storeFrames?: false;
76
+ readonly storeNamespaces?: false;
77
+ readonly storeAttributes?: false;
69
78
  }): Generator<XmlSourceStep, XmlElement, void>;
70
79
  export declare function parseXmlSourceSteps(length: number | undefined, limits: XmlSourceLimits): Generator<XmlParseStep, XmlElement, void>;
71
80
  export declare function parseXmlSteps(input: string, limits?: XmlStepLimits): Generator<number, XmlElement, void>;
@@ -1,4 +1,5 @@
1
- import { XmlFrames } from "./frames.js";
1
+ import { XmlAttributes } from "./attributes.js";
2
+ import { XmlFrames, bindNamespace, namespaceValue } from "./frames.js";
2
3
  import { validCharacter } from "./characters.js";
3
4
  export { normalizeXmlChunks } from "./characters.js";
4
5
  import { XmlSource } from "./source.js";
@@ -255,6 +256,10 @@ export function* parseXmlSourceSteps(length, limits = {}) {
255
256
  validateLimits(limits);
256
257
  if (limits.storeFrames && limits.retainTree !== false)
257
258
  throw new TypeError("Stored XML frames require retainTree: false");
259
+ if (limits.storeAttributes && limits.retainTree !== false)
260
+ throw new TypeError("Stored XML attributes require retainTree: false");
261
+ if (limits.storeNamespaces && limits.retainTree !== false)
262
+ throw new TypeError("Stored XML namespaces require retainTree: false");
258
263
  if (limits.fragmentContent && limits.retainTree !== false)
259
264
  throw new TypeError("XML content fragments require retainTree: false");
260
265
  const source = new XmlSource(length);
@@ -277,6 +282,7 @@ export function* parseXmlSourceSteps(length, limits = {}) {
277
282
  throw new XmlLimitError("maxTextLength", "XML text limit exceeded");
278
283
  textLength += text.length;
279
284
  };
285
+ const initialNamespaces = limits.storeNamespaces ? yield* bindNamespace({ reference: 0, size: 0 }, "xml", xmlNamespace) : new Map([["xml", xmlNamespace]]);
280
286
  const stack = new XmlFrames(limits.storeFrames === true);
281
287
  let root;
282
288
  const prolog = [];
@@ -552,9 +558,9 @@ export function* parseXmlSourceSteps(length, limits = {}) {
552
558
  yield 512;
553
559
  pendingWork -= 512;
554
560
  }
555
- let attributes;
556
- const attrMeta = [];
557
- let namespaces = (yield* stack.peek())?.namespaces ?? new Map([["xml", xmlNamespace]]);
561
+ const attributes = new XmlAttributes(limits.storeAttributes === true);
562
+ const namespaceParent = yield* stack.peek();
563
+ let namespaces = namespaceParent?.namespaceScope ?? namespaceParent?.namespaces ?? (initialNamespaces instanceof Map ? new Map(initialNamespaces) : initialNamespaces);
558
564
  let ownsNamespaces = stack.length === 0;
559
565
  while (!repeated) {
560
566
  const ws = (yield* skipWhitespace());
@@ -572,11 +578,11 @@ export function* parseXmlSourceSteps(length, limits = {}) {
572
578
  yield 512;
573
579
  pendingWork -= 512;
574
580
  }
575
- if (attributes?.has(attribute))
581
+ if ((yield* attributes.has(attribute)))
576
582
  invalid("duplicate attribute");
577
583
  if (++attributeCount > maxAttributes)
578
584
  throw new XmlLimitError("maxAttributes", "XML attribute limit exceeded");
579
- if ((attributes?.size ?? 0) >= maxAttributesPerElement)
585
+ if (attributes.size >= maxAttributesPerElement)
580
586
  throw new XmlLimitError("maxAttributesPerElement", "XML attribute limit exceeded");
581
587
  yield* skipWhitespace();
582
588
  if ((yield* source.slice(offset++, offset)) !== "=")
@@ -609,8 +615,7 @@ export function* parseXmlSourceSteps(length, limits = {}) {
609
615
  pendingWork -= 512;
610
616
  }
611
617
  admitText(value);
612
- (attributes ??= new Map()).set(attribute, value);
613
- attrMeta.push([attribute, attrPrefix, attrLocal]);
618
+ yield* attributes.append(attribute, attrPrefix, attrLocal, value);
614
619
  offset = end + 1;
615
620
  if (attribute === "xmlns" || attribute.startsWith("xmlns:")) {
616
621
  const nsPrefix = attribute === "xmlns" ? "" : attribute.slice(6);
@@ -618,7 +623,7 @@ export function* parseXmlSourceSteps(length, limits = {}) {
618
623
  || (nsPrefix === "xml") !== (value === xmlNamespace)
619
624
  || (nsPrefix !== "" && value === ""))
620
625
  invalid("invalid namespace binding");
621
- if (!ownsNamespaces) {
626
+ if (!ownsNamespaces && namespaces instanceof Map) {
622
627
  const copy = new Map();
623
628
  for (const [key, uri] of namespaces) {
624
629
  copy.set(key, uri);
@@ -627,25 +632,25 @@ export function* parseXmlSourceSteps(length, limits = {}) {
627
632
  namespaces = copy;
628
633
  ownsNamespaces = true;
629
634
  }
630
- namespaces.set(nsPrefix, value);
635
+ namespaces = yield* bindNamespace(namespaces, nsPrefix, value);
631
636
  if (namespaces.size > maxNamespaces)
632
637
  throw new XmlLimitError("maxNamespaces", "XML namespace scope limit exceeded");
633
638
  }
634
639
  }
635
- if (attrMeta.length > 0) {
636
- const expanded = attrMeta.length > 1 ? new Set() : undefined;
637
- for (let i = 0; i < attrMeta.length; i++) {
638
- const [attribute, attrPrefix, attrLocal] = attrMeta[i];
640
+ if (attributes.length > 0) {
641
+ for (let cursor = attributes.first; cursor;) {
642
+ const record = yield* attributes.read(cursor, false);
643
+ cursor = record.next;
644
+ const { name: attribute, prefix: attrPrefix, localName: attrLocal } = record;
639
645
  if (attribute === "xmlns" || attribute.startsWith("xmlns:"))
640
646
  continue;
641
647
  pendingWork += attribute.length;
642
- if (attrPrefix && !namespaces.has(attrPrefix))
648
+ if (attrPrefix && (yield* namespaceValue(namespaces, attrPrefix)) === undefined)
643
649
  invalid("unbound attribute prefix");
644
- if (expanded) {
645
- const key = `${attrPrefix ? namespaces.get(attrPrefix) : ""}\0${attrLocal}`;
646
- if (expanded.has(key))
650
+ if (attributes.length > 1) {
651
+ const key = `${attrPrefix ? (yield* namespaceValue(namespaces, attrPrefix)) : ""}\0${attrLocal}`;
652
+ if (yield* attributes.expanded(key))
647
653
  invalid("duplicate expanded attribute");
648
- expanded.add(key);
649
654
  }
650
655
  }
651
656
  }
@@ -654,23 +659,25 @@ export function* parseXmlSourceSteps(length, limits = {}) {
654
659
  yield 512;
655
660
  pendingWork -= 512;
656
661
  }
657
- if (prefix === "xmlns" || (prefix && !namespaces.has(prefix)))
662
+ if (prefix === "xmlns" || (prefix && (yield* namespaceValue(namespaces, prefix)) === undefined))
658
663
  invalid("unbound element prefix");
659
664
  if (++nodes > maxNodes)
660
665
  throw new XmlLimitError("maxNodes", "XML resource limit exceeded");
661
666
  if (stack.length + 1 > maxDepth)
662
667
  throw new XmlLimitError("maxDepth", "XML resource limit exceeded");
663
- const namespace = namespaces.get(prefix) ?? "";
668
+ const namespace = (yield* namespaceValue(namespaces, prefix)) ?? "";
664
669
  limits.onElement?.({ name, namespace, localName }, (yield* stack.peek())?.element, stack.length + 1);
665
670
  admitContent();
666
671
  const retainedAttributes = [];
667
- if (retainContent && attributes) {
668
- for (let i = 0; i < attrMeta.length; i++) {
669
- const [attribute, attrPrefix, attrLocal] = attrMeta[i];
670
- const value = attributes.get(attribute);
672
+ if (retainContent) {
673
+ for (let cursor = attributes.first; cursor;) {
674
+ const record = yield* attributes.read(cursor, !limits.storeAttributes);
675
+ cursor = record.next;
676
+ const { name: attribute, prefix: attrPrefix, localName: attrLocal, value } = record;
671
677
  admitContent();
672
- retainedAttributes.push({ name: attribute, localName: attrLocal,
673
- namespace: attribute === "xmlns" || attrPrefix === "xmlns" ? xmlnsNamespace : attrPrefix ? namespaces.get(attrPrefix) : "", value });
678
+ if (!limits.storeAttributes)
679
+ retainedAttributes.push({ name: attribute, localName: attrLocal,
680
+ namespace: attribute === "xmlns" || attrPrefix === "xmlns" ? xmlnsNamespace : attrPrefix ? (yield* namespaceValue(namespaces, attrPrefix)) : "", value });
674
681
  pendingWork += attribute.length + 1;
675
682
  }
676
683
  }
@@ -679,7 +686,7 @@ export function* parseXmlSourceSteps(length, limits = {}) {
679
686
  pendingWork -= 512;
680
687
  }
681
688
  const content = retain ? [] : undefined;
682
- const element = { kind: "element", name, namespace, localName, children: [], text: "", content: content ?? emptyContent, attributes: retainContent ? retainedAttributes : emptyAttributes, namespaces: retainContent ? namespaces : emptyNamespaces, ...(root === undefined && retainContent ? { prolog, epilog, ...(declaration === undefined ? {} : { declaration }) } : {}) };
689
+ const element = { kind: "element", name, namespace, localName, children: [], text: "", content: content ?? emptyContent, attributes: retainContent ? retainedAttributes : emptyAttributes, namespaces: retainContent && namespaces instanceof Map ? namespaces : emptyNamespaces, ...(root === undefined && retainContent ? { prolog, epilog, ...(declaration === undefined ? {} : { declaration }) } : {}) };
683
690
  const parent = (yield* stack.peek());
684
691
  if (parent) {
685
692
  if (retainTree) {
@@ -701,11 +708,22 @@ export function* parseXmlSourceSteps(length, limits = {}) {
701
708
  }
702
709
  // Only cache compact, attribute-free spellings. Namespace resolution and
703
710
  // every admission counter still run for each distinct physical element.
704
- if (empty && !attributes && name.length <= 512 && (yield* source.slice(offset - name.length - 3, offset)) === `<${name}/>`)
711
+ if (empty && !attributes.length && name.length <= 512 && (yield* source.slice(offset - name.length - 3, offset)) === `<${name}/>`)
705
712
  previousEmpty = { suffix: name + "/>", name, prefix, localName };
706
713
  limits.events?.({ type: "open", element, parent: parent?.element });
714
+ if (limits.storeAttributes && limits.onAttribute && retainContent) {
715
+ yield 1;
716
+ for (let cursor = attributes.first; cursor;) {
717
+ const attribute = yield* attributes.read(cursor, true);
718
+ cursor = attribute.next;
719
+ const namespace = attribute.name === "xmlns" || attribute.prefix === "xmlns" ? xmlnsNamespace
720
+ : attribute.prefix ? (yield* namespaceValue(namespaces, attribute.prefix)) : "";
721
+ limits.onAttribute({ name: attribute.name, localName: attribute.localName, namespace, value: attribute.value }, element);
722
+ yield 1;
723
+ }
724
+ }
707
725
  if (!empty)
708
- yield* stack.push({ element, content, name, namespaces });
726
+ yield* stack.push({ element, content, name, namespaces: namespaces instanceof Map ? namespaces : new Map(), ...(namespaces instanceof Map ? {} : { namespaceScope: namespaces }) });
709
727
  else
710
728
  limits.events?.({ type: "close", element, parent: parent?.element });
711
729
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poe-platform/safe-fs",
3
- "version": "0.1.816",
3
+ "version": "0.1.818",
4
4
  "description": "Composable filesystem with a portable core and explicit Node adapters",
5
5
  "type": "module",
6
6
  "license": "MIT",