PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.17
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.17
1.10.25 1.10.24 1.10.23 1.10.22 1.10.21 1.10.20 1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 All 169 releases
← All changes | includes/Sync/Sync_Journal.php +77 -25 1.10.21 → 1.10.17 View file →
@@ -37,16 +37,29 @@
37 37 * rows and ~66 ms for ONE online order, measured 2026-09-03 on dev-next.
38 38 * A journal row is a change POINTER (ADR 0033), so one row per order per
39 39 * request carries the same information.
40 40 *
41 - * Capacity one preserves ordering before a different order or other-origin
42 - * row. Static so that guarantee holds across journal instances. The first
43 - * instance needing the queue binds its writer, including after shutdown;
44 - * all instances write the same table. Retaining the hook's order object
45 - * avoids a refetch. See Request_Write_Queue for the queue mechanics.
41 + * Single slot, not a map: a save for a DIFFERENT order flushes the pending
42 + * one first (so a bulk loop never holds rows until process end), which
43 + * means at most one order is ever pending. Static, not per instance: the
44 + * "update row lands before any other-origin row" guarantee must hold for
45 + * whichever `Sync_Journal` instance writes the other row. The slot keeps the
46 + * blog id so a multisite `switch_to_blog()` between save and flush still
47 + * writes to the originating site's table, and the order object the hook
48 + * handed us so the flush never refetches.
49 + *
50 + * Rows land on {@see flush_pending_order_updates()}: at `shutdown` (last,
51 + * after WooCommerce's own shutdown saves), before any other-origin row, or
52 + * when a different order is saved. Once the shutdown flush has run, later
53 + * updates write immediately.
54 + *
55 + * @var array{blog: int, id: int, order: \WC_Abstract_Order|null}|null
46 56 */
47 - private static ?Request_Write_Queue $pending_updates = null;
57 + private static ?array $pending_order_update = null;
48 58
59 + /** Set by the shutdown flush; afterwards updates are written immediately. */
60 + private static bool $shutdown_flushed = false;
61 +
49 62 /**
50 63 * Option-name prefix for the per-object-type lossy-prune watermarks.
51 64 *
52 65 * The watermark is scoped per object type for the same reason heads are
@@ -523,9 +536,9 @@
523 536
524 537 /**
525 538 * Mark an order's `hook:update` row as owed; the row lands on flush.
526 539 *
527 - * See {@see $pending_updates} for why this is deferred. Direct callers
540 + * See {@see $pending_order_updates} for why this is deferred. Direct callers
528 541 * that need an immediate row use {@see record_order_change()}.
529 542 *
530 543 * @param int $order_id Order id from the hook.
531 544 * @param \WC_Abstract_Order|mixed $order Order object from the hook (second
@@ -534,9 +547,27 @@
534 547 * refetch at flush time.
535 548 */
536 549 public function record_order_updated( int $order_id, $order = null ): void {
537 550 $order = $order instanceof \WC_Abstract_Order ? $order : null;
538 - $this->queue()->owe( 'order', $order_id, $order );
551 + if ( self::$shutdown_flushed ) {
552 + // The request boundary has passed (a save triggered by another
553 + // shutdown handler): nothing will flush again, so write now.
554 + $this->record_order_change( $order_id, 'hook:update', false, $order );
555 + return;
556 + }
557 + $blog = get_current_blog_id();
558 + $slot = self::$pending_order_update;
559 + if ( null !== $slot && ( $slot['id'] !== $order_id || $slot['blog'] !== $blog ) ) {
560 + // A different order began: land what is owed so a bulk loop (WP-CLI
561 + // import, Action Scheduler runner) never holds rows until process end.
562 + $this->flush_pending_order_updates();
563 + $slot = null;
564 + }
565 + self::$pending_order_update = array(
566 + 'blog' => $blog,
567 + 'id' => $order_id,
568 + 'order' => $order ?? ( $slot['order'] ?? null ),
569 + );
539 570 }
540 571
541 572 /**
542 573 * Write the owed `hook:update` row, if any.
@@ -545,11 +576,19 @@
545 576 * from the shutdown flush. Safe to call repeatedly: a flushed order is no
546 577 * longer pending.
547 578 */
548 579 public function flush_pending_order_updates(): void {
549 - if ( null !== self::$pending_updates ) {
550 - self::$pending_updates->flush();
580 + $slot = self::$pending_order_update;
581 + if ( null === $slot ) {
582 + return;
551 583 }
584 + self::$pending_order_update = null;
585 + self::in_blog(
586 + $slot['blog'],
587 + function () use ( $slot ): void {
588 + $this->record_order_change( $slot['id'], 'hook:update', false, $slot['order'] );
589 + }
590 + );
552 591 }
553 592
554 593 /**
555 594 * The `shutdown` callback: flush, then write every later update immediately.
@@ -554,33 +593,45 @@
554 593 /**
555 594 * The `shutdown` callback: flush, then write every later update immediately.
556 595 */
557 596 public function flush_pending_order_updates_at_shutdown(): void {
558 - $this->queue()->flush_at_shutdown();
597 + self::$shutdown_flushed = true;
598 + $this->flush_pending_order_updates();
559 599 }
560 600
561 601 /**
562 602 * Discard per-request coalescing state. Tests only: the PHPUnit process
563 - * never reaches `shutdown`, so the static queue would leak between
603 + * never reaches `shutdown`, so the static slot and flag would leak between
564 604 * test cases otherwise.
565 605 *
566 606 * @internal
567 607 */
568 608 public static function reset_request_state(): void {
569 - self::$pending_updates = null;
609 + self::$pending_order_update = null;
610 + self::$shutdown_flushed = false;
570 611 }
571 612
572 - /** Bind the first journal instance to the request's shared queue. */
573 - private function queue(): Request_Write_Queue {
574 - if ( null === self::$pending_updates ) {
575 - self::$pending_updates = new Request_Write_Queue(
576 - 1,
577 - function ( $type, $id, $order ): void {
578 - $this->record_order_change( $id, 'hook:update', false, $order );
579 - }
580 - );
613 + /**
614 + * Run a write under the blog it was recorded on.
615 + *
616 + * The journal table is blog-scoped, so a deferred write must not follow a
617 + * `switch_to_blog()` that happened between the save and the flush.
618 + *
619 + * @param int $blog_id Blog the write belongs to.
620 + * @param callable $write The write.
621 + */
622 + private static function in_blog( int $blog_id, callable $write ): void {
623 + $switch = is_multisite() && get_current_blog_id() !== $blog_id;
624 + if ( $switch ) {
625 + switch_to_blog( $blog_id );
581 626 }
582 - return self::$pending_updates;
627 + try {
628 + $write();
629 + } finally {
630 + if ( $switch ) {
631 + restore_current_blog();
632 + }
633 + }
583 634 }
584 635
585 636 public function record_order_deleted( int $order_id ): void {
586 637 $this->record_order_change( $order_id, 'hook:delete', true );
@@ -636,13 +687,14 @@
636 687 */
637 688 public function record_order_change( int $order_id, string $origin, bool $deleted, $order = null ): bool {
638 689 global $wpdb;
639 690 if ( 'hook:update' !== $origin ) {
640 - if ( 'hook:create' === $origin && $this->queue()->owes( 'order', $order_id ) ) {
691 + $slot = self::$pending_order_update;
692 + if ( 'hook:create' === $origin && null !== $slot && $order_id === $slot['id'] && get_current_blog_id() === $slot['blog'] ) {
641 693 // The Store API saves a checkout-draft several times BEFORE
642 694 // `woocommerce_new_order` fires. Both rows would point at the same
643 695 // live record, so the create row makes the owed update row redundant.
644 - $this->queue()->drop( 'order', $order_id );
696 + self::$pending_order_update = null;
645 697 } else {
646 698 // Land the owed update row FIRST so the stream never reads as
647 699 // delete-then-update (a replay would resurrect a trashed order).
648 700 $this->flush_pending_order_updates();